Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/tools/ask-browserstack/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 4 additions & 5 deletions src/tools/ask-browserstack/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
7 changes: 4 additions & 3 deletions src/tools/tool-handoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
24 changes: 15 additions & 9 deletions tests/tools/askBrowserstackE2E.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading