@iblai/iblai-js 2.3.11 → 2.3.13

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.
@@ -1741,15 +1741,6 @@ async function switchToSandboxTab(page) {
1741
1741
  await test$1.expect(page.getByRole('heading', { name: 'Sandbox' }).or(page.getByText('Sandbox'))).toBeVisible({ timeout: 10000 });
1742
1742
  logger.info('Switched to Sandbox tab');
1743
1743
  }
1744
- /**
1745
- * Switch to the Skills tab inside the edit-mentor modal.
1746
- */
1747
- async function switchToSkillsTab(page) {
1748
- const tab = page.getByRole('tab', { name: 'Skills', exact: true });
1749
- await test$1.expect(tab).toBeVisible({ timeout: 10000 });
1750
- await tab.click();
1751
- logger.info('Switched to Skills tab');
1752
- }
1753
1744
  // ============================
1754
1745
  // Instance Table Helpers (not-connected state)
1755
1746
  // ============================
@@ -2249,96 +2240,251 @@ async function editAgentPrompt(page, field, content) {
2249
2240
  logger.info(`Edited agent prompt "${field}" with ${content.length} chars`);
2250
2241
  }
2251
2242
  // ============================
2252
- // Skills Tab Helpers
2243
+ // Combined Workflow Helpers
2244
+ // ============================
2245
+ /**
2246
+ * Full workflow: open Sandbox tab, create a new instance, connect the mentor to it.
2247
+ * Returns when the mentor is successfully connected.
2248
+ */
2249
+ async function setupSandboxInstance(page, instance) {
2250
+ await switchToSandboxTab(page);
2251
+ await createInstance(page, instance);
2252
+ await connectToInstance(page, instance.name);
2253
+ await verifyConnectedInstanceCard(page);
2254
+ logger.info(`Sandbox setup complete for "${instance.name}"`);
2255
+ }
2256
+ /**
2257
+ * Full workflow: open Sandbox tab, disconnect the current instance (if any),
2258
+ * then delete it from the table.
2259
+ */
2260
+ async function teardownSandboxInstance(page, instanceName) {
2261
+ await switchToSandboxTab(page);
2262
+ // If a connected instance is present, disconnect first
2263
+ const disconnectButton = page.getByRole('button', { name: /^Disconnect$/i });
2264
+ if (await disconnectButton.isVisible().catch(() => false)) {
2265
+ await disconnectInstance(page);
2266
+ }
2267
+ await deleteInstance(page, instanceName);
2268
+ logger.info(`Sandbox teardown complete for "${instanceName}"`);
2269
+ }
2270
+
2271
+ // Agent Skills helpers — the Skills section of the edit-agent dialog
2272
+ // ("Agent Skills" / "Available Skills" sub-tabs, skill CRUD with the
2273
+ // General/Resources dialog tabs, skill resources, and the chat `/` skill
2274
+ // picker). Kept separate from the Claw sandbox helpers, mirroring the
2275
+ // agent-skills / claw split in the data layer.
2276
+ //
2277
+ // Flake-resistance rules used throughout:
2278
+ // - Every dialog is captured ONCE via `page.getByRole('dialog', { name })`
2279
+ // (the accessible name comes from the dialog title only), and all fields
2280
+ // and buttons are resolved from that scoped locator. Dialogs stack here —
2281
+ // New/Edit Skill sits over the edit-agent dialog, resource dialogs sit
2282
+ // over Edit Skill — so unscoped text filters would match several dialogs.
2283
+ // - Rows are located via their test ids (`agent-skill-row`,
2284
+ // `available-skill-row`, `skill-resource-row`) filtered by visible name,
2285
+ // never via CSS classes.
2286
+ // - All waits are element-renders or toasts — never networkidle/timeouts.
2287
+ // - Page-level queries are used only for true portals: toasts, the dropdown
2288
+ // menu contents (they portal to document.body), and the Radix Select
2289
+ // listbox inside resource dialogs.
2290
+ // ============================
2291
+ // Navigation
2253
2292
  // ============================
2254
2293
  /**
2255
- * Verify the Skills tab content is loaded (either showing skill rows or the empty state).
2294
+ * Switch to the Skills tab inside the edit-agent dialog and wait for the
2295
+ * skills section to render.
2296
+ */
2297
+ async function switchToSkillsTab(page) {
2298
+ const tab = page.getByRole('tab', { name: 'Skills', exact: true });
2299
+ await test$1.expect(tab).toBeVisible({ timeout: 10000 });
2300
+ await tab.click();
2301
+ await test$1.expect(page.getByTestId('agent-skills-content')).toBeVisible({ timeout: 10000 });
2302
+ logger.info('Switched to Skills tab');
2303
+ }
2304
+ /** The Skills section container — scope for rows, sub-tabs, and pagination. */
2305
+ function skillsSection(page) {
2306
+ return page.getByTestId('agent-skills-content');
2307
+ }
2308
+ /** A row on the "Agent Skills" sub-tab, located by the skill's name. */
2309
+ function agentSkillRow(page, skillName) {
2310
+ return skillsSection(page)
2311
+ .getByTestId('agent-skill-row')
2312
+ .filter({ hasText: skillName });
2313
+ }
2314
+ /** A row on the "Available Skills" sub-tab, located by the skill's name. */
2315
+ function availableSkillRow(page, skillName) {
2316
+ return skillsSection(page)
2317
+ .getByTestId('available-skill-row')
2318
+ .filter({ hasText: skillName });
2319
+ }
2320
+ /**
2321
+ * Verify the Skills section is loaded: the section container and its
2322
+ * "Agent Skills" / "Available Skills" sub-tabs are visible.
2256
2323
  */
