Skip to content
92 changes: 92 additions & 0 deletions capability/tm.capability-index.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,98 @@
"tm": {
"routing_terms": ["team", "setting", "workspace", "suite"],
"summary": "BrowserStack Test Management: author and organise test cases, plan and track their execution, and report on the results. Everything is scoped to a project, which owns the folders, cases, runs, plans and reports beneath it. Search and filter cases; create and update cases, runs and plans; record results; bulk edit, move, copy, archive and delete; restructure folders; manage tags and custom-field definitions; assign, clone and close runs; review AI duplicate recommendations; read the activity feed and a case's version history; and create, schedule and download reports. Cases are authored and their outcomes recorded here, but tests are not RUN here: execution happens in your own runner or another BrowserStack product. Renaming and deleting a project are not exposed. A run is executed against a named configuration and environment, and records the app version and build its results came from. Destructive capabilities exist and always need explicit human approval.",
"agents": [
{
"name": "tm-generate-test-cases",
"title": "Test case generation",
"description": "Drafts a full set of new test cases from a requirement (a PRD, BRD, spec, ticket, acceptance criteria, Figma design or pasted text), applying coverage and priority rules, and returns them for review before anything is saved.",
"signals": [
"generate",
"draft",
"prd",
"brd",
"spec",
"specification",
"requirement",
"figma",
"document",
"acceptance criteria"
],
"intents": [
"generate new test cases for this requirement",
"write test cases from this document",
"create test cases in this empty folder from the PRD",
"review the attached BRD and create a complete set of QA test cases",
"create test cases for this feature from the attached requirement document",
"create test cases from the attached spec",
"draft QA test cases for the documented requirements",
"generate test cases from this Figma design",
"add more test cases to the ones you just generated"
]
},
{
"name": "tm-maintain-test-cases",
"title": "Test case maintenance",
"description": "Checks a project's existing test cases against a new or changed requirement, drafts edits to the ones it affects and lists what no case covers, for review before saving. The uncovered gaps go on to test case generation.",
"signals": [
"requirement",
"prd",
"spec",
"specification",
"reconcile",
"outdated",
"coverage"
],
"intents": [
"generate test cases for this requirement",
"generate and update test cases from this PRD",
"update these test cases for the new requirement",
"the spec changed, update the test cases in this folder",
"check these test cases against the updated PRD and fix what's wrong",
"reconcile this folder's test cases with the latest requirement"
]
},
{
"name": "tm-generate-test-data",
"title": "Test data generation",
"description": "Drafts datasets (named columns and rows) for data-driven testing from a requirement, random or derived from a stated rule, limit or boundary.",
"signals": [
"dataset",
"test data",
"boundary",
"data driven"
],
"intents": [
"generate test data for this requirement",
"generate test cases and test data for this requirement",
"create a dataset with valid and invalid values for this field",
"I need random test data for these columns",
"generate boundary values for this input",
"build a dataset for data-driven testing of this form",
"add more rows to that dataset"
]
},
{
"name": "tm-dedupe",
"title": "Test deduplication",
"description": "Finds exact and near-exact duplicate test cases in a folder, a list of cases or a small project (up to 100 cases) and saves them as suggestions to merge on the Review duplicates screen. Merges nothing itself.",
"signals": [
"duplicate",
"duplicates",
"duplicated",
"dedupe",
"deduplicate",
"deduplication"
],
"intents": [
"find duplicate test cases in this folder",
"dedupe the test cases in this project",
"are there duplicate test cases in this folder",
"check these test cases for duplicates",
"remove duplicate test cases from this folder"
]
}
],
"capabilities": [
{
"name": "assign_test_run_owner",
Expand Down
51 changes: 43 additions & 8 deletions src/tools/ask-browserstack/deferred.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,18 @@ import {
decisionUrl,
parseAsk,
} from "./stream.js";
import { RELAY_OFF_DETAILS, buildResult } from "./relay.js";
import { AgentRequest, AskResult, PermissionAsk, RelayMode } from "./types.js";
import {
RELAY_OFF_DETAILS,
buildResult,
readAppliedBeforeStop,
} from "./relay.js";
import {
AgentRequest,
ApprovalRecord,
AskResult,
PermissionAsk,
RelayMode,
} from "./types.js";

/** What a parked run needs the caller to send back. */
export interface ParkedAsk {
Expand Down Expand Up @@ -87,6 +97,10 @@ export function needsApprovalResult(
runId: string,
ask: ParkedAsk,
product: string,
// From the parked payload: an earlier approved step already landed in this run.
appliedBeforeStop = false,
// The decision this call delivered, when it was a resume that parked again.
delivered: ApprovalRecord[] = [],
): AskResult {
return {
// NOT ok, and not an error either: nothing was applied and nothing failed. A caller
Expand All @@ -97,19 +111,22 @@ export function needsApprovalResult(
// the one thing that must happen next is not something it can do by itself.
answer:
`A HUMAN MUST APPROVE THIS BEFORE IT CAN CONTINUE. BrowserStack AI stopped at: ` +
`${ask.description}. Nothing has been changed yet.\n\n` +
`${ask.description}. ` +
(appliedBeforeStop
? "Changes approved earlier in this task have already been made; this one has not.\n\n"
: "Nothing has been changed yet.\n\n") +
`Put this to the user and WAIT for their answer. If you have a way to ask them a ` +
`structured question or show a confirmation — any prompt, question or approval ` +
`affordance your client offers — use that; otherwise ask plainly in your reply. ` +
`Quote what is being done either way.\n\n` +
`Do NOT decide on their behalf, and do not treat "it is what they asked for" as ` +
`approval: they are being asked to confirm this specific change, which is a ` +
`different question. Once they have answered, follow \`next_step\`.`,
approvals: [],
approvals: delivered,
approvals_source: "atlas",
elicitations: [],
needs_approval: [{ description: ask.description, mode: ask.mode }],
applied_before_stop: false,
applied_before_stop: appliedBeforeStop,
permission_relay: {
used: true,
reason: "",
Expand Down Expand Up @@ -160,7 +177,12 @@ export async function runDeferred(
"askBrowserStackAI: run parked awaiting approval (run=%s)",
parked.runId,
);
return needsApprovalResult(parked.runId, parked.ask, product);
return needsApprovalResult(
parked.runId,
parked.ask,
product,
readAppliedBeforeStop(asRecord(response.body)) === true,
);
}
// No ask: an ordinary result, passed through the same builder the streaming path
// uses so an entitlement refusal, a 401 and a plain failure all read identically
Expand Down Expand Up @@ -197,6 +219,7 @@ export async function runResumed(
perm_id: permId,
decision,
reason: "",
contract: RELAY_CONTRACT,
// Required. The run resumes on a fresh Agent that must be scoped before it can be
// built, and Atlas deliberately will not take that scope from persisted state.
product,
Expand All @@ -212,6 +235,12 @@ export async function runResumed(
return decisionUnknown();
}

// This call's own decision: the trail of a two-call approval lives on the call that
// delivered it, since nothing is kept between calls.
const delivered: ApprovalRecord[] = [
{ description: `approval ${permId}`, decision, reason: "" },
];

if (response.status === 404) {
// The only failure worth its own words: there is no such parked run. It expired, it
// was already answered, or this is not the caller that owns it. None of those is an
Expand All @@ -232,11 +261,17 @@ export async function runResumed(
"askBrowserStackAI: run parked again after a decision (run=%s)",
runId,
);
return needsApprovalResult(parked.runId, parked.ask, product);
return needsApprovalResult(
parked.runId,
parked.ask,
product,
readAppliedBeforeStop(asRecord(response.body)) === true,
delivered,
);
}
// Finished. Through the SAME builder the start path uses, so an entitlement refusal,
// a 401 and a plain failure read identically whichever call produced them.
return buildResult(response, [], mode, product);
return buildResult(response, delivered, mode, product);
}

/** A resume that did not happen. Every field says "nothing changed", because in each
Expand Down
Loading
Loading