diff --git a/src/tools/ask-browserstack/register.ts b/src/tools/ask-browserstack/register.ts index 380a4c4c..85f06389 100644 --- a/src/tools/ask-browserstack/register.ts +++ b/src/tools/ask-browserstack/register.ts @@ -551,7 +551,7 @@ export function addAskBrowserStackAITool( product: z .enum(PRODUCTS) .describe( - "Which product to work in: tm (Test Management), " + + "Which product to work in: tm (Test Management), a11y (Accessibility), " + "tra (Test Reporting & Analytics).", ), // OPTIONAL, because a resume does not carry one. `next_step` tells the model to diff --git a/src/tools/ask-browserstack/types.ts b/src/tools/ask-browserstack/types.ts index b490ae4d..b0b21850 100644 --- a/src/tools/ask-browserstack/types.ts +++ b/src/tools/ask-browserstack/types.ts @@ -7,11 +7,10 @@ * other half will never send. Nothing here changes without changing that document first. */ -// a11y is deliberately ABSENT while Ask AI is in limited alpha. Atlas itself serves the -// product; this tool just does not offer it yet. Re-adding it means this list, the `product` -// describe text in register.ts, and the two a11y handoffs in tool-handoff.ts — which stop -// pointing here precisely because the call would now be rejected. -export const PRODUCTS = ["tm", "tra"] as const; +// Dropping a product means this list, the `product` describe text in register.ts, and any +// handoff in tool-handoff.ts that sends the model here with that product — the two a11y ones +// do — or those handoffs become instructions to make a call the schema rejects. +export const PRODUCTS = ["tm", "a11y", "tra"] as const; export type Product = (typeof PRODUCTS)[number]; /** CONTRACT §2 — what Atlas emits on the run's stream when its gate needs a human. */ diff --git a/src/tools/tool-handoff.ts b/src/tools/tool-handoff.ts index ce41a2b3..53c5adff 100644 --- a/src/tools/tool-handoff.ts +++ b/src/tools/tool-handoff.ts @@ -91,13 +91,14 @@ export const NEEDS_SESSION_ID = /** A completed scan's ids come from startAccessibilityScan, or from the agent. */ export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan, which are returned by startAccessibilityScan. " + - "No tool here lists past scans, so if you do not have the ids, start a new scan rather " + - "than guessing."; + "No tool here lists past scans; for one run earlier, call askBrowserStackAI with product " + + '"a11y" to locate it, or start a new scan rather than guessing.'; /** Auth-config ids are not listable by any tool here. */ export const NEEDS_A11Y_CONFIG_ID = " Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " + - "existing configurations, so if you do not have the id, create one rather than guessing."; + "existing configurations, so if you do not have the id, call askBrowserStackAI with " + + 'product "a11y" to find it, or create one rather than guessing.'; /** Test ids come from listTestIds, which itself needs a build id. */ export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds"); diff --git a/tests/tools/askBrowserstackE2E.test.ts b/tests/tools/askBrowserstackE2E.test.ts index 19969038..9c49e44f 100644 --- a/tests/tools/askBrowserstackE2E.test.ts +++ b/tests/tools/askBrowserstackE2E.test.ts @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { AskError } from "../../src/tools/ask-browserstack/config.js"; import { resetTokenCache } from "../../src/tools/ask-browserstack/central-oauth.js"; import { addAskBrowserStackAITool } from "../../src/tools/ask-browserstack/register.js"; +import { NEEDS_A11Y_CONFIG_ID, NEEDS_A11Y_SCAN_ID } from "../../src/tools/tool-handoff.js"; // `apiClient` adapted onto the global `fetch` stub these tests already install. // @@ -294,20 +295,25 @@ describe("askBrowserStackAI, end to end through the server factory", () => { expect(description).toMatch(/One task per call/); }); - it("offers tm and tra only — a11y is not on the tool while it is in alpha", async () => { - // Atlas serves a11y; this TOOL does not offer it yet. Pinned because dropping a - // product is a one-word edit that four other places have to agree with, and the two - // a11y handoffs in tool-handoff.ts used to send the model here with product "a11y" — - // which the schema now rejects, so a silent re-add would strand them again. + it("offers tm, a11y and tra, and every handoff that names a product names one of them", async () => { + // Pinned because changing a product is a one-word edit that other places have to agree + // with: the `product` describe text, and the a11y handoffs in tool-handoff.ts, which + // send the model here with product "a11y". Drop a11y from the enum without moving those + // and they become instructions to make a call the schema rejects. const server = await buildServer(); const shape = (server.getTools().askBrowserStackAI as any).inputSchema?.shape; const values = shape?.product?._def?.values ?? shape?.product?.options; - expect(values).toEqual(["tm", "tra"]); - expect(values).not.toContain("a11y"); + expect(values).toEqual(["tm", "a11y", "tra"]); - const description = (server.getTools().askBrowserStackAI as any).description as string; - expect(description).not.toMatch(/a11y/); + const productText = shape?.product?.description as string; + for (const value of values) expect(productText).toContain(`${value} (`); + + for (const handoff of [NEEDS_A11Y_SCAN_ID, NEEDS_A11Y_CONFIG_ID]) { + const named = handoff.match(/askBrowserStackAI with product "([^"]+)"/)?.[1]; + expect(named).toBeDefined(); + expect(values).toContain(named); + } }); it("relays an ask, approves it, and forwards the caller's own credentials", async () => {