2257
2324
  async function verifySkillsTabVisible(page) {
2258
- // Either at least one skill toggle row is visible, or the empty message is shown
2259
- const hasSkills = await page
2260
- .getByRole('switch')
2325
+ const section = skillsSection(page);
2326
+ await test$1.expect(section).toBeVisible({ timeout: 10000 });
2327
+ await test$1.expect(page.getByTestId('agent-skills-tab-agent')).toBeVisible({ timeout: 10000 });
2328
+ await test$1.expect(page.getByTestId('agent-skills-tab-available')).toBeVisible({ timeout: 10000 });
2329
+ logger.info('Skills section is visible with Agent/Available sub-tabs');
2330
+ }
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.
2334
+ */
2335
+ async function switchToAgentSkillsSubTab(page) {
2336
+ await page.getByTestId('agent-skills-tab-agent').click();
2337
+ await test$1.expect(skillsSection(page)
2338
+ .getByTestId('agent-skill-row')
2261
2339
  .first()
2262
- .isVisible()
2263
- .catch(() => false);
2264
- if (hasSkills) {
2265
- logger.info('Skills tab is visible with skills rendered');
2266
- return;
2267
- }
2340
+ .or(page.getByText(/No skills enabled for this agent yet/i))).toBeVisible({ timeout: 10000 });
2341
+ logger.info('Switched to Agent Skills sub-tab');
2342
+ }
2343
+ /**
2344
+ * Switch to the "Available Skills" sub-tab (the platform catalog, paged
2345
+ * server-side 10 per page) and wait for its panel to render past the
2346
+ * loading spinner.
2347
+ */
2348
+ async function switchToAvailableSkillsSubTab(page) {
2349
+ await page.getByTestId('agent-skills-tab-available').click();
2350
+ await test$1.expect(skillsSection(page)
2351
+ .getByTestId('available-skill-row')
2352
+ .first()
2353
+ .or(page.getByText(/No skills available for this platform/i))).toBeVisible({ timeout: 10000 });
2354
+ logger.info('Switched to Available Skills sub-tab');
2355
+ }
2356
+ /**
2357
+ * Go to a page of the currently visible skills list via the numbered
2358
+ * IblPagination buttons (the pagination renders nothing on a single page).
2359
+ */
2360
+ async function goToSkillsListPage(page, pageNumber) {
2361
+ await skillsSection(page)
2362
+ .getByRole('navigation')
2363
+ .getByText(String(pageNumber), { exact: true })
2364
+ .click();
2365
+ logger.info(`Went to skills list page ${pageNumber}`);
2366
+ }
2367
+ /**
2368
+ * Verify the "Available Skills" empty state ("No skills available for this
2369
+ * platform…") is shown.
2370
+ */
2371
+ async function verifySkillsEmptyState(page) {
2268
2372
  await test$1.expect(page.getByText(/No skills available for this platform/i)).toBeVisible({
2269
2373
  timeout: 10000,
2270
2374
  });
2271
- logger.info('Skills tab is visible with empty state');
2375
+ logger.info('Available Skills empty state is displayed');
2272
2376
  }
2273
2377
  /**
2274
- * Verify that the "No skills available" empty state is shown.
2378
+ * Verify the "Agent Skills" empty state ("No skills enabled for this agent
2379
+ * yet…") is shown.
2275
2380
  */
