@iblai/iblai-js 2.4.4 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2329,8 +2329,9 @@ async function verifySkillsTabVisible(page) {
2329
2329
  logger.info('Skills section is visible with Agent/Available sub-tabs');
2330
2330
  }
2331
2331
  /**
2332
- * Switch to the "Agent Skills" sub-tab (the agent's own skills, with enable
2333
- * toggles; the default sub-tab) and wait for its panel to render.
2332
+ * Switch to the "Agent Skills" sub-tab (the agent's assigned skills with
2333
+ * enable toggles, paged server-side 10 per page; the default sub-tab) and
2334
+ * wait for its panel to render.
2334
2335
  */
2335
2336
  async function switchToAgentSkillsSubTab(page) {
2336
2337
  await page.getByTestId('agent-skills-tab-agent').click();
@@ -2416,8 +2417,8 @@ async function addSkillToAgent(page, skillName) {
2416
2417
  logger.info(`Added skill "${skillName}" to the agent`);
2417
2418
  }
2418
2419
  /**
2419
- * Verify a catalog row shows the "Added" chip (already on the agent, or
2420
- * shadowed by a same-slug private skill) and offers no Add button.
2420
+ * Verify a catalog row shows the "Added" chip (the skill is assigned to the
2421
+ * agent) and offers no Add button.
2421
2422
  */
2422
2423
  async function verifySkillAdded(page, skillName) {
2423
2424
  const row = availableSkillRow(page, skillName);
@@ -2426,8 +2427,10 @@ async function verifySkillAdded(page, skillName) {
2426
2427
  logger.info(`Skill "${skillName}" shows as Added`);
2427
2428
  }
2428
2429
  /**
2429
- * Open the three-dots actions menu for a skill row. The menu content portals
2430
- * to document.body, so the returned locator is page-level by design.
2430
+ * Open the three-dots actions menu for a skill row on the active sub-tab.
2431
+ * Agent-tab rows offer "Remove from Agent" only; catalog rows offer
2432
+ * Edit/Delete (for editable skills). The menu content portals to
2433
+ * document.body, so the returned locator is page-level by design.
2431
2434
  */
2432
2435
  async function openSkillActionsMenu(page, skillName) {
2433
2436
  const actionsButton = page.getByRole('button', { name: `${skillName} actions` });
@@ -2503,6 +2506,23 @@ async function toggleSkill(page, skillName) {
2503
2506
  await enableSkill(page, skillName);
2504
2507
  return true;
2505
2508
  }
2509
+ /**
2510
+ * Set a skill's platform-wide enabled flag via its "Available Skills"
2511
+ * catalog row Switch (skill-level PATCH — the agent-tab switches PATCH the
2512
+ * assignment instead). No-op when already in the requested state; waits for
2513
+ * the success toast otherwise. Read-only featured rows have no switch.
2514
+ */
2515
+ async function setCatalogSkillEnabled(page, skillName, enabled) {
2516
+ const switchEl = availableSkillRow(page, skillName).getByRole('switch');
2517
+ await test$1.expect(switchEl).toBeVisible({ timeout: 10000 });
2518
+ if ((await switchEl.isChecked()) === enabled) {
2519
+ logger.info(`Catalog skill "${skillName}" already ${enabled ? 'enabled' : 'disabled'}`);
2520
+ return;
2521
+ }
2522
+ await switchEl.click();
2523
+ await test$1.expect(page.getByText(`${skillName} ${enabled ? 'enabled' : 'disabled'}`, { exact: false })).toBeVisible({ timeout: 10000 });
2524
+ logger.info(`${enabled ? 'Enabled' : 'Disabled'} catalog skill "${skillName}"`);
2525
+ }
2506
2526
  /**
2507
2527
  * Open the "New Skill" dialog from the Skills section. Returns the dialog
2508
2528
  * locator — resolve every field from it (the dialog stacks over the
@@ -2573,7 +2593,9 @@ async function createSkill(page, values) {
2573
2593
  logger.info(`Created skill "${values.name}"`);
2574
2594
  }
2575
2595
  /**
2576
- * Open the Edit Skill dialog for a row (lands on the General sub-tab).
2596
+ * Open the Edit Skill dialog for a catalog row (lands on the General
2597
+ * sub-tab). Skill edit/delete actions live on the "Available Skills"
2598
+ * sub-tab — switch there first (`switchToAvailableSkillsSubTab`).
2577
2599
  * Returns the dialog locator — resolve fields and sub-tabs from it.
2578
2600
  */
2579
2601
  async function openEditSkillDialog(page, skillName) {
@@ -2583,7 +2605,8 @@ async function openEditSkillDialog(page, skillName) {
2583
2605
  return waitForDialogReady(page, dialog);
2584
2606
  }
2585
2607
  /**
2586
- * Edit an existing platform-level skill (General sub-tab fields).
2608
+ * Edit an existing platform-level skill (General sub-tab fields) from its
2609
+ * catalog row — switch to the "Available Skills" sub-tab first.
2587
2610
  */
2588
2611
  async function editSkill(page, skillName, updates) {
2589
2612
  const dialog = await openEditSkillDialog(page, skillName);
@@ -2621,8 +2644,8 @@ async function editSkill(page, skillName, updates) {
2621
2644
  logger.info(`Edited skill "${skillName}"`);
2622
2645
  }
2623
2646
  /**
2624
- * Delete a platform-level skill via its row menu, confirming the
2625
- * "Delete Skill" dialog.
2647
+ * Delete a platform-level skill via its catalog row menu (switch to the
2648
+ * "Available Skills" sub-tab first), confirming the "Delete Skill" dialog.
2626
2649
  */
2627
2650
  async function deleteSkill(page, skillName) {
2628
2651
  const menu = await openSkillActionsMenu(page, skillName);
@@ -4359,19 +4382,19 @@ const GRADER_LABELS = {
4359
4382
  both: 'Overall + per Criterion',
4360
4383
  },
4361
4384
  };
4362
- const UI_TIMEOUT = 10000;
4363
- const MUTATION_TIMEOUT = 15000;
4385
+ const UI_TIMEOUT$1 = 10000;
4386
+ const MUTATION_TIMEOUT$1 = 15000;
4364
4387
  /**
4365
4388
  * The edit-agent dialog that hosts the settings tabs. Scoping through the
4366
4389
  * dialog first keeps every subsequent query away from same-named elements
4367
4390
  * elsewhere on the page (nested portals, background page content).
4368
4391
  */
4369
- function editAgentDialog(page) {
4392
+ function editAgentDialog$1(page) {
4370
4393
  return page.getByRole('dialog').filter({ has: page.getByRole('tablist') });
4371
4394
  }
4372
4395
  /** The Grader tab's body, scoped through the edit-agent dialog. */
4373
4396
  function graderTabBody(page) {
4374
- return editAgentDialog(page).getByTestId('grader-tab-body');
4397
+ return editAgentDialog$1(page).getByTestId('grader-tab-body');
4375
4398
  }
4376
4399
  /** The Rubric sub-tab's section within the tab body. */
4377
4400
  function criteriaSection(page) {
@@ -4388,7 +4411,7 @@ function criterionRow(page, name) {
4388
4411
  * edit-agent dialog (host didn't register it, or RBAC hid it).
4389
4412
  */
4390
4413
  async function isGraderTabVisible(page) {
4391
- const tab = editAgentDialog(page).getByRole('tab', {
4414
+ const tab = editAgentDialog$1(page).getByRole('tab', {
4392
4415
  name: GRADER_LABELS.tabName,
4393
4416
  exact: true,
4394
4417
  });
@@ -4405,11 +4428,11 @@ async function isGraderTabVisible(page) {
4405
4428
  * open. Completion is gated on the tab body's testid, not on timing.
4406
4429
  */
4407
4430
  async function switchToGraderTab(page) {
4408
- const dialog = editAgentDialog(page);
4431
+ const dialog = editAgentDialog$1(page);
4409
4432
  const tab = dialog.getByRole('tab', { name: GRADER_LABELS.tabName, exact: true });
4410
- await test$1.expect(tab).toBeVisible({ timeout: UI_TIMEOUT });
4433
+ await test$1.expect(tab).toBeVisible({ timeout: UI_TIMEOUT$1 });
4411
4434
  await tab.click();
4412
- await test$1.expect(graderTabBody(page)).toBeVisible({ timeout: UI_TIMEOUT });
4435
+ await test$1.expect(graderTabBody(page)).toBeVisible({ timeout: UI_TIMEOUT$1 });
4413
4436
  logger.info('Switched to Grader tab');
4414
4437
  }
4415
4438
  /**
@@ -4419,7 +4442,7 @@ async function switchToGraderTab(page) {
4419
4442
  async function switchToGraderSubTab(page, subTab) {
4420
4443
  const body = graderTabBody(page);
4421
4444
  const trigger = body.getByTestId(`grader-sub-tab-${subTab}`);
4422
- await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT });
4445
+ await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT$1 });
4423
4446
  await trigger.click();
4424
4447
  const sectionTestId = {
4425
4448
  setup: 'grader-setup-section',
@@ -4427,13 +4450,13 @@ async function switchToGraderSubTab(page, subTab) {
4427
4450
  results: 'grader-results-section',
4428
4451
  }[subTab];
4429
4452
  const section = body.getByTestId(sectionTestId);
4430
- await test$1.expect(section).toBeVisible({ timeout: UI_TIMEOUT });
4453
+ await test$1.expect(section).toBeVisible({ timeout: UI_TIMEOUT$1 });
4431
4454
  logger.info(`Switched to Grader ${subTab} sub-tab`);
4432
4455
  }
4433
4456
  /** Read the current on/off state of the Grading capability toggle. */
4434
4457
  async function isGradingEnabled(page) {
4435
4458
  const toggle = graderTabBody(page).getByTestId('grader-capability-toggle');
4436
- await test$1.expect(toggle).toBeVisible({ timeout: UI_TIMEOUT });
4459
+ await test$1.expect(toggle).toBeVisible({ timeout: UI_TIMEOUT$1 });
4437
4460
  return (await toggle.getAttribute('aria-checked')) === 'true';
4438
4461
  }
4439
4462
  /**
@@ -4445,35 +4468,35 @@ async function isGradingEnabled(page) {
4445
4468
  async function setGradingEnabled(page, enabled) {
4446
4469
  const body = graderTabBody(page);
4447
4470
  const toggle = body.getByTestId('grader-capability-toggle');
4448
- await test$1.expect(toggle).toBeVisible({ timeout: UI_TIMEOUT });
4471
+ await test$1.expect(toggle).toBeVisible({ timeout: UI_TIMEOUT$1 });
4449
4472
  if ((await toggle.getAttribute('aria-checked')) === String(enabled)) {
4450
4473
  logger.info(`Grading already ${enabled ? 'enabled' : 'disabled'} — no toggle needed`);
4451
4474
  return;
4452
4475
  }
4453
- await test$1.expect(toggle).toBeEnabled({ timeout: UI_TIMEOUT });
4476
+ await test$1.expect(toggle).toBeEnabled({ timeout: UI_TIMEOUT$1 });
4454
4477
  await toggle.click();
4455
4478
  const toastText = enabled ? GRADER_LABELS.toasts.toggleOn : GRADER_LABELS.toasts.toggleOff;
4456
4479
  // .first() is deliberate: rapid toggles can stack identical sonner toasts,
4457
4480
  // and any one of them proves the PATCH resolved.
4458
- await test$1.expect(page.getByText(toastText).first()).toBeVisible({ timeout: MUTATION_TIMEOUT });
4481
+ await test$1.expect(page.getByText(toastText).first()).toBeVisible({ timeout: MUTATION_TIMEOUT$1 });
4459
4482
  await test$1.expect(toggle).toHaveAttribute('aria-checked', String(enabled), {
4460
- timeout: MUTATION_TIMEOUT,
4483
+ timeout: MUTATION_TIMEOUT$1,
4461
4484
  });
4462
- await test$1.expect(body.getByTestId('capability-gate-content')).toHaveAttribute('data-enabled', String(enabled), { timeout: UI_TIMEOUT });
4485
+ await test$1.expect(body.getByTestId('capability-gate-content')).toHaveAttribute('data-enabled', String(enabled), { timeout: UI_TIMEOUT$1 });
4463
4486
  logger.info(`Grading ${enabled ? 'enabled' : 'disabled'}`);
4464
4487
  }
4465
4488
  /** Pick an option in one of the setup form's two Radix selects. */
4466
- async function pickSelectOption(page, triggerTestId, optionLabel) {
4489
+ async function pickSelectOption$1(page, triggerTestId, optionLabel) {
4467
4490
  const trigger = graderTabBody(page).getByTestId(triggerTestId);
4468
- await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT });
4491
+ await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT$1 });
4469
4492
  await trigger.click();
4470
4493
  // Radix renders the listbox in a portal outside the dialog, so the
4471
4494
  // option is looked up by role at page level — the open listbox is the
4472
4495
  // only one in the document.
4473
4496
  const option = page.getByRole('option', { name: optionLabel, exact: true });
4474
- await test$1.expect(option).toBeVisible({ timeout: UI_TIMEOUT });
4497
+ await test$1.expect(option).toBeVisible({ timeout: UI_TIMEOUT$1 });
4475
4498
  await option.click();
4476
- await test$1.expect(option).toBeHidden({ timeout: UI_TIMEOUT });
4499
+ await test$1.expect(option).toBeHidden({ timeout: UI_TIMEOUT$1 });
4477
4500
  }
4478
4501
  /**
4479
4502
  * Fill and persist the Grading setup form (switches to the setup sub-tab
@@ -4485,26 +4508,26 @@ async function saveGraderConfig(page, values) {
4485
4508
  await switchToGraderSubTab(page, 'setup');
4486
4509
  const body = graderTabBody(page);
4487
4510
  if (values.gradingMode) {
4488
- await pickSelectOption(page, 'grader-grading-mode-select', GRADER_LABELS.gradingModeOptions[values.gradingMode]);
4511
+ await pickSelectOption$1(page, 'grader-grading-mode-select', GRADER_LABELS.gradingModeOptions[values.gradingMode]);
4489
4512
  }
4490
4513
  if (values.feedbackMode) {
4491
- await pickSelectOption(page, 'grader-feedback-mode-select', GRADER_LABELS.feedbackModeOptions[values.feedbackMode]);
4514
+ await pickSelectOption$1(page, 'grader-feedback-mode-select', GRADER_LABELS.feedbackModeOptions[values.feedbackMode]);
4492
4515
  }
4493
4516
  if (values.instructions !== undefined) {
4494
4517
  const textarea = body.getByTestId('grader-instructions-textarea');
4495
- await test$1.expect(textarea).toBeVisible({ timeout: UI_TIMEOUT });
4518
+ await test$1.expect(textarea).toBeVisible({ timeout: UI_TIMEOUT$1 });
4496
4519
  await textarea.fill(values.instructions);
4497
4520
  }
4498
4521
  const saveButton = body.getByTestId('grader-save-button');
4499
- await test$1.expect(saveButton).toBeEnabled({ timeout: UI_TIMEOUT });
4522
+ await test$1.expect(saveButton).toBeEnabled({ timeout: UI_TIMEOUT$1 });
4500
4523
  await saveButton.click();
4501
- await test$1.expect(saveButton).toBeDisabled({ timeout: MUTATION_TIMEOUT });
4524
+ await test$1.expect(saveButton).toBeDisabled({ timeout: MUTATION_TIMEOUT$1 });
4502
4525
  logger.info('Saved grader configuration');
4503
4526
  }
4504
4527
  /** Fill the criterion modal's three fields and submit it. */
4505
4528
  async function submitCriterionModal(page, criterion) {
4506
4529
  const modal = page.getByTestId('grader-criterion-modal');
4507
- await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT });
4530
+ await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT$1 });
4508
4531
  await modal.getByLabel(GRADER_LABELS.modal.fields.name, { exact: true }).fill(criterion.name);
4509
4532
  await modal
4510
4533
  .getByLabel(GRADER_LABELS.modal.fields.criteria, { exact: true })
@@ -4513,21 +4536,21 @@ async function submitCriterionModal(page, criterion) {
4513
4536
  .getByLabel(GRADER_LABELS.modal.fields.points, { exact: true })
4514
4537
  .fill(String(criterion.points));
4515
4538
  const saveButton = modal.getByTestId('grader-criterion-modal-save');
4516
- await test$1.expect(saveButton).toBeEnabled({ timeout: UI_TIMEOUT });
4539
+ await test$1.expect(saveButton).toBeEnabled({ timeout: UI_TIMEOUT$1 });
4517
4540
  await saveButton.click();
4518
4541
  // The modal only closes itself after the mutation resolves.
4519
- await test$1.expect(modal).toBeHidden({ timeout: MUTATION_TIMEOUT });
4542
+ await test$1.expect(modal).toBeHidden({ timeout: MUTATION_TIMEOUT$1 });
4520
4543
  }
4521
4544
  /** Open a rubric row's three-dots menu and click one of its actions. */
4522
4545
  async function clickRowMenuAction(page, name, action) {
4523
4546
  const trigger = criterionRow(page, name).getByRole('button', {
4524
4547
  name: GRADER_LABELS.menu.actionsAria(name),
4525
4548
  });
4526
- await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT });
4549
+ await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT$1 });
4527
4550
  await trigger.click();
4528
4551
  // The menu portals to the page root — the open menu is the only one.
4529
4552
  const item = page.getByRole('menuitem', { name: action, exact: true });
4530
- await test$1.expect(item).toBeVisible({ timeout: UI_TIMEOUT });
4553
+ await test$1.expect(item).toBeVisible({ timeout: UI_TIMEOUT$1 });
4531
4554
  await item.click();
4532
4555
  }
4533
4556
  /**
@@ -4539,10 +4562,10 @@ async function clickRowMenuAction(page, name, action) {
4539
4562
  async function addGraderCriterion(page, criterion) {
4540
4563
  await switchToGraderSubTab(page, 'rubric');
4541
4564
  const addButton = criteriaSection(page).getByTestId('grader-add-criterion-button');
4542
- await test$1.expect(addButton).toBeEnabled({ timeout: UI_TIMEOUT });
4565
+ await test$1.expect(addButton).toBeEnabled({ timeout: UI_TIMEOUT$1 });
4543
4566
  await addButton.click();
4544
4567
  await submitCriterionModal(page, criterion);
4545
- await test$1.expect(criterionRow(page, criterion.name)).toBeVisible({ timeout: MUTATION_TIMEOUT });
4568
+ await test$1.expect(criterionRow(page, criterion.name)).toBeVisible({ timeout: MUTATION_TIMEOUT$1 });
4546
4569
  logger.info(`Added rubric criterion "${criterion.name}"`);
4547
4570
  }
4548
4571
  /**
@@ -4555,7 +4578,7 @@ async function editGraderCriterion(page, currentName, updates) {
4555
4578
  await switchToGraderSubTab(page, 'rubric');
4556
4579
  await clickRowMenuAction(page, currentName, GRADER_LABELS.menu.edit);
4557
4580
  await submitCriterionModal(page, updates);
4558
- await test$1.expect(criterionRow(page, updates.name)).toBeVisible({ timeout: MUTATION_TIMEOUT });
4581
+ await test$1.expect(criterionRow(page, updates.name)).toBeVisible({ timeout: MUTATION_TIMEOUT$1 });
4559
4582
  logger.info(`Edited rubric criterion "${currentName}" → "${updates.name}"`);
4560
4583
  }
4561
4584
  /**
@@ -4567,12 +4590,12 @@ async function deleteGraderCriterion(page, name) {
4567
4590
  await switchToGraderSubTab(page, 'rubric');
4568
4591
  await clickRowMenuAction(page, name, GRADER_LABELS.menu.delete);
4569
4592
  const modal = page.getByTestId('grader-criterion-delete-modal');
4570
- await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT });
4593
+ await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT$1 });
4571
4594
  const confirmButton = modal.getByTestId('grader-criterion-delete-confirm');
4572
- await test$1.expect(confirmButton).toBeEnabled({ timeout: UI_TIMEOUT });
4595
+ await test$1.expect(confirmButton).toBeEnabled({ timeout: UI_TIMEOUT$1 });
4573
4596
  await confirmButton.click();
4574
- await test$1.expect(modal).toBeHidden({ timeout: MUTATION_TIMEOUT });
4575
- await test$1.expect(criterionRow(page, name)).toBeHidden({ timeout: MUTATION_TIMEOUT });
4597
+ await test$1.expect(modal).toBeHidden({ timeout: MUTATION_TIMEOUT$1 });
4598
+ await test$1.expect(criterionRow(page, name)).toBeHidden({ timeout: MUTATION_TIMEOUT$1 });
4576
4599
  logger.info(`Deleted rubric criterion "${name}"`);
4577
4600
  }
4578
4601
  /**
@@ -4583,18 +4606,18 @@ async function deleteGraderCriterion(page, name) {
4583
4606
  async function expectLastCriterionDeleteDisabled(page, name) {
4584
4607
  await switchToGraderSubTab(page, 'rubric');
4585
4608
  await test$1.expect(criteriaSection(page).getByTestId('grader-last-criterion-hint')).toBeVisible({
4586
- timeout: UI_TIMEOUT,
4609
+ timeout: UI_TIMEOUT$1,
4587
4610
  });
4588
4611
  const trigger = criterionRow(page, name).getByRole('button', {
4589
4612
  name: GRADER_LABELS.menu.actionsAria(name),
4590
4613
  });
4591
- await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT });
4614
+ await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT$1 });
4592
4615
  await trigger.click();
4593
4616
  const deleteItem = page.getByRole('menuitem', { name: GRADER_LABELS.menu.delete, exact: true });
4594
- await test$1.expect(deleteItem).toBeVisible({ timeout: UI_TIMEOUT });
4617
+ await test$1.expect(deleteItem).toBeVisible({ timeout: UI_TIMEOUT$1 });
4595
4618
  await test$1.expect(deleteItem).toHaveAttribute('aria-disabled', 'true');
4596
4619
  await page.keyboard.press('Escape');
4597
- await test$1.expect(deleteItem).toBeHidden({ timeout: UI_TIMEOUT });
4620
+ await test$1.expect(deleteItem).toBeHidden({ timeout: UI_TIMEOUT$1 });
4598
4621
  }
4599
4622
  /**
4600
4623
  * Assert whether the amber misconfiguration banner is shown (grading on
@@ -4604,7 +4627,7 @@ async function expectLastCriterionDeleteDisabled(page, name) {
4604
4627
  async function expectGraderMisconfiguredWarning(page, visible) {
4605
4628
  const warning = graderTabBody(page).getByTestId('grader-misconfigured-warning');
4606
4629
  if (visible) {
4607
- await test$1.expect(warning).toBeVisible({ timeout: UI_TIMEOUT });
4630
+ await test$1.expect(warning).toBeVisible({ timeout: UI_TIMEOUT$1 });
4608
4631
  }
4609
4632
  else {
4610
4633
  await test$1.expect(warning).toBeHidden();
@@ -4613,7 +4636,7 @@ async function expectGraderMisconfiguredWarning(page, visible) {
4613
4636
  /** Assert the rubric's live "total possible points" readout (Rubric sub-tab). */
4614
4637
  async function expectGraderTotalPoints(page, total) {
4615
4638
  await switchToGraderSubTab(page, 'rubric');
4616
- await test$1.expect(criteriaSection(page).getByText(`Total Possible Points: ${total}`, { exact: true })).toBeVisible({ timeout: UI_TIMEOUT });
4639
+ await test$1.expect(criteriaSection(page).getByText(`Total Possible Points: ${total}`, { exact: true })).toBeVisible({ timeout: UI_TIMEOUT$1 });
4617
4640
  }
4618
4641
  /** The Results sub-tab's section within the tab body. */
4619
4642
  function resultsSection(page) {
@@ -4633,21 +4656,21 @@ function gradeResultRow(page, email) {
4633
4656
  async function filterGradeResultsByEmail(page, email) {
4634
4657
  await switchToGraderSubTab(page, 'results');
4635
4658
  const trigger = resultsSection(page).getByTestId('grader-results-user-filter');
4636
- await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT });
4659
+ await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT$1 });
4637
4660
  await trigger.click();
4638
4661
  const searchBox = resultsSection(page).getByPlaceholder(GRADER_LABELS.results.searchUsersPlaceholder);
4639
- await test$1.expect(searchBox).toBeVisible({ timeout: UI_TIMEOUT });
4662
+ await test$1.expect(searchBox).toBeVisible({ timeout: UI_TIMEOUT$1 });
4640
4663
  await searchBox.fill(email);
4641
4664
  const option = resultsSection(page).getByRole('option', { name: email });
4642
- await test$1.expect(option).toBeVisible({ timeout: UI_TIMEOUT });
4665
+ await test$1.expect(option).toBeVisible({ timeout: UI_TIMEOUT$1 });
4643
4666
  await option.click();
4644
- await test$1.expect(gradeResultRow(page, email)).toBeVisible({ timeout: MUTATION_TIMEOUT });
4667
+ await test$1.expect(gradeResultRow(page, email)).toBeVisible({ timeout: MUTATION_TIMEOUT$1 });
4645
4668
  logger.info(`Filtered grade results by learner "${email}"`);
4646
4669
  }
4647
4670
  /** Assert a grade-result row for the given learner email is visible. */
4648
4671
  async function expectGradeResultRow(page, email) {
4649
4672
  await switchToGraderSubTab(page, 'results');
4650
- await test$1.expect(gradeResultRow(page, email)).toBeVisible({ timeout: UI_TIMEOUT });
4673
+ await test$1.expect(gradeResultRow(page, email)).toBeVisible({ timeout: UI_TIMEOUT$1 });
4651
4674
  }
4652
4675
  /**
4653
4676
  * Override a learner's grade via the row's three-dots menu → Override grade
@@ -4659,10 +4682,10 @@ async function overrideGradeResult(page, email, values) {
4659
4682
  const overrideButton = gradeResultRow(page, email).getByRole('button', {
4660
4683
  name: GRADER_LABELS.results.overrideButtonAria(email),
4661
4684
  });
4662
- await test$1.expect(overrideButton).toBeVisible({ timeout: UI_TIMEOUT });
4685
+ await test$1.expect(overrideButton).toBeVisible({ timeout: UI_TIMEOUT$1 });
4663
4686
  await overrideButton.click();
4664
4687
  const modal = page.getByTestId('grader-override-modal');
4665
- await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT });
4688
+ await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT$1 });
4666
4689
  await modal
4667
4690
  .getByLabel(GRADER_LABELS.results.modal.pointsLabel, { exact: true })
4668
4691
  .fill(String(values.points));
@@ -4672,9 +4695,9 @@ async function overrideGradeResult(page, email, values) {
4672
4695
  .fill(values.feedback);
4673
4696
  }
4674
4697
  const saveButton = modal.getByTestId('grader-override-save');
4675
- await test$1.expect(saveButton).toBeEnabled({ timeout: UI_TIMEOUT });
4698
+ await test$1.expect(saveButton).toBeEnabled({ timeout: UI_TIMEOUT$1 });
4676
4699
  await saveButton.click();
4677
- await test$1.expect(modal).toBeHidden({ timeout: MUTATION_TIMEOUT });
4700
+ await test$1.expect(modal).toBeHidden({ timeout: MUTATION_TIMEOUT$1 });
4678
4701
  logger.info(`Overrode grade for "${email}" with ${values.points} points`);
4679
4702
  }
4680
4703
  /**
@@ -4686,14 +4709,14 @@ async function clearGradeResultOverride(page, email) {
4686
4709
  const overrideButton = gradeResultRow(page, email).getByRole('button', {
4687
4710
  name: GRADER_LABELS.results.overrideButtonAria(email),
4688
4711
  });
4689
- await test$1.expect(overrideButton).toBeVisible({ timeout: UI_TIMEOUT });
4712
+ await test$1.expect(overrideButton).toBeVisible({ timeout: UI_TIMEOUT$1 });
4690
4713
  await overrideButton.click();
4691
4714
  const modal = page.getByTestId('grader-override-modal');
4692
- await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT });
4715
+ await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT$1 });
4693
4716
  const clearButton = modal.getByTestId('grader-override-clear');
4694
- await test$1.expect(clearButton).toBeEnabled({ timeout: UI_TIMEOUT });
4717
+ await test$1.expect(clearButton).toBeEnabled({ timeout: UI_TIMEOUT$1 });
4695
4718
  await clearButton.click();
4696
- await test$1.expect(modal).toBeHidden({ timeout: MUTATION_TIMEOUT });
4719
+ await test$1.expect(modal).toBeHidden({ timeout: MUTATION_TIMEOUT$1 });
4697
4720
  logger.info(`Cleared grade override for "${email}"`);
4698
4721
  }
4699
4722
 
@@ -4892,6 +4915,653 @@ async function clickBillingManageUsage(page) {
4892
4915
  await btn.click();
4893
4916
  }
4894
4917
 
4918
+ /**
4919
+ * Spend limits ("Billing") helpers — Playwright bindings for the LLM
4920
+ * spend-cap admin UI from `@iblai/web-containers`:
4921
+ *
4922
+ * - the **agent settings Billing tab** (`AgentSpendCapsTab`) with its
4923
+ * "This Agent" / "Per User" sub-tabs, the per-user table with a
4924
+ * three-dots row menu, and the add/edit user-limit modal (email-based
4925
+ * user picker), and
4926
+ * - the **tenant settings Billing tab** with its "Plan & Credits" /
4927
+ * "Spend Limits" tabs and the workspace-wide limit section
4928
+ * (`SpendLimitsSection` inside `BillingTab`).
4929
+ *
4930
+ * Selector policy (flakiness-proof by construction):
4931
+ * - Dialog-first scoping: every modal is resolved into a Locator variable
4932
+ * first (`user-cap-modal`, the delete dialogs, the edit-agent dialog, the
4933
+ * Agent Limits manage popup) and all sub-elements are queried from that
4934
+ * variable — never a bare page-wide match that could hit same-named
4935
+ * elements in nested portals.
4936
+ * - Nested dialogs: the tenant Agent Limits tab opens the agent Billing
4937
+ * editor in a popup *on top of* the tenant settings dialog, so several
4938
+ * dialogs with tablists can be open at once. Every agent-scope helper
4939
+ * accepts an optional `scope` Locator (the host dialog) for that case;
4940
+ * `openAgentLimitsManage` returns the popup Locator to pass as `scope`.
4941
+ * - Stable hooks only: `data-testid`, role + accessible name (aria-labels).
4942
+ * No CSS class or structural selectors.
4943
+ * - No `waitForTimeout` / `networkidle`. Progress is gated on UI state that
4944
+ * only exists after the awaited transition: a section testid rendering, a
4945
+ * modal closing after its mutation resolves, a table row
4946
+ * appearing/disappearing after the list refetch, a success toast.
4947
+ */
4948
+ const SPEND_LIMITS_LABELS = {
4949
+ /** Agent-settings modal tab name (host registries label it "Billing"). */
4950
+ tabName: 'Billing',
4951
+ subTabs: {
4952
+ agent: 'This Agent',
4953
+ users: 'Per User',
4954
+ },
4955
+ tenantTabs: {
4956
+ planAndCredits: 'Plan & Credits',
4957
+ spendLimits: 'Spend Limits',
4958
+ agentLimits: 'Agent Limits',
4959
+ },
4960
+ /** aria-label of the tenant Billing tab's tablist. */
4961
+ tenantTablistAria: 'Billing sections',
4962
+ addUserLimitButton: 'Add User Limit',
4963
+ menu: {
4964
+ /**
4965
+ * aria-label template for a table row's three-dots trigger. Takes the
4966
+ * DISPLAYED name — the user's email when the API provides one, username
4967
+ * only as fallback. Helpers locate the trigger by its username-keyed
4968
+ * testid instead (`user-cap-actions-<username>`), which is immune to
4969
+ * that distinction; prefer the testid in new code.
4970
+ */
4971
+ actionsAria: (displayName) => `Actions for ${displayName}`,
4972
+ edit: 'Edit',
4973
+ delete: 'Delete',
4974
+ },
4975
+ toasts: {
4976
+ saved: 'Spend limit saved',
4977
+ deleted: 'Spend limit deleted',
4978
+ },
4979
+ intervalOptions: {
4980
+ day: 'Day',
4981
+ week: 'Week',
4982
+ month: 'Month',
4983
+ year: 'Year',
4984
+ },
4985
+ enforcementOptions: {
4986
+ block: 'Block Requests',
4987
+ alert_only: 'Alert Only',
4988
+ },
4989
+ };
4990
+ const UI_TIMEOUT = 10000;
4991
+ const MUTATION_TIMEOUT = 15000;
4992
+ /**
4993
+ * The dialog that hosts the agent settings tabs. Several dialogs can be
4994
+ * open at once (the Agent Limits manage popup stacks on top of the tenant
4995
+ * settings dialog, and both host a tablist), so this resolves to the
4996
+ * TOPMOST match — stacked Radix dialogs portal to the end of `<body>` in
4997
+ * mount order, making the last match the one on top. Helpers that must
4998
+ * target a specific dialog take an explicit `scope` Locator instead, which
4999
+ * always wins over this default.
5000
+ */
5001
+ function editAgentDialog(page) {
5002
+ return page
5003
+ .getByRole('dialog')
5004
+ .filter({ has: page.getByRole('tablist') })
5005
+ .last();
5006
+ }
5007
+ /**
5008
+ * The agent Billing tab's body. `scope` is the dialog hosting the editor
5009
+ * (e.g. the Locator returned by `openAgentLimitsManage`); defaults to the
5010
+ * topmost settings dialog.
5011
+ */
5012
+ function spendLimitsTabBody(page, scope) {
5013
+ return (scope !== null && scope !== void 0 ? scope : editAgentDialog(page)).getByTestId('spend-caps-tab-body');
5014
+ }
5015
+ /** The per-user limits section within the Billing tab body. */
5016
+ function userCapsSection(page, scope) {
5017
+ return spendLimitsTabBody(page, scope).getByTestId('user-caps-section');
5018
+ }
5019
+ /** A per-user table row for the given username. */
5020
+ function userSpendLimitRow(page, username, scope) {
5021
+ return userCapsSection(page, scope).getByTestId(`user-cap-row-${username}`);
5022
+ }
5023
+ /**
5024
+ * Returns false if the Billing tab isn't currently rendered in the
5025
+ * edit-agent dialog (host didn't register it, or RBAC hid it).
5026
+ */
5027
+ async function isSpendLimitsTabVisible(page) {
5028
+ const tab = editAgentDialog(page).getByRole('tab', {
5029
+ name: SPEND_LIMITS_LABELS.tabName,
5030
+ exact: true,
5031
+ });
5032
+ try {
5033
+ await test$1.expect(tab).toBeVisible({ timeout: 5000 });
5034
+ return true;
5035
+ }
5036
+ catch (_a) {
5037
+ return false;
5038
+ }
5039
+ }
5040
+ /**
5041
+ * Switch to the Billing top-level tab of the edit-agent dialog. Completion
5042
+ * is gated on the tab body's testid, not on timing.
5043
+ */
5044
+ async function switchToSpendLimitsTab(page) {
5045
+ const dialog = editAgentDialog(page);
5046
+ const tab = dialog.getByRole('tab', { name: SPEND_LIMITS_LABELS.tabName, exact: true });
5047
+ await test$1.expect(tab).toBeVisible({ timeout: UI_TIMEOUT });
5048
+ await tab.click();
5049
+ await test$1.expect(spendLimitsTabBody(page)).toBeVisible({ timeout: UI_TIMEOUT });
5050
+ logger.info('Switched to agent Billing (spend limits) tab');
5051
+ }
5052
+ /**
5053
+ * Switch between the Billing tab's two sub-tabs. Completion is gated on the
5054
+ * sub-tab's content rendering: the agent limit form (or its loading/denied
5055
+ * states) for "agent", the per-user section (or its spinner) for "users".
5056
+ * `scope` is the dialog hosting the editor (see `spendLimitsTabBody`).
5057
+ */
5058
+ async function switchToSpendLimitsSubTab(page, subTab, scope) {
5059
+ const body = spendLimitsTabBody(page, scope);
5060
+ const trigger = body.getByTestId(`spend-caps-sub-tab-${subTab}`);
5061
+ await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT });
5062
+ await trigger.click();
5063
+ const content = subTab === 'agent'
5064
+ ? body
5065
+ .getByTestId('agent-cap-form')
5066
+ .or(body.getByTestId('agent-cap-loading'))
5067
+ .or(body.getByTestId('spend-caps-denied'))
5068
+ : body
5069
+ .getByTestId('user-caps-section')
5070
+ .or(body.getByTestId('user-caps-loading'))
5071
+ .or(body.getByTestId('spend-caps-denied'));
5072
+ await test$1.expect(content.first()).toBeVisible({ timeout: UI_TIMEOUT });
5073
+ logger.info(`Switched to spend limits "${subTab}" sub-tab`);
5074
+ }
5075
+ /** Pick an option in one of the limit form's Radix selects. */
5076
+ async function pickSelectOption(scope, page, triggerTestId, label) {
5077
+ const trigger = scope.getByTestId(triggerTestId);
5078
+ await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT });
5079
+ await trigger.click();
5080
+ // Radix renders the listbox in a portal outside the scope — the open
5081
+ // listbox is the only one in the document, so a role-level lookup is safe.
5082
+ const option = page.getByRole('option', { name: label, exact: true });
5083
+ await test$1.expect(option).toBeVisible({ timeout: UI_TIMEOUT });
5084
+ await option.click();
5085
+ await test$1.expect(option).toBeHidden({ timeout: UI_TIMEOUT });
5086
+ }
5087
+ /**
5088
+ * Fill a spend-limit form (any scope — agent, workspace, or the user-limit
5089
+ * modal). Only the provided fields are changed. `testIdPrefix` matches the
5090
+ * form's testId prop: 'agent-cap', 'tenant-cap' or 'user-cap-modal'.
5091
+ */
5092
+ async function fillSpendLimitForm(page, scope, testIdPrefix, values) {
5093
+ if (values.enabled !== undefined) {
5094
+ const toggle = scope.getByTestId(`${testIdPrefix}-enabled-switch`);
5095
+ await test$1.expect(toggle).toBeVisible({ timeout: UI_TIMEOUT });
5096
+ if ((await toggle.getAttribute('aria-checked')) !== String(values.enabled)) {
5097
+ await toggle.click();
5098
+ await test$1.expect(toggle).toHaveAttribute('aria-checked', String(values.enabled), {
5099
+ timeout: UI_TIMEOUT,
5100
+ });
5101
+ }
5102
+ }
5103
+ if (values.limitUsd !== undefined) {
5104
+ const input = scope.getByTestId(`${testIdPrefix}-limit-input`);
5105
+ await test$1.expect(input).toBeVisible({ timeout: UI_TIMEOUT });
5106
+ await input.fill(String(values.limitUsd));
5107
+ }
5108
+ if (values.interval) {
5109
+ await pickSelectOption(scope, page, `${testIdPrefix}-interval-select`, SPEND_LIMITS_LABELS.intervalOptions[values.interval]);
5110
+ }
5111
+ if (values.enforcement) {
5112
+ await pickSelectOption(scope, page, `${testIdPrefix}-enforcement-select`, SPEND_LIMITS_LABELS.enforcementOptions[values.enforcement]);
5113
+ }
5114
+ if (values.alertThresholds !== undefined) {
5115
+ const input = scope.getByTestId(`${testIdPrefix}-thresholds-input`);
5116
+ await test$1.expect(input).toBeVisible({ timeout: UI_TIMEOUT });
5117
+ await input.fill(values.alertThresholds);
5118
+ }
5119
+ }
5120
+ /** Click a form's Save and gate on the saved toast (fires after the PUT resolves). */
5121
+ async function saveSpendLimitForm(page, scope, testIdPrefix) {
5122
+ const saveButton = scope.getByTestId(`${testIdPrefix}-save-button`);
5123
+ await test$1.expect(saveButton).toBeEnabled({ timeout: UI_TIMEOUT });
5124
+ await saveButton.click();
5125
+ // .first() is deliberate: repeated saves can stack identical sonner
5126
+ // toasts, and any one of them proves the mutation resolved.
5127
+ await test$1.expect(page.getByText(SPEND_LIMITS_LABELS.toasts.saved).first()).toBeVisible({
5128
+ timeout: MUTATION_TIMEOUT,
5129
+ });
5130
+ }
5131
+ /** Confirm a captured delete dialog and gate on it closing + the deleted toast. */
5132
+ async function confirmDeleteDialog(page, dialogTestId) {
5133
+ const dialog = page.getByTestId(dialogTestId);
5134
+ await test$1.expect(dialog).toBeVisible({ timeout: UI_TIMEOUT });
5135
+ // All three delete dialogs share the '<x>-delete-modal' / '<x>-delete-confirm'
5136
+ // testid convention.
5137
+ const confirmButton = dialog.getByTestId(`${dialogTestId.replace('-modal', '')}-confirm`);
5138
+ await test$1.expect(confirmButton).toBeEnabled({ timeout: UI_TIMEOUT });
5139
+ await confirmButton.click();
5140
+ await test$1.expect(dialog).toBeHidden({ timeout: MUTATION_TIMEOUT });
5141
+ await test$1.expect(page.getByText(SPEND_LIMITS_LABELS.toasts.deleted).first()).toBeVisible({
5142
+ timeout: MUTATION_TIMEOUT,
5143
+ });
5144
+ }
5145
+ /**
5146
+ * Create or update the agent-scoped spend limit (switches to the "This
5147
+ * agent" sub-tab first). Completion is gated on the saved toast and the
5148
+ * usage strip rendering (it only exists once a cap is persisted).
5149
+ */
5150
+ async function setAgentSpendLimit(page, values, scope) {
5151
+ await switchToSpendLimitsSubTab(page, 'agent', scope);
5152
+ const body = spendLimitsTabBody(page, scope);
5153
+ await fillSpendLimitForm(page, body, 'agent-cap', values);
5154
+ await saveSpendLimitForm(page, body, 'agent-cap');
5155
+ await test$1.expect(body.getByTestId('agent-cap-usage')).toBeVisible({ timeout: MUTATION_TIMEOUT });
5156
+ logger.info('Saved agent spend limit');
5157
+ }
5158
+ /**
5159
+ * Delete the agent-scoped spend limit via the form's Delete button →
5160
+ * confirmation dialog. Completion is gated on the dialog closing, the
5161
+ * deleted toast, and the form returning to create mode (no-cap hint).
5162
+ */
5163
+ async function deleteAgentSpendLimit(page, scope) {
5164
+ await switchToSpendLimitsSubTab(page, 'agent', scope);
5165
+ const body = spendLimitsTabBody(page, scope);
5166
+ const deleteButton = body.getByTestId('agent-cap-delete-button');
5167
+ await test$1.expect(deleteButton).toBeEnabled({ timeout: UI_TIMEOUT });
5168
+ await deleteButton.click();
5169
+ await confirmDeleteDialog(page, 'spend-cap-delete-modal');
5170
+ await test$1.expect(body.getByTestId('agent-cap-no-cap-hint')).toBeVisible({
5171
+ timeout: MUTATION_TIMEOUT,
5172
+ });
5173
+ logger.info('Deleted agent spend limit');
5174
+ }
5175
+ /**
5176
+ * Fill the user-limit modal's form and save it. The modal Locator is
5177
+ * captured once and every sub-element is resolved from it.
5178
+ */
5179
+ async function submitUserCapModal(page, values) {
5180
+ const modal = page.getByTestId('user-cap-modal');
5181
+ await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT });
5182
+ await fillSpendLimitForm(page, modal, 'user-cap-modal', values);
5183
+ await saveSpendLimitForm(page, modal, 'user-cap-modal');
5184
+ // The modal only closes itself after the upsert resolves.
5185
+ await test$1.expect(modal).toBeHidden({ timeout: MUTATION_TIMEOUT });
5186
+ }
5187
+ /**
5188
+ * Add a per-user spend limit through the modal (switches to the "Per User"
5189
+ * sub-tab first): opens the modal, searches the user by email, clicks the
5190
+ * email in the results (the picker is email-only by design), fills the
5191
+ * form, saves. Completion is gated on the modal closing and the user's
5192
+ * table row rendering after the list refetch.
5193
+ */
5194
+ async function addUserSpendLimit(page, values, scope) {
5195
+ await switchToSpendLimitsSubTab(page, 'users', scope);
5196
+ const addButton = userCapsSection(page, scope).getByTestId('user-caps-add-button');
5197
+ await test$1.expect(addButton).toBeEnabled({ timeout: UI_TIMEOUT });
5198
+ await addButton.click();
5199
+ const modal = page.getByTestId('user-cap-modal');
5200
+ await test$1.expect(modal).toBeVisible({ timeout: UI_TIMEOUT });
5201
+ const pickerInput = modal.getByTestId('user-cap-modal-user-picker-input');
5202
+ await test$1.expect(pickerInput).toBeVisible({ timeout: UI_TIMEOUT });
5203
+ await pickerInput.fill(values.email);
5204
+ // Options render as buttons labelled with the email only.
5205
+ const option = modal
5206
+ .getByTestId('user-cap-modal-user-picker-results')
5207
+ .getByRole('button', { name: values.email, exact: true });
5208
+ await test$1.expect(option).toBeVisible({ timeout: MUTATION_TIMEOUT });
5209
+ await option.click();
5210
+ await test$1.expect(modal.getByTestId('user-cap-modal-user-picker-selected')).toBeVisible({
5211
+ timeout: UI_TIMEOUT,
5212
+ });
5213
+ await submitUserCapModal(page, values);
5214
+ await test$1.expect(userSpendLimitRow(page, values.username, scope)).toBeVisible({
5215
+ timeout: MUTATION_TIMEOUT,
5216
+ });
5217
+ logger.info(`Added user spend limit for "${values.email}"`);
5218
+ }
5219
+ /**
5220
+ * Open a per-user table row's three-dots menu and click one of its actions.
5221
+ * The trigger is located by testid, not aria-label — the accessible name is
5222
+ * email-first while rows stay keyed by username.
5223
+ */
5224
+ async function clickUserRowMenuAction(page, username, action, scope) {
5225
+ const trigger = userSpendLimitRow(page, username, scope).getByTestId(`user-cap-actions-${username}`);
5226
+ await test$1.expect(trigger).toBeVisible({ timeout: UI_TIMEOUT });
5227
+ await trigger.click();
5228
+ // The menu portals to the page root — the open menu is the only one.
5229
+ const item = page.getByRole('menuitem', { name: action, exact: true });
5230
+ await test$1.expect(item).toBeVisible({ timeout: UI_TIMEOUT });
5231
+ await item.click();
5232
+ }
5233
+ /**
5234
+ * Edit an existing per-user limit via its row's three-dots menu → Edit
5235
+ * modal. Completion is gated on the modal closing after the upsert.
5236
+ */
5237
+ async function editUserSpendLimit(page, username, values, scope) {
5238
+ await switchToSpendLimitsSubTab(page, 'users', scope);
5239
+ await clickUserRowMenuAction(page, username, SPEND_LIMITS_LABELS.menu.edit, scope);
5240
+ await submitUserCapModal(page, values);
5241
+ logger.info(`Edited user spend limit for "${username}"`);
5242
+ }
5243
+ /**
5244
+ * Idempotently flip a per-user limit's Status toggle (the switch in the
5245
+ * table's Status column — datasets-table pattern, saves immediately).
5246
+ * Completion is gated on the saved toast and the switch reflecting the new
5247
+ * state after the row refetch.
5248
+ */
5249
+ async function setUserSpendLimitEnabled(page, username, enabled, scope) {
5250
+ await switchToSpendLimitsSubTab(page, 'users', scope);
5251
+ const toggle = userSpendLimitRow(page, username, scope).getByTestId(`user-cap-status-switch-${username}`);
5252
+ await test$1.expect(toggle).toBeVisible({ timeout: UI_TIMEOUT });
5253
+ if ((await toggle.getAttribute('aria-checked')) === String(enabled)) {
5254
+ logger.info(`User spend limit for "${username}" already ${enabled ? 'enabled' : 'disabled'}`);
5255
+ return;
5256
+ }
5257
+ await test$1.expect(toggle).toBeEnabled({ timeout: UI_TIMEOUT });
5258
+ await toggle.click();
5259
+ await test$1.expect(page.getByText(SPEND_LIMITS_LABELS.toasts.saved).first()).toBeVisible({
5260
+ timeout: MUTATION_TIMEOUT,
5261
+ });
5262
+ await test$1.expect(toggle).toHaveAttribute('aria-checked', String(enabled), {
5263
+ timeout: MUTATION_TIMEOUT,
5264
+ });
5265
+ logger.info(`User spend limit for "${username}" ${enabled ? 'enabled' : 'disabled'}`);
5266
+ }
5267
+ /**
5268
+ * Delete a per-user limit via its row's three-dots menu → confirmation
5269
+ * dialog. Completion is gated on the dialog closing, the deleted toast and
5270
+ * the row disappearing after the list refetch.
5271
+ */
5272
+ async function deleteUserSpendLimit(page, username, scope) {
5273
+ await switchToSpendLimitsSubTab(page, 'users', scope);
5274
+ await clickUserRowMenuAction(page, username, SPEND_LIMITS_LABELS.menu.delete, scope);
5275
+ await confirmDeleteDialog(page, 'user-cap-delete-modal');
5276
+ await test$1.expect(userSpendLimitRow(page, username, scope)).toBeHidden({ timeout: MUTATION_TIMEOUT });
5277
+ logger.info(`Deleted user spend limit for "${username}"`);
5278
+ }
5279
+ // ---------------------------------------------------------------------------
5280
+ // Tenant settings — Billing tab ("Plan & Credits" / "Spend Limits")
5281
+ // ---------------------------------------------------------------------------
5282
+ /** The tenant Billing tab's tablist (scoped by its aria-label). */
5283
+ function tenantBillingTablist(page) {
5284
+ return page.getByRole('tablist', { name: SPEND_LIMITS_LABELS.tenantTablistAria });
5285
+ }
5286
+ /** The workspace spend limit section card in the tenant Billing tab. */
5287
+ function workspaceSpendLimitSection(page) {
5288
+ return page.getByTestId('spend-limits-workspace-section');
5289
+ }
5290
+ /**
5291
+ * Switch the tenant Billing tab to "Spend Limits". Assumes the tenant
5292
+ * settings Billing tab is already open (see `waitForBillingTabReady`).
5293
+ * Completion is gated on the workspace section rendering.
5294
+ */
5295
+ async function switchToWorkspaceSpendLimits(page) {
5296
+ const tab = tenantBillingTablist(page).getByRole('tab', {
5297
+ name: SPEND_LIMITS_LABELS.tenantTabs.spendLimits,
5298
+ exact: true,
5299
+ });
5300
+ await test$1.expect(tab).toBeVisible({ timeout: UI_TIMEOUT });
5301
+ await tab.click();
5302
+ await test$1.expect(workspaceSpendLimitSection(page)).toBeVisible({ timeout: UI_TIMEOUT });
5303
+ logger.info('Switched to tenant Spend Limits tab');
5304
+ }
5305
+ /**
5306
+ * Switch the tenant Billing tab back to "Plan & Credits". Completion is
5307
+ * gated on the Plan section rendering.
5308
+ */
5309
+ async function switchToPlanAndCredits(page) {
5310
+ const tab = tenantBillingTablist(page).getByRole('tab', {
5311
+ name: SPEND_LIMITS_LABELS.tenantTabs.planAndCredits,
5312
+ exact: true,
5313
+ });
5314
+ await test$1.expect(tab).toBeVisible({ timeout: UI_TIMEOUT });
5315
+ await tab.click();
5316
+ await test$1.expect(page.getByTestId('billing-plan-section')).toBeVisible({ timeout: UI_TIMEOUT });
5317
+ logger.info('Switched to tenant Plan & Credits tab');
5318
+ }
5319
+ /**
5320
+ * Create or update the workspace-wide spend limit (switches to the Spend
5321
+ * Limits tab first). Completion is gated on the saved toast and the
5322
+ * usage strip rendering (it only exists once a cap is persisted).
5323
+ */
5324
+ async function setWorkspaceSpendLimit(page, values) {
5325
+ await switchToWorkspaceSpendLimits(page);
5326
+ const section = workspaceSpendLimitSection(page);
5327
+ await fillSpendLimitForm(page, section, 'tenant-cap', values);
5328
+ await saveSpendLimitForm(page, section, 'tenant-cap');
5329
+ await test$1.expect(section.getByTestId('tenant-cap-usage')).toBeVisible({
5330
+ timeout: MUTATION_TIMEOUT,
5331
+ });
5332
+ logger.info('Saved workspace spend limit');
5333
+ }
5334
+ /**
5335
+ * Delete the workspace-wide spend limit. Completion is gated on the dialog
5336
+ * closing, the deleted toast, and the usage strip disappearing.
5337
+ */
5338
+ async function deleteWorkspaceSpendLimit(page) {
5339
+ await switchToWorkspaceSpendLimits(page);
5340
+ const section = workspaceSpendLimitSection(page);
5341
+ const deleteButton = section.getByTestId('tenant-cap-delete-button');
5342
+ await test$1.expect(deleteButton).toBeEnabled({ timeout: UI_TIMEOUT });
5343
+ await deleteButton.click();
5344
+ await confirmDeleteDialog(page, 'tenant-cap-delete-modal');
5345
+ await test$1.expect(section.getByTestId('tenant-cap-usage')).toBeHidden({
5346
+ timeout: MUTATION_TIMEOUT,
5347
+ });
5348
+ logger.info('Deleted workspace spend limit');
5349
+ }
5350
+ /**
5351
+ * Assert the workspace usage strip (the same one the agent editor shows) is
5352
+ * visible, optionally matching formatted dollar values like '$30.50' inside
5353
+ * its "Spent: …" / "Remaining: …" texts.
5354
+ */
5355
+ async function expectWorkspaceSpendStats(page, values) {
5356
+ const section = workspaceSpendLimitSection(page);
5357
+ const usage = section.getByTestId('tenant-cap-usage');
5358
+ await test$1.expect(usage).toBeVisible({ timeout: UI_TIMEOUT });
5359
+ if (values === null || values === void 0 ? void 0 : values.spent) {
5360
+ await test$1.expect(section.getByTestId('tenant-cap-spent')).toContainText(values.spent);
5361
+ }
5362
+ if (values === null || values === void 0 ? void 0 : values.remaining) {
5363
+ await test$1.expect(section.getByTestId('tenant-cap-remaining')).toContainText(values.remaining);
5364
+ }
5365
+ }
5366
+ /**
5367
+ * Assert the "actual spend" stats (financial analytics) shown while no
5368
+ * workspace cap is configured yet.
5369
+ */
5370
+ async function expectWorkspaceActualSpendStats(page) {
5371
+ await test$1.expect(workspaceSpendLimitSection(page).getByTestId('workspace-actual-spend-stats')).toBeVisible({ timeout: UI_TIMEOUT });
5372
+ }
5373
+ // ---------------------------------------------------------------------------
5374
+ // Tenant settings — Billing tab, "Agent Limits" tab (configured caps table)
5375
+ // ---------------------------------------------------------------------------
5376
+ /** The Agent Limits section card in the tenant Billing tab. */
5377
+ function agentLimitsSection(page) {
5378
+ return page.getByTestId('spend-limits-agents-section');
5379
+ }
5380
+ /** An Agent Limits table row for the given agent unique_id. */
5381
+ function agentLimitsRow(page, mentorUniqueId) {
5382
+ return agentLimitsSection(page).getByTestId(`agent-limits-row-${mentorUniqueId}`);
5383
+ }
5384
+ /**
5385
+ * Switch the tenant Billing tab to "Agent Limits". Assumes the tenant
5386
+ * Billing tab is already open. Completion is gated on the section rendering.
5387
+ */
5388
+ async function switchToAgentLimits(page) {
5389
+ const tab = tenantBillingTablist(page).getByRole('tab', {
5390
+ name: SPEND_LIMITS_LABELS.tenantTabs.agentLimits,
5391
+ exact: true,
5392
+ });
5393
+ await test$1.expect(tab).toBeVisible({ timeout: UI_TIMEOUT });
5394
+ await tab.click();
5395
+ await test$1.expect(agentLimitsSection(page)).toBeVisible({ timeout: UI_TIMEOUT });
5396
+ logger.info('Switched to tenant Agent Limits tab');
5397
+ }
5398
+ /**
5399
+ * Filter the Agent Limits table to one agent via the autocomplete: types the
5400
+ * name, clicks the matching option, and gates on the selected chip.
5401
+ */
5402
+ async function filterAgentLimits(page, agentName) {
5403
+ const section = agentLimitsSection(page);
5404
+ const input = section.getByTestId('agent-limits-filter-input');
5405
+ await test$1.expect(input).toBeVisible({ timeout: UI_TIMEOUT });
5406
+ await input.fill(agentName);
5407
+ const option = section
5408
+ .getByTestId('agent-limits-filter-results')
5409
+ .getByRole('button', { name: agentName, exact: true });
5410
+ await test$1.expect(option).toBeVisible({ timeout: MUTATION_TIMEOUT });
5411
+ await option.click();
5412
+ await test$1.expect(section.getByTestId('agent-limits-filter-selected')).toBeVisible({
5413
+ timeout: UI_TIMEOUT,
5414
+ });
5415
+ logger.info(`Filtered agent limits to "${agentName}"`);
5416
+ }
5417
+ /** Clear the Agent Limits autocomplete filter (back to the full caps list). */
5418
+ async function clearAgentLimitsFilter(page) {
5419
+ const section = agentLimitsSection(page);
5420
+ const clearButton = section.getByTestId('agent-limits-filter-clear');
5421
+ await test$1.expect(clearButton).toBeVisible({ timeout: UI_TIMEOUT });
5422
+ await clearButton.click();
5423
+ await test$1.expect(section.getByTestId('agent-limits-filter-input')).toBeVisible({
5424
+ timeout: UI_TIMEOUT,
5425
+ });
5426
+ }
5427
+ /** Gate on the Agent Limits manage popup being open with the Billing editor loaded. */
5428
+ async function expectAgentLimitsPopupOpen(page) {
5429
+ const popup = page.getByTestId('agent-limits-modal');
5430
+ await test$1.expect(popup).toBeVisible({ timeout: UI_TIMEOUT });
5431
+ await test$1.expect(popup.getByTestId('spend-caps-tab-body')).toBeVisible({ timeout: UI_TIMEOUT });
5432
+ return popup;
5433
+ }
5434
+ /**
5435
+ * Open the manage popup for a capped agent's row and return the popup's
5436
+ * Locator. The popup hosts the same `AgentSpendCapsTab` editor as the
5437
+ * agent's own settings and stacks ON TOP of the tenant settings dialog, so
5438
+ * pass the returned Locator as the `scope` argument of the agent-scope
5439
+ * helpers (`setAgentSpendLimit`, `addUserSpendLimit`, …) — that pins every
5440
+ * query to the popup instead of the dialog underneath.
5441
+ */
5442
+ async function openAgentLimitsManage(page, mentorUniqueId) {
5443
+ const manageButton = agentLimitsRow(page, mentorUniqueId).getByTestId(`agent-limits-manage-${mentorUniqueId}`);
5444
+ await test$1.expect(manageButton).toBeVisible({ timeout: UI_TIMEOUT });
5445
+ await manageButton.click();
5446
+ const popup = await expectAgentLimitsPopupOpen(page);
5447
+ logger.info(`Opened agent limits popup for "${mentorUniqueId}"`);
5448
+ return popup;
5449
+ }
5450
+ /**
5451
+ * With the filter set to an agent that has no cap yet, click the empty
5452
+ * state's "Set Spend Limit" button and return the opened popup's Locator
5453
+ * (pass it as `scope` to the agent-scope helpers, see
5454
+ * `openAgentLimitsManage`).
5455
+ */
5456
+ async function clickSetSpendLimitForFilteredAgent(page) {
5457
+ const button = agentLimitsSection(page).getByTestId('agent-limits-set-limit-button');
5458
+ await test$1.expect(button).toBeVisible({ timeout: UI_TIMEOUT });
5459
+ await button.click();
5460
+ const popup = await expectAgentLimitsPopupOpen(page);
5461
+ logger.info('Opened agent limits popup from the filtered empty state');
5462
+ return popup;
5463
+ }
5464
+ /** Close the Agent Limits manage popup (Escape) and gate on it disappearing. */
5465
+ async function closeAgentLimitsPopup(page) {
5466
+ const popup = page.getByTestId('agent-limits-modal');
5467
+ await test$1.expect(popup).toBeVisible({ timeout: UI_TIMEOUT });
5468
+ await page.keyboard.press('Escape');
5469
+ await test$1.expect(popup).toBeHidden({ timeout: UI_TIMEOUT });
5470
+ }
5471
+ /**
5472
+ * Idempotently flip an agent cap's Status toggle in the Agent Limits table
5473
+ * (saves immediately). Gates on the saved toast and the switch reflecting
5474
+ * the new state after the list refetch.
5475
+ */
5476
+ async function setAgentLimitsRowEnabled(page, mentorUniqueId, enabled) {
5477
+ const toggle = agentLimitsRow(page, mentorUniqueId).getByTestId(`agent-limits-status-switch-${mentorUniqueId}`);
5478
+ await test$1.expect(toggle).toBeVisible({ timeout: UI_TIMEOUT });
5479
+ if ((await toggle.getAttribute('aria-checked')) === String(enabled)) {
5480
+ logger.info(`Agent cap "${mentorUniqueId}" already ${enabled ? 'enabled' : 'disabled'}`);
5481
+ return;
5482
+ }
5483
+ await test$1.expect(toggle).toBeEnabled({ timeout: UI_TIMEOUT });
5484
+ await toggle.click();
5485
+ await test$1.expect(page.getByText(SPEND_LIMITS_LABELS.toasts.saved).first()).toBeVisible({
5486
+ timeout: MUTATION_TIMEOUT,
5487
+ });
5488
+ await test$1.expect(toggle).toHaveAttribute('aria-checked', String(enabled), {
5489
+ timeout: MUTATION_TIMEOUT,
5490
+ });
5491
+ logger.info(`Agent cap "${mentorUniqueId}" ${enabled ? 'enabled' : 'disabled'}`);
5492
+ }
5493
+ /**
5494
+ * Assert an Agent Limits row is visible, optionally checking its formatted
5495
+ * cell contents, e.g. `{ name: 'Math Tutor', limit: '$250.00', spent: '$30.50' }`.
5496
+ * Text matching is substring-based so a limit can be checked with or without
5497
+ * its "/ Month" interval suffix.
5498
+ */
5499
+ async function expectAgentLimitsRowContent(page, mentorUniqueId, texts) {
5500
+ const row = agentLimitsRow(page, mentorUniqueId);
5501
+ await test$1.expect(row).toBeVisible({ timeout: UI_TIMEOUT });
5502
+ for (const text of Object.values(texts !== null && texts !== void 0 ? texts : {})) {
5503
+ if (text)
5504
+ await test$1.expect(row).toContainText(text);
5505
+ }
5506
+ }
5507
+ // ---------------------------------------------------------------------------
5508
+ // Tenant settings — end-to-end composites through the manage popup
5509
+ // ---------------------------------------------------------------------------
5510
+ /**
5511
+ * End to end from the tenant Agent Limits tab, for an agent with NO cap yet:
5512
+ * filter to the agent by name, open the popup from the empty state's "Set
5513
+ * Spend Limit" button, save the agent-scoped limit inside the popup, close
5514
+ * it, and gate on the agent's row rendering after the list refetch.
5515
+ */
5516
+ async function createAgentSpendLimitViaFilter(page, agent, values) {
5517
+ await filterAgentLimits(page, agent.name);
5518
+ const popup = await clickSetSpendLimitForFilteredAgent(page);
5519
+ await setAgentSpendLimit(page, values, popup);
5520
+ await closeAgentLimitsPopup(page);
5521
+ await test$1.expect(agentLimitsRow(page, agent.mentorUniqueId)).toBeVisible({
5522
+ timeout: MUTATION_TIMEOUT,
5523
+ });
5524
+ logger.info(`Created agent spend limit for "${agent.name}" via the Agent Limits filter`);
5525
+ }
5526
+ /**
5527
+ * End to end from the tenant Agent Limits tab: open a capped agent's manage
5528
+ * popup, save the agent-scoped limit inside it, close the popup, and gate on
5529
+ * the row still rendering after the list refetch.
5530
+ */
5531
+ async function setAgentSpendLimitFromTenantBilling(page, mentorUniqueId, values) {
5532
+ const popup = await openAgentLimitsManage(page, mentorUniqueId);
5533
+ await setAgentSpendLimit(page, values, popup);
5534
+ await closeAgentLimitsPopup(page);
5535
+ await test$1.expect(agentLimitsRow(page, mentorUniqueId)).toBeVisible({ timeout: MUTATION_TIMEOUT });
5536
+ logger.info(`Saved agent spend limit for "${mentorUniqueId}" from tenant Billing`);
5537
+ }
5538
+ /**
5539
+ * End to end from the tenant Agent Limits tab: open the agent's manage popup
5540
+ * and delete its agent-scoped limit through the confirmation dialog. Closing
5541
+ * the popup afterwards gates on the agent's row disappearing from the table
5542
+ * (the tenant-wide list only holds configured caps).
5543
+ */
5544
+ async function deleteAgentSpendLimitFromTenantBilling(page, mentorUniqueId) {
5545
+ const popup = await openAgentLimitsManage(page, mentorUniqueId);
5546
+ await deleteAgentSpendLimit(page, popup);
5547
+ await closeAgentLimitsPopup(page);
5548
+ await test$1.expect(agentLimitsRow(page, mentorUniqueId)).toBeHidden({ timeout: MUTATION_TIMEOUT });
5549
+ logger.info(`Deleted agent spend limit for "${mentorUniqueId}" from tenant Billing`);
5550
+ }
5551
+ /**
5552
+ * End to end per-user limit from the tenant Agent Limits tab: open the
5553
+ * agent's manage popup, add the user limit through its "Per User" sub-tab
5554
+ * (nested user-cap modal inside the popup), then close the popup. Completion
5555
+ * is gated inside `addUserSpendLimit` on the modal closing and the user's
5556
+ * row rendering in the popup's table.
5557
+ */
5558
+ async function addUserSpendLimitFromTenantBilling(page, mentorUniqueId, values) {
5559
+ const popup = await openAgentLimitsManage(page, mentorUniqueId);
5560
+ await addUserSpendLimit(page, values, popup);
5561
+ await closeAgentLimitsPopup(page);
5562
+ logger.info(`Added user spend limit for "${values.email}" on "${mentorUniqueId}" from tenant Billing`);
5563
+ }
5564
+
4895
5565
  /**
4896
5566
  * Tasks tab helpers — Playwright bindings for the `AgentTasksTab`
4897
5567
  * component from `@iblai/web-containers`.
@@ -7193,6 +7863,7 @@ exports.LTI_TEST_IDS = LTI_TEST_IDS;
7193
7863
  exports.MailsacClient = MailsacClient;
7194
7864
  exports.PRIVACY_LABELS = PRIVACY_LABELS;
7195
7865
  exports.SCREENSHARE_LABELS = SCREENSHARE_LABELS;
7866
+ exports.SPEND_LIMITS_LABELS = SPEND_LIMITS_LABELS;
7196
7867
  exports.SUPPORT_LABELS = SUPPORT_LABELS;
7197
7868
  exports.TASKS_LABELS = TASKS_LABELS;
7198
7869
  exports.VOICE_LABELS = VOICE_LABELS;
@@ -7202,6 +7873,10 @@ exports.addMemory = addMemory;
7202
7873
  exports.addQaPairsManually = addQaPairsManually;
7203
7874
  exports.addSkillToAgent = addSkillToAgent;
7204
7875
  exports.addTextResource = addTextResource;
7876
+ exports.addUserSpendLimit = addUserSpendLimit;
7877
+ exports.addUserSpendLimitFromTenantBilling = addUserSpendLimitFromTenantBilling;
7878
+ exports.agentLimitsRow = agentLimitsRow;
7879
+ exports.agentLimitsSection = agentLimitsSection;
7205
7880
  exports.archiveFirstMemory = archiveFirstMemory;
7206
7881
  exports.archiveMemoryByContent = archiveMemoryByContent;
7207
7882
  exports.billingAutoRechargeSection = billingAutoRechargeSection;
@@ -7220,6 +7895,7 @@ exports.cancelNewInstanceDialog = cancelNewInstanceDialog;
7220
7895
  exports.cancelStartEvaluation = cancelStartEvaluation;
7221
7896
  exports.checkAdminStatus = checkAdminStatus;
7222
7897
  exports.checkRunStatus = checkRunStatus;
7898
+ exports.clearAgentLimitsFilter = clearAgentLimitsFilter;
7223
7899
  exports.clearDateRangeFilter = clearDateRangeFilter;
7224
7900
  exports.clearGradeResultOverride = clearGradeResultOverride;
7225
7901
  exports.clearInstanceSearch = clearInstanceSearch;
@@ -7232,6 +7908,8 @@ exports.clickBillingUpgrade = clickBillingUpgrade;
7232
7908
  exports.clickChatPrivacyToggle = clickChatPrivacyToggle;
7233
7909
  exports.clickDownloadAgain = clickDownloadAgain;
7234
7910
  exports.clickManualDownloadLink = clickManualDownloadLink;
7911
+ exports.clickSetSpendLimitForFilteredAgent = clickSetSpendLimitForFilteredAgent;
7912
+ exports.closeAgentLimitsPopup = closeAgentLimitsPopup;
7235
7913
  exports.closeBenchmarkItemsDialog = closeBenchmarkItemsDialog;
7236
7914
  exports.closeCreditBalanceDropdown = closeCreditBalanceDropdown;
7237
7915
  exports.closeEvaluationDetailDialog = closeEvaluationDetailDialog;
@@ -7242,6 +7920,7 @@ exports.confirmEnableChatPrivacyMidSession = confirmEnableChatPrivacyMidSession;
7242
7920
  exports.confirmKeyDelete = confirmKeyDelete;
7243
7921
  exports.connectToInstance = connectToInstance;
7244
7922
  exports.copyEndpoint = copyEndpoint;
7923
+ exports.createAgentSpendLimitViaFilter = createAgentSpendLimitViaFilter;
7245
7924
  exports.createAuthSetup = createAuthSetup;
7246
7925
  exports.createBenchmark = createBenchmark;
7247
7926
  exports.createEnvConfig = createEnvConfig;
@@ -7255,6 +7934,8 @@ exports.createTool = createTool;
7255
7934
  exports.creditBalancePanel = creditBalancePanel;
7256
7935
  exports.creditBalancePlanBadge = creditBalancePlanBadge;
7257
7936
  exports.creditBalanceTrigger = creditBalanceTrigger;
7937
+ exports.deleteAgentSpendLimit = deleteAgentSpendLimit;
7938
+ exports.deleteAgentSpendLimitFromTenantBilling = deleteAgentSpendLimitFromTenantBilling;
7258
7939
  exports.deleteEvaluation = deleteEvaluation;
7259
7940
  exports.deleteFirstMemory = deleteFirstMemory;
7260
7941
  exports.deleteGraderCriterion = deleteGraderCriterion;
@@ -7265,6 +7946,8 @@ exports.deleteQaItem = deleteQaItem;
7265
7946
  exports.deleteResource = deleteResource;
7266
7947
  exports.deleteSkill = deleteSkill;
7267
7948
  exports.deleteTask = deleteTask;
7949
+ exports.deleteUserSpendLimit = deleteUserSpendLimit;
7950
+ exports.deleteWorkspaceSpendLimit = deleteWorkspaceSpendLimit;
7268
7951
  exports.disableSkill = disableSkill;
7269
7952
  exports.disableSupport = disableSupport;
7270
7953
  exports.disconnectInstance = disconnectInstance;
@@ -7275,10 +7958,12 @@ exports.editLink = editLink;
7275
7958
  exports.editSkill = editSkill;
7276
7959
  exports.editTextResource = editTextResource;
7277
7960
  exports.editTool = editTool;
7961
+ exports.editUserSpendLimit = editUserSpendLimit;
7278
7962
  exports.enableSkill = enableSkill;
7279
7963
  exports.enableSupport = enableSupport;
7280
7964
  exports.expandReview = expandReview;
7281
7965
  exports.expandTrace = expandTrace;
7966
+ exports.expectAgentLimitsRowContent = expectAgentLimitsRowContent;
7282
7967
  exports.expectAllEndpointsVisible = expectAllEndpointsVisible;
7283
7968
  exports.expectBenchmarkListed = expectBenchmarkListed;
7284
7969
  exports.expectBillingAutoRechargeSection = expectBillingAutoRechargeSection;
@@ -7362,9 +8047,12 @@ exports.expectTraceScore = expectTraceScore;
7362
8047
  exports.expectTtsSelectDisabled = expectTtsSelectDisabled;
7363
8048
  exports.expectVoiceProviderSelected = expectVoiceProviderSelected;
7364
8049
  exports.expectVoiceVisible = expectVoiceVisible;
8050
+ exports.expectWorkspaceActualSpendStats = expectWorkspaceActualSpendStats;
8051
+ exports.expectWorkspaceSpendStats = expectWorkspaceSpendStats;
7365
8052
  exports.exportRunCsv = exportRunCsv;
7366
8053
  exports.fillLinkName = fillLinkName;
7367
8054
  exports.fillToolForm = fillToolForm;
8055
+ exports.filterAgentLimits = filterAgentLimits;
7368
8056
  exports.filterByAction = filterByAction;
7369
8057
  exports.filterByActionAndVerify = filterByActionAndVerify;
7370
8058
  exports.filterByActor = filterByActor;
@@ -7487,6 +8175,7 @@ exports.isPrivateModeTabVisible = isPrivateModeTabVisible;
7487
8175
  exports.isSandboxTabVisible = isSandboxTabVisible;
7488
8176
  exports.isScreenShareTabVisible = isScreenShareTabVisible;
7489
8177
  exports.isSkillEnabled = isSkillEnabled;
8178
+ exports.isSpendLimitsTabVisible = isSpendLimitsTabVisible;
7490
8179
  exports.isSupportEnabled = isSupportEnabled;
7491
8180
  exports.isSupportTabVisible = isSupportTabVisible;
7492
8181
  exports.isTasksTabVisible = isTasksTabVisible;
@@ -7501,6 +8190,7 @@ exports.navigateToDataReports = navigateToDataReports;
7501
8190
  exports.navigateToReportDownload = navigateToReportDownload;
7502
8191
  exports.openAddItemsDialog = openAddItemsDialog;
7503
8192
  exports.openAddMemoryDialog = openAddMemoryDialog;
8193
+ exports.openAgentLimitsManage = openAgentLimitsManage;
7504
8194
  exports.openAgentPromptEditModal = openAgentPromptEditModal;
7505
8195
  exports.openBenchmarkItems = openBenchmarkItems;
7506
8196
  exports.openCallConfigVoicePicker = openCallConfigVoicePicker;
@@ -7581,8 +8271,12 @@ exports.selectTtsProvider = selectTtsProvider;
7581
8271
  exports.selectVoice = selectVoice;
7582
8272
  exports.selectVoiceProvider = selectVoiceProvider;
7583
8273
  exports.sendAgentChatMessage = sendAgentChatMessage;
8274
+ exports.setAgentLimitsRowEnabled = setAgentLimitsRowEnabled;
8275
+ exports.setAgentSpendLimit = setAgentSpendLimit;
8276
+ exports.setAgentSpendLimitFromTenantBilling = setAgentSpendLimitFromTenantBilling;
7584
8277
  exports.setBlockMessage = setBlockMessage;
7585
8278
  exports.setCallLanguage = setCallLanguage;
8279
+ exports.setCatalogSkillEnabled = setCatalogSkillEnabled;
7586
8280
  exports.setEnableVideo = setEnableVideo;
7587
8281
  exports.setEntitySelected = setEntitySelected;
7588
8282
  exports.setGradingEnabled = setGradingEnabled;
@@ -7592,6 +8286,8 @@ exports.setSupportEnabled = setSupportEnabled;
7592
8286
  exports.setTenantChatPrivacyEnabled = setTenantChatPrivacyEnabled;
7593
8287
  exports.setTicketStatus = setTicketStatus;
7594
8288
  exports.setUseFunctionCallingEnabled = setUseFunctionCallingEnabled;
8289
+ exports.setUserSpendLimitEnabled = setUserSpendLimitEnabled;
8290
+ exports.setWorkspaceSpendLimit = setWorkspaceSpendLimit;
7595
8291
  exports.setupSandboxInstance = setupSandboxInstance;
7596
8292
  exports.shouldAddNewRowWhenClickingAddRowButton = shouldAddNewRowWhenClickingAddRowButton;
7597
8293
  exports.shouldAllowEditingCellValuesInCSVEditor = shouldAllowEditingCellValuesInCSVEditor;
@@ -7610,11 +8306,13 @@ exports.shouldSaveEditedCSVAndTriggerDownload = shouldSaveEditedCSVAndTriggerDow
7610
8306
  exports.shouldShowCombiningReportsDialog = shouldShowCombiningReportsDialog;
7611
8307
  exports.shouldVerifyCSVEditorDialogAccessibility = shouldVerifyCSVEditorDialogAccessibility;
7612
8308
  exports.signUpWithEmailAndPassword = signUpWithEmailAndPassword;
8309
+ exports.spendLimitsTabBody = spendLimitsTabBody;
7613
8310
  exports.startEvaluation = startEvaluation;
7614
8311
  exports.submitLinkModal = submitLinkModal;
7615
8312
  exports.submitLlmJudge = submitLlmJudge;
7616
8313
  exports.submitToolModal = submitToolModal;
7617
8314
  exports.switchToAddItemsSubTab = switchToAddItemsSubTab;
8315
+ exports.switchToAgentLimits = switchToAgentLimits;
7618
8316
  exports.switchToAgentSkillsSubTab = switchToAgentSkillsSubTab;
7619
8317
  exports.switchToAvailableSkillsSubTab = switchToAvailableSkillsSubTab;
7620
8318
  exports.switchToEvaluationTab = switchToEvaluationTab;
@@ -7623,16 +8321,20 @@ exports.switchToGraderTab = switchToGraderTab;
7623
8321
  exports.switchToLtiSubTab = switchToLtiSubTab;
7624
8322
  exports.switchToLtiTab = switchToLtiTab;
7625
8323
  exports.switchToMemoryTab = switchToMemoryTab;
8324
+ exports.switchToPlanAndCredits = switchToPlanAndCredits;
7626
8325
  exports.switchToPrivacyTab = switchToPrivacyTab;
7627
8326
  exports.switchToPrivateModeTab = switchToPrivateModeTab;
7628
8327
  exports.switchToSandboxTab = switchToSandboxTab;
7629
8328
  exports.switchToScreenShareTab = switchToScreenShareTab;
7630
8329
  exports.switchToSkillResourcesSubTab = switchToSkillResourcesSubTab;
7631
8330
  exports.switchToSkillsTab = switchToSkillsTab;
8331
+ exports.switchToSpendLimitsSubTab = switchToSpendLimitsSubTab;
8332
+ exports.switchToSpendLimitsTab = switchToSpendLimitsTab;
7632
8333
  exports.switchToSupportTab = switchToSupportTab;
7633
8334
  exports.switchToTasksTab = switchToTasksTab;
7634
8335
  exports.switchToVoiceSubTab = switchToVoiceSubTab;
7635
8336
  exports.switchToVoiceTab = switchToVoiceTab;
8337
+ exports.switchToWorkspaceSpendLimits = switchToWorkspaceSpendLimits;
7636
8338
  exports.teardownSandboxInstance = teardownSandboxInstance;
7637
8339
  exports.test = test;
7638
8340
  exports.toggleAutoPush = toggleAutoPush;
@@ -7641,6 +8343,7 @@ exports.toggleSkill = toggleSkill;
7641
8343
  exports.toolFields = toolFields;
7642
8344
  exports.uploadAssetResource = uploadAssetResource;
7643
8345
  exports.uploadQaCsv = uploadQaCsv;
8346
+ exports.userSpendLimitRow = userSpendLimitRow;
7644
8347
  exports.verifyAgentConfigPromptsVisible = verifyAgentConfigPromptsVisible;
7645
8348
  exports.verifyAgentSkillsEmptyState = verifyAgentSkillsEmptyState;
7646
8349
  exports.verifyAuditLogEmptyState = verifyAuditLogEmptyState;
@@ -7676,4 +8379,5 @@ exports.waitForPageLoad = waitForPageLoad;
7676
8379
  exports.waitForPageReady = waitForPageReady;
7677
8380
  exports.waitForReportDownload = waitForReportDownload;
7678
8381
  exports.waitForTicketInList = waitForTicketInList;
8382
+ exports.workspaceSpendLimitSection = workspaceSpendLimitSection;
7679
8383
  //# sourceMappingURL=index.cjs.map