2276
- async function verifySkillsEmptyState(page) {
2277
- await test$1.expect(page.getByText(/No skills available for this platform/i)).toBeVisible({
2381
+ async function verifyAgentSkillsEmptyState(page) {
2382
+ await test$1.expect(page.getByText(/No skills enabled for this agent yet/i)).toBeVisible({
2278
2383
  timeout: 10000,
2279
2384
  });
2280
- logger.info('Skills empty state is displayed');
2385
+ logger.info('Agent Skills empty state is displayed');
2281
2386
  }
2282
2387
  /**
2283
- * Count the number of skill rows rendered in the Skills tab.
2284
- * Each row represents an available (enabled) platform skill.
2388
+ * Count the skill rows on the "Agent Skills" sub-tab (max 10 per page).
2285
2389
  */
2286
2390
  async function getSkillRowCount(page) {
2287
- const rows = page.locator('div.rounded-lg.border.p-6').filter({ has: page.getByRole('switch') });
2288
- const count = await rows.count();
2289
- logger.info(`${count} skill row(s) displayed`);
2391
+ const count = await skillsSection(page).getByTestId('agent-skill-row').count();
2392
+ logger.info(`${count} agent skill row(s) displayed`);
2290
2393
  return count;
2291
2394
  }
2292
2395
  /**
2293
- * Verify that a specific skill row is visible in the Skills tab.
2396
+ * Verify that a specific skill row is visible on the "Agent Skills" sub-tab.
2294
2397
  */
2295
2398
  async function verifySkillVisible(page, skillName) {
2296
- const row = page
2297
- .locator('div.rounded-lg.border.p-6')
2298
- .filter({ hasText: skillName })
2299
- .filter({ has: page.getByRole('switch') });
2300
- await test$1.expect(row).toBeVisible({ timeout: 10000 });
2399
+ await test$1.expect(agentSkillRow(page, skillName)).toBeVisible({ timeout: 10000 });
2301
2400
  logger.info(`Skill "${skillName}" is visible`);
2302
2401
  }
2402
+ // ============================
2403
+ // Attach / Detach / Toggle
2404
+ // ============================
2405
+ /**
2406
+ * Attach a platform skill to the agent from the "Available Skills" sub-tab.
2407
+ * Waits for the toast and for the row to flip to its "Added" chip.
2408
+ */
2409
+ async function addSkillToAgent(page, skillName) {
2410
+ const row = availableSkillRow(page, skillName);
2411
+ await row.getByRole('button', { name: `Add ${skillName} to this agent` }).click();
2412
+ await test$1.expect(page.getByText(`${skillName} added`, { exact: false })).toBeVisible({
2413
+ timeout: 10000,
2414
+ });
2415
+ await test$1.expect(row.getByText('Added', { exact: true })).toBeVisible({ timeout: 10000 });
2416
+ logger.info(`Added skill "${skillName}" to the agent`);
2417
+ }
2303
2418
  /**
2304
- * Check if a specific skill is currently enabled for the mentor.
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.
2421
+ */
2422
+ async function verifySkillAdded(page, skillName) {
2423
+ const row = availableSkillRow(page, skillName);
2424
+ await test$1.expect(row.getByText('Added', { exact: true })).toBeVisible({ timeout: 10000 });
2425
+ await test$1.expect(row.getByRole('button', { name: `Add ${skillName} to this agent` })).toBeHidden();
2426
+ logger.info(`Skill "${skillName}" shows as Added`);
2427
+ }
2428
+ /**
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.
2431
+ */
2432
+ async function openSkillActionsMenu(page, skillName) {
2433
+ const actionsButton = page.getByRole('button', { name: `${skillName} actions` });
2434
+ await test$1.expect(actionsButton).toBeVisible({ timeout: 10000 });
2435
+ await actionsButton.click();
2436
+ const menu = page.getByRole('menu');
2437
+ await test$1.expect(menu).toBeVisible({ timeout: 5000 });
2438
+ return menu;
2439
+ }
2440
+ /**
2441
+ * Detach a skill from the agent via its row menu ("Remove from Agent") on the
2442
+ * "Agent Skills" sub-tab. Waits for the toast and for the row to disappear.
2443
+ */
2444
+ async function removeSkillFromAgent(page, skillName) {
2445
+ const menu = await openSkillActionsMenu(page, skillName);
2446
+ await menu.getByRole('menuitem', { name: /Remove from Agent/i }).click();
2447
+ await test$1.expect(page.getByText(`${skillName} removed`, { exact: false })).toBeVisible({
2448
+ timeout: 10000,
2449
+ });
2450
+ logger.info(`Removed skill "${skillName}" from the agent`);
2451
+ }
2452
+ /**
2453
+ * Check if a specific skill is currently enabled (its row Switch state) on
2454
+ * the "Agent Skills" sub-tab.
2305
2455
  */
2306
2456
  async function isSkillEnabled(page, skillName) {
2307
- const switchEl = page.getByLabel(new RegExp(`^${skillName} (enabled|disabled)$`));
2457
+ const switchEl = agentSkillRow(page, skillName).getByRole('switch');
2308
2458
  await test$1.expect(switchEl).toBeVisible({ timeout: 10000 });
2309
2459
  const enabled = await switchEl.isChecked();
2310
2460
  logger.info(`Skill "${skillName}" is ${enabled ? 'enabled' : 'disabled'}`);
2311
2461
  return enabled;
2312
2462
  }
2313
2463
  /**
2314
- * Enable a skill for the current mentor by toggling it on.
2315
- * No-op if already enabled. Waits for the success toast.
2464
+ * Enable an attached skill by flipping its Switch on (PATCHes the assignment;
2465
+ * attaching itself is `addSkillToAgent`). No-op if already enabled.
2316
2466
  */
2317
2467
  async function enableSkill(page, skillName) {
2318
- const disabledSwitch = page.getByLabel(`${skillName} disabled`);
2319
- const isDisabled = await disabledSwitch.isVisible().catch(() => false);
2320
- if (!isDisabled) {
2468
+ if (await isSkillEnabled(page, skillName)) {
2321
2469
  logger.info(`Skill "${skillName}" already enabled`);
2322
2470
  return;
2323
2471
  }
2324
- await disabledSwitch.click();
2472
+ await agentSkillRow(page, skillName).getByRole('switch').click();
2325
2473
  await test$1.expect(page.getByText(`${skillName} enabled`, { exact: false })).toBeVisible({
2326
2474
  timeout: 10000,
2327
2475
  });
2328
2476
  logger.info(`Enabled skill "${skillName}"`);
2329
2477
  }
2330
2478
  /**
2331
- * Disable a skill for the current mentor by toggling it off.
2332
- * No-op if already disabled. Waits for the success toast.
2479
+ * Disable an attached skill by flipping its Switch off, without detaching it
2480
+ * (detaching is `removeSkillFromAgent`). No-op if already disabled.
2333
2481
  */
2334
2482
  async function disableSkill(page, skillName) {
2335
- const enabledSwitch = page.getByLabel(`${skillName} enabled`);
2336
- const isEnabled = await enabledSwitch.isVisible().catch(() => false);
2337
- if (!isEnabled) {
2483
+ if (!(await isSkillEnabled(page, skillName))) {
2338
2484
  logger.info(`Skill "${skillName}" already disabled`);
2339
2485
  return;
2340
2486
  }
2341
- await enabledSwitch.click();
2487
+ await agentSkillRow(page, skillName).getByRole('switch').click();
2342
2488
  await test$1.expect(page.getByText(`${skillName} disabled`, { exact: false })).toBeVisible({
2343
2489
  timeout: 10000,
2344
2490
  });
@@ -2358,188 +2504,291 @@ async function toggleSkill(page, skillName) {
2358
2504
  return true;
2359
2505
  }
2360
2506
  /**
2361
- * Open the "New Skill" dialog by clicking the New Skill button in the Skills tab.
2362
- * Returns the dialog locator.
2507
+ * Open the "New Skill" dialog from the Skills section. Returns the dialog
2508
+ * locator — resolve every field from it (the dialog stacks over the
2509
+ * edit-agent dialog, so page-level field queries would be ambiguous).
2363
2510
  */
2364
2511
  async function openNewSkillDialog(page) {
2365
- const newButton = page.getByRole('button', { name: /^New Skill$/i });
2366
- await test$1.expect(newButton).toBeVisible({ timeout: 10000 });
2367
- await newButton.click();
2368
- const dialog = page.getByRole('dialog').filter({ hasText: 'New Skill' });
2512
+ await skillsSection(page).getByRole('button', { name: /^New Skill$/ }).click();
2513
+ // Accessible name comes from the dialog title only — never matches the
2514
+ // "New Skill" button text inside the edit-agent dialog behind it.
2515
+ const dialog = page.getByRole('dialog', { name: 'New Skill' });
2369
2516
  return waitForDialogReady(page, dialog);
2370
2517
  }
2371
2518
  /**
2372
- * Type content into the RichTextEditor Instruction field.
2373
- * The editor renders as a contenteditable element.
2374
- *
2375
- * `dialog` scopes the lookups to the open skill modal so the
2376
- * contenteditable `.first()` and the "Instruction" label don't match
2377
- * elements on the modal behind it. The keyboard is page-global.
2519
+ * Type content into the RichTextEditor Instruction field (a contenteditable
2520
+ * region). `dialog` scopes the editor lookup to the open skill modal.
2378
2521
  */
2379
2522
  async function fillInstructionEditor(dialog, content) {
2380
2523
  const page = dialog.page();
2381
- // RichTextEditor uses a contenteditable region
2382
2524
  const editor = dialog.locator('[contenteditable="true"]').first();
2383
- const hasEditor = await editor.isVisible().catch(() => false);
2384
- if (hasEditor) {
2385
- await editor.click();
2386
- await page.keyboard.press('ControlOrMeta+A');
2387
- await page.keyboard.press('Delete');
2388
- if (content) {
2389
- await page.keyboard.type(content);
2390
- }
2391
- return;
2525
+ await test$1.expect(editor).toBeVisible({ timeout: 5000 });
2526
+ await editor.click();
2527
+ await page.keyboard.press('ControlOrMeta+A');
2528
+ await page.keyboard.press('Delete');
2529
+ if (content) {
2530
+ await page.keyboard.type(content);
2392
2531
  }
2393
- // Fallback: a plain labelled input/textarea named "Instruction"
2394
- const textarea = dialog.getByLabel('Instruction');
2395
- await textarea.fill(content);
2396
2532
  }
2397
2533
  /**
2398
- * Fill the skill form fields (name, slug, description, version, instruction).
2399
- * The form is used by both the New and Edit dialogs. `dialog` scopes every
2400
- * field query to the open modal (see fillInstructionEditor).
2401
- * The Instruction field is a rich-text (markdown) editor.
2534
+ * Fill the skill form (used by both the New and Edit dialogs). Every field is
2535
+ * resolved from `dialog`; labels use exact matches so "Name" can never match
2536
+ * a "File Name" field in a stacked resource dialog.
2402
2537
  */
2403
2538
  async function fillSkillForm(dialog, values) {
2404
- await dialog.getByLabel('Name').fill(values.name);
2405
- await dialog.getByLabel('Slug').fill(values.slug);
2539
+ if (values.onlyThisAgent !== undefined) {
2540
+ const privateSwitch = dialog.getByRole('switch', { name: 'Only This Agent' });
2541
+ if ((await privateSwitch.isChecked()) !== values.onlyThisAgent) {
2542
+ await privateSwitch.click();
2543
+ }
2544
+ }
2545
+ await dialog.getByLabel('Name', { exact: true }).fill(values.name);
2546
+ await dialog.getByLabel('Slug', { exact: true }).fill(values.slug);
2406
2547
  if (values.version !== undefined) {
2407
- await dialog.getByLabel('Version').fill(values.version);
2548
+ await dialog.getByLabel('Version', { exact: true }).fill(values.version);
2549
+ }
2550
+ if (values.category !== undefined) {
2551
+ await dialog.getByLabel('Category', { exact: true }).fill(values.category);
2408
2552
  }
2409
2553
  if (values.description !== undefined) {
2410
- await dialog.getByLabel('Description').fill(values.description);
2554
+ await dialog.getByLabel('Description', { exact: true }).fill(values.description);
2411
2555
  }
2412
2556
  if (values.instruction !== undefined) {
2413
2557
  await fillInstructionEditor(dialog, values.instruction);
2414
2558
  }
2415
2559
  }
2416
2560
  /**
2417
- * Create a new platform-level skill via the Skills tab.
2561
+ * Create a new platform-level skill via the Skills section.
2418
2562
  */
2419
2563
  async function createSkill(page, values) {
2420
2564
  const dialog = await openNewSkillDialog(page);
2421
2565
  await fillSkillForm(dialog, values);
2422
- const createButton = dialog.getByRole('button', { name: /^Create$/i });
2566
+ const createButton = dialog.getByRole('button', { name: /^Create$/ });
2423
2567
  await test$1.expect(createButton).toBeEnabled({ timeout: 5000 });
2424
2568
  await createButton.click();
2425
2569
  await test$1.expect(page.getByText('Skill created', { exact: false })).toBeVisible({
2426
2570
  timeout: 10000,
2427
2571
  });
2572
+ await test$1.expect(dialog).toBeHidden({ timeout: 5000 });
2428
2573
  logger.info(`Created skill "${values.name}"`);
2429
2574
  }
2430
2575
  /**
2431
- * Open the Actions dropdown for a specific skill row.
2432
- * Returns the dropdown menu locator.
2433
- */
2434
- async function openSkillActionsMenu(page, skillName) {
2435
- const actionsButton = page.getByRole('button', { name: `${skillName} actions` });
2436
- await test$1.expect(actionsButton).toBeVisible({ timeout: 10000 });
2437
- await actionsButton.click();
2438
- const menu = page.getByRole('menu');
2439
- await test$1.expect(menu).toBeVisible({ timeout: 5000 });
2440
- return menu;
2441
- }
2442
- /**
2443
- * Open the Edit Skill dialog from the actions menu.
2444
- * Returns the dialog locator.
2576
+ * Open the Edit Skill dialog for a row (lands on the General sub-tab).
2577
+ * Returns the dialog locator — resolve fields and sub-tabs from it.
2445
2578
  */
2446
2579
  async function openEditSkillDialog(page, skillName) {
2447
- await openSkillActionsMenu(page, skillName);
2448
- const editItem = page.getByRole('menuitem', { name: /Edit/i });
2449
- await test$1.expect(editItem).toBeVisible({ timeout: 5000 });
2450
- await editItem.click();
2451
- const dialog = page.getByRole('dialog').filter({ hasText: 'Edit Skill' });
2580
+ const menu = await openSkillActionsMenu(page, skillName);
2581
+ await menu.getByRole('menuitem', { name: /^Edit$/ }).click();
2582
+ const dialog = page.getByRole('dialog', { name: 'Edit Skill' });
2452
2583
  return waitForDialogReady(page, dialog);
2453
2584
  }
2454
2585
  /**
2455
- * Edit an existing platform-level skill.
2456
- * @param skillName - The current name of the skill to edit
2457
- * @param updates - Fields to update on the skill
2586
+ * Edit an existing platform-level skill (General sub-tab fields).
2458
2587
  */
2459
2588
  async function editSkill(page, skillName, updates) {
2460
2589
  const dialog = await openEditSkillDialog(page, skillName);
2590
+ if (updates.onlyThisAgent !== undefined) {
2591
+ const privateSwitch = dialog.getByRole('switch', { name: 'Only This Agent' });
2592
+ if ((await privateSwitch.isChecked()) !== updates.onlyThisAgent) {
2593
+ await privateSwitch.click();
2594
+ }
2595
+ }
2461
2596
  if (updates.name !== undefined) {
2462
- await dialog.getByLabel('Name').fill(updates.name);
2597
+ await dialog.getByLabel('Name', { exact: true }).fill(updates.name);
2463
2598
  }
2464
2599
  if (updates.slug !== undefined) {
2465
- await dialog.getByLabel('Slug').fill(updates.slug);
2600
+ await dialog.getByLabel('Slug', { exact: true }).fill(updates.slug);
2466
2601
  }
2467
2602
  if (updates.version !== undefined) {
2468
- await dialog.getByLabel('Version').fill(updates.version);
2603
+ await dialog.getByLabel('Version', { exact: true }).fill(updates.version);
2604
+ }
2605
+ if (updates.category !== undefined) {
2606
+ await dialog.getByLabel('Category', { exact: true }).fill(updates.category);
2469
2607
  }
2470
2608
  if (updates.description !== undefined) {
2471
- await dialog.getByLabel('Description').fill(updates.description);
2609
+ await dialog.getByLabel('Description', { exact: true }).fill(updates.description);
2472
2610
  }
2473
2611
  if (updates.instruction !== undefined) {
2474
2612
  await fillInstructionEditor(dialog, updates.instruction);
2475
2613
  }
2476
- const saveButton = dialog.getByRole('button', { name: /^Save$/i });
2614
+ const saveButton = dialog.getByRole('button', { name: /^Save$/ });
2477
2615
  await test$1.expect(saveButton).toBeEnabled({ timeout: 5000 });
2478
2616
  await saveButton.click();
2479
2617
  await test$1.expect(page.getByText('Skill updated', { exact: false })).toBeVisible({
2480
2618
  timeout: 10000,
2481
2619
  });
2482
- logger.info(`Edited skill "${skillName}" with updates: ${JSON.stringify(updates)}`);
2620
+ await test$1.expect(dialog).toBeHidden({ timeout: 5000 });
2621
+ logger.info(`Edited skill "${skillName}"`);
2483
2622
  }
2484
2623
  /**
2485
- * Delete a platform-level skill, confirming the deletion dialog.
2624
+ * Delete a platform-level skill via its row menu, confirming the
2625
+ * "Delete Skill" dialog.
2486
2626
  */
2487
2627
  async function deleteSkill(page, skillName) {
2488
- await openSkillActionsMenu(page, skillName);
2489
- const deleteItem = page.getByRole('menuitem', { name: /Delete/i });
2490
- await test$1.expect(deleteItem).toBeVisible({ timeout: 5000 });
2491
- await deleteItem.click();
2492
- const dialog = page.getByRole('dialog').filter({ hasText: 'Delete Skill' });
2493
- await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
2494
- const confirmButton = dialog.getByRole('button', { name: /^Delete$/i });
2495
- await test$1.expect(confirmButton).toBeVisible({ timeout: 5000 });
2496
- await confirmButton.click();
2628
+ const menu = await openSkillActionsMenu(page, skillName);
2629
+ await menu.getByRole('menuitem', { name: /^Delete$/ }).click();
2630
+ const confirm = page.getByRole('dialog', { name: 'Delete Skill' });
2631
+ await test$1.expect(confirm).toBeVisible({ timeout: 5000 });
2632
+ await confirm.getByRole('button', { name: /^Delete$/ }).click();
2497
2633
  await test$1.expect(page.getByText('Skill deleted', { exact: false })).toBeVisible({
2498
2634
  timeout: 10000,
2499
2635
  });
2636
+ await test$1.expect(confirm).toBeHidden({ timeout: 5000 });
2500
2637
  logger.info(`Deleted skill "${skillName}"`);
2501
2638
  }
2502
2639
  /**
2503
2640
  * Open the delete confirmation for a skill and cancel it (no deletion).
2504
2641
  */
2505
2642
  async function cancelDeleteSkill(page, skillName) {
2506
- await openSkillActionsMenu(page, skillName);
2507
- const deleteItem = page.getByRole('menuitem', { name: /Delete/i });
2508
- await deleteItem.click();
2509
- const dialog = page.getByRole('dialog').filter({ hasText: 'Delete Skill' });
2510
- await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
2511
- const cancelButton = dialog.getByRole('button', { name: /Cancel/i });
2512
- await cancelButton.click();
2513
- await test$1.expect(dialog).not.toBeVisible({ timeout: 5000 });
2643
+ const menu = await openSkillActionsMenu(page, skillName);
2644
+ await menu.getByRole('menuitem', { name: /^Delete$/ }).click();
2645
+ const confirm = page.getByRole('dialog', { name: 'Delete Skill' });
2646
+ await test$1.expect(confirm).toBeVisible({ timeout: 5000 });
2647
+ await confirm.getByRole('button', { name: /^Cancel$/ }).click();
2648
+ await test$1.expect(confirm).toBeHidden({ timeout: 5000 });
2514
2649
  logger.info(`Cancelled delete of skill "${skillName}"`);
2515
2650
  }
2516
2651
  // ============================
2517
- // Combined Workflow Helpers
2652
+ // Skill Resources (Edit Skill → Resources sub-tab)
2518
2653
  // ============================
2519
2654
  /**
2520
- * Full workflow: open Sandbox tab, create a new instance, connect the mentor to it.
2521
- * Returns when the mentor is successfully connected.
2655
+ * Switch the open Edit Skill dialog to its Resources sub-tab. Pass the
2656
+ * dialog returned by `openEditSkillDialog`; returns the resources section
2657
+ * locator to scope row lookups.
2522
2658
  */
2523
- async function setupSandboxInstance(page, instance) {
2524
- await switchToSandboxTab(page);
2525
- await createInstance(page, instance);
2526
- await connectToInstance(page, instance.name);
2527
- await verifyConnectedInstanceCard(page);
2528
- logger.info(`Sandbox setup complete for "${instance.name}"`);
2659
+ async function switchToSkillResourcesSubTab(editSkillDialog) {
2660
+ await editSkillDialog.getByTestId('skill-dialog-tab-files').click();
2661
+ const section = editSkillDialog.getByTestId('skill-resources-section');
2662
+ await test$1.expect(section).toBeVisible({ timeout: 5000 });
2663
+ logger.info('Switched to the Resources sub-tab');
2664
+ return section;
2665
+ }
2666
+ /** A resource row inside the Resources sub-tab, located by filename. */
2667
+ function resourceRow(section, filename) {
2668
+ return section.getByTestId('skill-resource-row').filter({ hasText: filename });
2529
2669
  }
2530
2670
  /**
2531
- * Full workflow: open Sandbox tab, disconnect the current instance (if any),
2532
- * then delete it from the table.
2671
+ * Open the "New Resource" dialog from the resources section. Returns the
2672
+ * dialog locator (it stacks over the Edit Skill dialog).
2533
2673
  */
2534
- async function teardownSandboxInstance(page, instanceName) {
2535
- await switchToSandboxTab(page);
2536
- // If a connected instance is present, disconnect first
2537
- const disconnectButton = page.getByRole('button', { name: /^Disconnect$/i });
2538
- if (await disconnectButton.isVisible().catch(() => false)) {
2539
- await disconnectInstance(page);
2674
+ async function openNewResourceDialog(page, resourcesSection) {
2675
+ await resourcesSection.getByRole('button', { name: /^New Resource$/ }).click();
2676
+ const dialog = page.getByRole('dialog', { name: 'New Resource' });
2677
+ await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
2678
+ return dialog;
2679
+ }
2680
+ /**
2681
+ * Add a text resource (reference/script) via the "New Resource" dialog.
2682
+ * The type Select's listbox portals inside the dialog panel, so options are
2683
+ * resolved from the dialog too.
2684
+ */
2685
+ async function addTextResource(page, resourcesSection, { filename, content, type = 'Reference' }) {
2686
+ const dialog = await openNewResourceDialog(page, resourcesSection);
2687
+ if (type !== 'Reference') {
2688
+ await dialog.getByRole('combobox').click();
2689
+ await dialog.getByRole('option', { name: type, exact: true }).click();
2540
2690
  }
2541
- await deleteInstance(page, instanceName);
2542
- logger.info(`Sandbox teardown complete for "${instanceName}"`);
2691
+ await dialog.getByLabel('File Name', { exact: true }).fill(filename);
2692
+ await dialog.getByLabel('Content', { exact: true }).fill(content);
2693
+ await dialog.getByRole('button', { name: /^Add Resource$/ }).click();
2694
+ await test$1.expect(page.getByText('Resource added', { exact: false })).toBeVisible({
2695
+ timeout: 10000,
2696
+ });
2697
+ await test$1.expect(dialog).toBeHidden({ timeout: 5000 });
2698
+ await test$1.expect(resourceRow(resourcesSection, filename)).toBeVisible({ timeout: 10000 });
2699
+ logger.info(`Added text resource "${filename}"`);
2700
+ }
2701
+ /**
2702
+ * Upload a binary asset resource via the "New Resource" dialog. `filePath`
2703
+ * is passed to Playwright's setInputFiles (absolute or test-relative).
2704
+ */
2705
+ async function uploadAssetResource(page, resourcesSection, filePath) {
2706
+ const dialog = await openNewResourceDialog(page, resourcesSection);
2707
+ await dialog.getByRole('combobox').click();
2708
+ await dialog.getByRole('option', { name: 'Asset', exact: true }).click();
2709
+ await dialog.getByTestId('resource-file-input').setInputFiles(filePath);
2710
+ await dialog.getByRole('button', { name: /^Upload$/ }).click();
2711
+ await test$1.expect(page.getByText('Resource uploaded', { exact: false })).toBeVisible({
2712
+ timeout: 30000,
2713
+ });
2714
+ await test$1.expect(dialog).toBeHidden({ timeout: 5000 });
2715
+ logger.info(`Uploaded asset resource from "${filePath}"`);
2716
+ }
2717
+ /**
2718
+ * Open a resource row's three-dots menu. The menu content portals to
2719
+ * document.body (above the dialog), so the returned locator is page-level.
2720
+ */
2721
+ async function openResourceActionsMenu(page, resourcesSection, filename) {
2722
+ await resourceRow(resourcesSection, filename)
2723
+ .getByRole('button', { name: `${filename} actions` })
2724
+ .click();
2725
+ const menu = page.getByRole('menu');
2726
+ await test$1.expect(menu).toBeVisible({ timeout: 5000 });
2727
+ return menu;
2728
+ }
2729
+ /**
2730
+ * Edit a text resource in place via its row menu ("Edit Resource" dialog).
2731
+ */
2732
+ async function editTextResource(page, resourcesSection, filename, updates) {
2733
+ const menu = await openResourceActionsMenu(page, resourcesSection, filename);
2734
+ await menu.getByRole('menuitem', { name: /^Edit$/ }).click();
2735
+ const dialog = page.getByRole('dialog', { name: 'Edit Resource' });
2736
+ await test$1.expect(dialog).toBeVisible({ timeout: 5000 });
2737
+ if (updates.filename !== undefined) {
2738
+ await dialog.getByLabel('File Name', { exact: true }).fill(updates.filename);
2739
+ }
2740
+ if (updates.content !== undefined) {
2741
+ await dialog.getByLabel('Content', { exact: true }).fill(updates.content);
2742
+ }
2743
+ await dialog.getByRole('button', { name: /^Save$/ }).click();
2744
+ await test$1.expect(page.getByText('Resource saved', { exact: false })).toBeVisible({
2745
+ timeout: 10000,
2746
+ });
2747
+ await test$1.expect(dialog).toBeHidden({ timeout: 5000 });
2748
+ logger.info(`Edited resource "${filename}"`);
2749
+ }
2750
+ /**
2751
+ * Delete a resource row via its menu, confirming the "Delete Resource"
2752
+ * dialog. Waits for the toast and for the row to disappear.
2753
+ */
2754
+ async function deleteResource(page, resourcesSection, filename) {
2755
+ const menu = await openResourceActionsMenu(page, resourcesSection, filename);
2756
+ await menu.getByRole('menuitem', { name: /^Delete$/ }).click();
2757
+ const confirm = page.getByRole('dialog', { name: 'Delete Resource' });
2758
+ await test$1.expect(confirm).toBeVisible({ timeout: 5000 });
2759
+ await confirm.getByRole('button', { name: /^Delete$/ }).click();
2760
+ await test$1.expect(page.getByText('Resource deleted', { exact: false })).toBeVisible({
2761
+ timeout: 10000,
2762
+ });
2763
+ await test$1.expect(confirm).toBeHidden({ timeout: 5000 });
2764
+ await test$1.expect(resourceRow(resourcesSection, filename)).toBeHidden({ timeout: 10000 });
2765
+ logger.info(`Deleted resource "${filename}"`);
2766
+ }
2767
+ // ============================
2768
+ // Chat `/` Skill Picker
2769
+ // ============================
2770
+ /**
2771
+ * Type a `/` query into the chat composer and wait for the skill picker
2772
+ * listbox to open (its lazy fetch may show a loading row first on cold
2773
+ * cache — waiting on the listbox element covers both paths).
2774
+ */
2775
+ async function openSlashSkillPicker(page, query = '/') {
2776
+ const composer = page.locator('#chat-input-textarea');
2777
+ await test$1.expect(composer).toBeVisible({ timeout: 10000 });
2778
+ await composer.click();
2779
+ await composer.fill(query);
2780
+ await test$1.expect(page.getByTestId('slash-skill-picker')).toBeVisible({ timeout: 15000 });
2781
+ logger.info(`Slash skill picker open for query "${query}"`);
2782
+ }
2783
+ /**
2784
+ * Pick a skill from the open `/` picker by its visible name. The picker
2785
+ * closes and the composer is left holding the inserted `/slug ` invocation.
2786
+ */
2787
+ async function selectSlashSkill(page, skillName) {
2788
+ const picker = page.getByTestId('slash-skill-picker');
2789
+ await picker.getByRole('option', { name: new RegExp(skillName, 'i') }).click();
2790
+ await test$1.expect(picker).toBeHidden({ timeout: 5000 });
2791
+ logger.info(`Selected slash skill "${skillName}"`);
2543
2792
  }
2544
2793
 
2545
2794
  // ============================
@@ -6951,6 +7200,8 @@ exports.addGraderCriterion = addGraderCriterion;
6951
7200
  exports.addManualScore = addManualScore;
6952
7201
  exports.addMemory = addMemory;
6953
7202
  exports.addQaPairsManually = addQaPairsManually;
7203
+ exports.addSkillToAgent = addSkillToAgent;
7204
+ exports.addTextResource = addTextResource;
6954
7205
  exports.archiveFirstMemory = archiveFirstMemory;
6955
7206
  exports.archiveMemoryByContent = archiveMemoryByContent;
6956
7207
  exports.billingAutoRechargeSection = billingAutoRechargeSection;
@@ -7011,6 +7262,7 @@ exports.deleteInstance = deleteInstance;
7011
7262
  exports.deleteKey = deleteKey;
7012
7263
  exports.deleteMemoryByContent = deleteMemoryByContent;
7013
7264
  exports.deleteQaItem = deleteQaItem;
7265
+ exports.deleteResource = deleteResource;
7014
7266
  exports.deleteSkill = deleteSkill;
7015
7267
  exports.deleteTask = deleteTask;
7016
7268
  exports.disableSkill = disableSkill;
@@ -7021,6 +7273,7 @@ exports.editGraderCriterion = editGraderCriterion;
7021
7273
  exports.editInstance = editInstance;
7022
7274
  exports.editLink = editLink;
7023
7275
  exports.editSkill = editSkill;
7276
+ exports.editTextResource = editTextResource;
7024
7277
  exports.editTool = editTool;
7025
7278
  exports.enableSkill = enableSkill;
7026
7279
  exports.enableSupport = enableSupport;
@@ -7217,6 +7470,7 @@ exports.goToLastPage = goToLastPage;
7217
7470
  exports.goToNextPage = goToNextPage;
7218
7471
  exports.goToPage = goToPage;
7219
7472
  exports.goToPreviousPage = goToPreviousPage;
7473
+ exports.goToSkillsListPage = goToSkillsListPage;
7220
7474
  exports.graderTabBody = graderTabBody;
7221
7475
  exports.inviteUserTest = inviteUserTest;
7222
7476
  exports.isEvaluationTabVisible = isEvaluationTabVisible;
@@ -7275,6 +7529,7 @@ exports.openRunResults = openRunResults;
7275
7529
  exports.openScheduleTaskDialog = openScheduleTaskDialog;
7276
7530
  exports.openScreenSharePromptEditor = openScreenSharePromptEditor;
7277
7531
  exports.openSkillActionsMenu = openSkillActionsMenu;
7532
+ exports.openSlashSkillPicker = openSlashSkillPicker;
7278
7533
  exports.openStartEvaluationDialog = openStartEvaluationDialog;
7279
7534
  exports.openTicket = openTicket;
7280
7535
  exports.overrideGradeResult = overrideGradeResult;
@@ -7290,6 +7545,7 @@ exports.refreshEvaluationDetail = refreshEvaluationDetail;
7290
7545
  exports.refreshTickets = refreshTickets;
7291
7546
  exports.reliableClick = reliableClick;
7292
7547
  exports.reliableFill = reliableFill;
7548
+ exports.removeSkillFromAgent = removeSkillFromAgent;
7293
7549
  exports.removeTraceScore = removeTraceScore;
7294
7550
  exports.renameKey = renameKey;
7295
7551
  exports.replyToTicket = replyToTicket;
@@ -7316,6 +7572,7 @@ exports.selectLLMModel = selectLLMModel;
7316
7572
  exports.selectLlmProvider = selectLlmProvider;
7317
7573
  exports.selectPrivacyAction = selectPrivacyAction;
7318
7574
  exports.selectPrivateMode = selectPrivateMode;
7575
+ exports.selectSlashSkill = selectSlashSkill;
7319
7576
  exports.selectSttProvider = selectSttProvider;
7320
7577
  exports.selectTaskInList = selectTaskInList;
7321
7578
  exports.selectToolKeySetMode = selectToolKeySetMode;
@@ -7358,6 +7615,8 @@ exports.submitLinkModal = submitLinkModal;
7358
7615
  exports.submitLlmJudge = submitLlmJudge;
7359
7616
  exports.submitToolModal = submitToolModal;
7360
7617
  exports.switchToAddItemsSubTab = switchToAddItemsSubTab;
7618
+ exports.switchToAgentSkillsSubTab = switchToAgentSkillsSubTab;
7619
+ exports.switchToAvailableSkillsSubTab = switchToAvailableSkillsSubTab;
7361
7620
  exports.switchToEvaluationTab = switchToEvaluationTab;
7362
7621
  exports.switchToGraderSubTab = switchToGraderSubTab;
7363
7622
  exports.switchToGraderTab = switchToGraderTab;
@@ -7368,6 +7627,7 @@ exports.switchToPrivacyTab = switchToPrivacyTab;
7368
7627
  exports.switchToPrivateModeTab = switchToPrivateModeTab;
7369
7628
  exports.switchToSandboxTab = switchToSandboxTab;
7370
7629
  exports.switchToScreenShareTab = switchToScreenShareTab;
7630
+ exports.switchToSkillResourcesSubTab = switchToSkillResourcesSubTab;
7371
7631
  exports.switchToSkillsTab = switchToSkillsTab;
7372
7632
  exports.switchToSupportTab = switchToSupportTab;
7373
7633
  exports.switchToTasksTab = switchToTasksTab;
@@ -7379,8 +7639,10 @@ exports.toggleAutoPush = toggleAutoPush;
7379
7639
  exports.toggleMemorySwitch = toggleMemorySwitch;
7380
7640
  exports.toggleSkill = toggleSkill;
7381
7641
  exports.toolFields = toolFields;
7642
+ exports.uploadAssetResource = uploadAssetResource;
7382
7643
  exports.uploadQaCsv = uploadQaCsv;
7383
7644
  exports.verifyAgentConfigPromptsVisible = verifyAgentConfigPromptsVisible;
7645
+ exports.verifyAgentSkillsEmptyState = verifyAgentSkillsEmptyState;
7384
7646
  exports.verifyAuditLogEmptyState = verifyAuditLogEmptyState;
7385
7647
  exports.verifyAuditLogEntryStructure = verifyAuditLogEntryStructure;
7386
7648
  exports.verifyAuditLogGenericError = verifyAuditLogGenericError;
@@ -7400,6 +7662,7 @@ exports.verifyMemoryNotExists = verifyMemoryNotExists;
7400
7662
  exports.verifyMemoryTabMemoriesList = verifyMemoryTabMemoriesList;
7401
7663
  exports.verifyMemoryTabSettings = verifyMemoryTabSettings;
7402
7664
  exports.verifyPreparingPhase = verifyPreparingPhase;
7665
+ exports.verifySkillAdded = verifySkillAdded;
7403
7666
  exports.verifySkillVisible = verifySkillVisible;
7404
7667
  exports.verifySkillsEmptyState = verifySkillsEmptyState;
7405
7668
  exports.verifySkillsTabVisible = verifySkillsTabVisible;