From 02bc35302e98fe1918045341a9003e583f9169f2 Mon Sep 17 00:00:00 2001 From: Erzhan Torokulov Date: Thu, 1 Oct 2026 11:56:25 +0000 Subject: [PATCH 1/2] feat(runner): tap, swipe and fill in runner actions sequences (AGE-5427) runner actions validated each step as a browser action, so a sequence refused tap, swipe and fill locally even though its description says it takes the same actions as runner act. api-contracts 0.74.0 widens the sequence contract to every runner action and adds the action-not-supported-on-browser step reason. bun.lock is left for after 0.74.0 publishes. --- .changeset/runner-actions-mobile-actions.md | 5 ++ package.json | 2 +- src/commands/runner/actions.register.ts | 6 +- .../interactiveRunner/browserAction.test.ts | 29 ------- src/core/interactiveRunner/browserAction.ts | 24 +----- .../performActions.touchscreen.test.ts | 86 +++++++++++++++++++ .../performActionsFailure.ts | 4 +- .../performActionsReasons.ts | 5 ++ src/domains/interactiveRunner/readActions.ts | 8 +- 9 files changed, 112 insertions(+), 57 deletions(-) create mode 100644 .changeset/runner-actions-mobile-actions.md create mode 100644 src/domains/interactiveRunner/performActions.touchscreen.test.ts diff --git a/.changeset/runner-actions-mobile-actions.md b/.changeset/runner-actions-mobile-actions.md new file mode 100644 index 000000000..cf687b2d7 --- /dev/null +++ b/.changeset/runner-actions-mobile-actions.md @@ -0,0 +1,5 @@ +--- +"@qawolf/cli": minor +--- + +`qawolf runner actions` takes the same actions as `qawolf runner act`, including the mobile `tap`, `swipe` and `fill`. A browser runner refuses a mobile action in a sequence with `action-not-supported-on-browser`; with `--continue-on-failure` the rest of the sequence still runs. Pins `@qawolf/api-contracts` 0.78.0. diff --git a/package.json b/package.json index 5c0fa9562..280d0ffa7 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@clack/prompts": "1.5.1", "@napi-rs/keyring": "1.3.0", "@oxc-node/core": "0.1.0", - "@qawolf/api-contracts": "0.77.0", + "@qawolf/api-contracts": "0.78.0", "@qawolf/emails": "1.1.1", "@qawolf/flow-targets": "1.0.0", "@qawolf/flows": "0.1.4", diff --git a/src/commands/runner/actions.register.ts b/src/commands/runner/actions.register.ts index 54c6759b6..63f347b35 100644 --- a/src/commands/runner/actions.register.ts +++ b/src/commands/runner/actions.register.ts @@ -13,7 +13,11 @@ Examples: $ qawolf runner actions '[{"type":"click","button":"left","x":480,"y":260},{"type":"type","text":"hello@example.com"},{"type":"keypress","keys":["Enter"]}]' --screenshot after-login.jpg $ echo '[{"type":"click","button":"left","x":1,"y":2},{"type":"type","text":"hi"}]' | qawolf runner actions - $ qawolf runner actions '[{"type":"click","button":"left","x":480,"y":260},{"type":"type","text":"hello"}]' --screenshot-mode each --screenshot step.jpg - $ qawolf runner actions '[{"type":"scroll","x":480,"y":260,"scroll_x":0,"scroll_y":600},{"type":"click","button":"left","x":120,"y":700}]' --continue-on-failure`; + $ qawolf runner actions '[{"type":"scroll","x":480,"y":260,"scroll_x":0,"scroll_y":600},{"type":"click","button":"left","x":120,"y":700}]' --continue-on-failure + +Mobile: + $ qawolf runner actions '[{"type":"tap","selector":"//*[@content-desc=\\"Email\\"]"},{"type":"type","text":"hello@example.com"},{"type":"tap","x":540,"y":1650}]' --screenshot after-login.jpg + $ qawolf runner actions '[{"type":"fill","selector":"name == \\"Postal code\\"","strategy":"ios-predicate","text":"94107"},{"type":"swipe","from":{"x":540,"y":1600},"to":{"x":540,"y":600}}]'`; type ActionsFlags = { continueOnFailure?: boolean; diff --git a/src/core/interactiveRunner/browserAction.test.ts b/src/core/interactiveRunner/browserAction.test.ts index e0aed041a..11e5c80dd 100644 --- a/src/core/interactiveRunner/browserAction.test.ts +++ b/src/core/interactiveRunner/browserAction.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it } from "bun:test"; import { type ActionFlags, buildRunnerAction, - parseBrowserAction, parseRunnerAction, } from "./browserAction.js"; @@ -219,31 +218,3 @@ describe("parseRunnerAction", () => { ).toBe(true); }); }); - -describe("parseBrowserAction", () => { - it("refuses a touchscreen action, which a sequence does not take", () => { - expect(parseBrowserAction({ type: "tap", x: 1, y: 2 }).ok).toBe(false); - }); - - it("takes a complete action as a model emitted it", () => { - const parsed = parseBrowserAction({ - button: "left", - type: "click", - x: 1, - y: 2, - }); - - expect(parsed.ok).toBe(true); - if (!parsed.ok) return; - expect(parsed.action).toEqual({ - button: "left", - type: "click", - x: 1, - y: 2, - }); - }); - - it("refuses one the published schema does not admit", () => { - expect(parseBrowserAction({ type: "click" }).ok).toBe(false); - }); -}); diff --git a/src/core/interactiveRunner/browserAction.ts b/src/core/interactiveRunner/browserAction.ts index a18335870..faa5cb947 100644 --- a/src/core/interactiveRunner/browserAction.ts +++ b/src/core/interactiveRunner/browserAction.ts @@ -1,7 +1,5 @@ import { - type BrowserAction, type RunnerAction, - browserActionSchema, runnerActionSchema, } from "@qawolf/api-contracts/v1"; import { z } from "zod"; @@ -104,27 +102,13 @@ export function buildRunnerAction( return parseRunnerAction(candidate); } -function parseWith( - schema: z.ZodType, +/** Puts one complete action, as a caller's model emitted it, to the schema `runner act` and `runner actions` send. */ +export function parseRunnerAction( candidate: unknown, -): BuiltAction { - const parsed = schema.safeParse(candidate); +): BuiltAction { + const parsed = runnerActionSchema.safeParse(candidate); if (!parsed.success) { return { error: z.prettifyError(parsed.error), ok: false }; } return { action: parsed.data, ok: true }; } - -/** Puts one complete action, as a caller's model emitted it, to the schema `runner act` sends. */ -export function parseRunnerAction( - candidate: unknown, -): BuiltAction { - return parseWith(runnerActionSchema, candidate); -} - -/** Puts one action of a `runner actions` sequence, which takes browser actions only, to its schema. */ -export function parseBrowserAction( - candidate: unknown, -): BuiltAction { - return parseWith(browserActionSchema, candidate); -} diff --git a/src/domains/interactiveRunner/performActions.touchscreen.test.ts b/src/domains/interactiveRunner/performActions.touchscreen.test.ts new file mode 100644 index 000000000..86da02aca --- /dev/null +++ b/src/domains/interactiveRunner/performActions.touchscreen.test.ts @@ -0,0 +1,86 @@ +import { publicContractsV1 } from "@qawolf/api-contracts/v1"; +import { describe, expect, it } from "bun:test"; + +import { makeAuthCtx, makeTestDeps } from "./deps.testUtils.js"; +import { handleRunnerActions } from "./performActions.js"; +import { + actionsOptions, + performed, + sequenceAnswer, + someTyping, +} from "./performActions.fixtures.js"; + +const sequenceTimeoutMs = 180_000; + +describe("handleRunnerActions on a touchscreen", () => { + it("sends a touchscreen sequence, as runner act takes it", async () => { + const { callPublicApi, ctx } = makeAuthCtx(); + callPublicApi.mockResolvedValue({ + ok: true, + value: sequenceAnswer({ + lastCompletedIndex: 2, + outcome: "success", + results: [performed(0), performed(1), performed(2)], + }), + }); + const touchscreenSequence = [ + { selector: "//*[@content-desc='Email']", type: "tap" }, + { selector: "~Postal code", text: "94107", type: "fill" }, + { from: { x: 540, y: 1600 }, to: { x: 540, y: 600 }, type: "swipe" }, + ]; + + const result = await handleRunnerActions( + ctx, + actionsOptions({ actions: JSON.stringify(touchscreenSequence) }), + makeTestDeps(), + ); + + expect(result).toBeUndefined(); + expect(callPublicApi).toHaveBeenCalledWith( + publicContractsV1.runner.performActions, + { + actions: touchscreenSequence, + id: "ci", + screenshotMode: "none", + stopOnFailure: true, + }, + { timeoutMs: sequenceTimeoutMs }, + ); + }); + + it("names a touchscreen action a browser runner refused, as a bad argument", async () => { + const { callPublicApi, ctx } = makeAuthCtx(); + callPublicApi.mockResolvedValue({ + ok: true, + value: sequenceAnswer({ + failedIndex: 0, + failureReason: "action-not-supported-on-browser", + lastCompletedIndex: 1, + outcome: "failure", + results: [ + { + effect: "not-performed", + failureReason: "action-not-supported-on-browser", + index: 0, + outcome: "failure", + }, + performed(1), + ], + stoppedEarly: false, + }), + }); + + const result = await handleRunnerActions( + ctx, + actionsOptions({ + actions: JSON.stringify([{ type: "tap", x: 1, y: 2 }, someTyping]), + continueOnFailure: true, + }), + makeTestDeps(), + ); + + expect(result?.exitCode).toBe(2); + expect(result?.error).toContain("tap"); + expect(result?.error).toContain("only a mobile runner performs"); + }); +}); diff --git a/src/domains/interactiveRunner/performActionsFailure.ts b/src/domains/interactiveRunner/performActionsFailure.ts index 4dda7679f..ad92f2e2d 100644 --- a/src/domains/interactiveRunner/performActionsFailure.ts +++ b/src/domains/interactiveRunner/performActionsFailure.ts @@ -1,4 +1,4 @@ -import type { BrowserAction } from "@qawolf/api-contracts/v1"; +import type { RunnerAction } from "@qawolf/api-contracts/v1"; import { interactiveRunnerMessages } from "~/core/messages/index.js"; import { appendSentence } from "~/core/sentences.js"; @@ -13,7 +13,7 @@ import { /** Every action that did not succeed, why, and what the caller should do about it. */ export function describeSequenceFailure(options: { - actions: BrowserAction[]; + actions: RunnerAction[]; answer: SequenceFailure; }): Exclude { const { actions, answer } = options; diff --git a/src/domains/interactiveRunner/performActionsReasons.ts b/src/domains/interactiveRunner/performActionsReasons.ts index 57d6c4466..a540110be 100644 --- a/src/domains/interactiveRunner/performActionsReasons.ts +++ b/src/domains/interactiveRunner/performActionsReasons.ts @@ -80,6 +80,11 @@ export function describeReason(step: FailedStep): Described { exitCode: exitCodes.invalidArgs, why: "has no touchscreen equivalent on a mobile runner.", }; + case "action-not-supported-on-browser": + return { + exitCode: exitCodes.invalidArgs, + why: "is a touchscreen action, which only a mobile runner performs.", + }; case "screen-needs-a-run": return { exitCode: exitCodes.invalidArgs, diff --git a/src/domains/interactiveRunner/readActions.ts b/src/domains/interactiveRunner/readActions.ts index e98e78455..e06da5a22 100644 --- a/src/domains/interactiveRunner/readActions.ts +++ b/src/domains/interactiveRunner/readActions.ts @@ -1,7 +1,7 @@ -import type { BrowserAction, ScreenshotMode } from "@qawolf/api-contracts/v1"; +import type { RunnerAction, ScreenshotMode } from "@qawolf/api-contracts/v1"; import { maxActionsPerRequest } from "@qawolf/api-contracts/v1"; -import { parseBrowserAction } from "~/core/interactiveRunner/browserAction.js"; +import { parseRunnerAction } from "~/core/interactiveRunner/browserAction.js"; import { interactiveRunnerMessages } from "~/core/messages/index.js"; import type { AuthCommandContext, @@ -16,7 +16,7 @@ import type { InteractiveRunnerDeps } from "./deps.js"; const stdinArgument = "-"; export type ReadActions = - | { actions: BrowserAction[]; ok: true } + | { actions: RunnerAction[]; ok: true } | { error: string; ok: false }; /** @@ -48,7 +48,7 @@ export async function readActions( }; } - const built = parsed.items.map(parseBrowserAction); + const built = parsed.items.map(parseRunnerAction); const firstRefused = built.findIndex((action) => !action.ok); const refused = built[firstRefused]; if (refused !== undefined && !refused.ok) { From 3e6b6eba0777244faf977f47f2459a8deee15c4a Mon Sep 17 00:00:00 2001 From: Erzhan Torokulov Date: Mon, 5 Oct 2026 17:35:01 +0000 Subject: [PATCH 2/2] build(runner): pin @qawolf/api-contracts 0.81.0 (AGE-5427) 0.81.0 is the first published version with both the runner actions sequence contract and the screenshot baseline commands 0.77.0 carried. It also makes every contract carry annotationJustifications and adds a backlog issue status, so the stand-in contracts in tests carry justifications and the issue.update flag test lists backlog. --- .changeset/runner-actions-mobile-actions.md | 2 +- bun.lock | 4 ++-- package.json | 2 +- skills/qawolf-cli/references/run-results.md | 1 + src/commands/publicApi/index.environment.test.ts | 3 +++ src/commands/publicApi/index.nested.test.ts | 2 ++ src/commands/publicApi/index.test.ts | 4 ++++ src/commands/skillRunResults.test.ts | 3 +++ src/core/publicApi/flagSpecs.test.ts | 2 +- src/domains/publicApi/commandSpecs.test.ts | 5 +++++ src/domains/publicApi/contract.fixtures.ts | 9 +++++++++ src/domains/publicApi/handle.testUtils.ts | 2 ++ 12 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 src/domains/publicApi/contract.fixtures.ts diff --git a/.changeset/runner-actions-mobile-actions.md b/.changeset/runner-actions-mobile-actions.md index cf687b2d7..233216f54 100644 --- a/.changeset/runner-actions-mobile-actions.md +++ b/.changeset/runner-actions-mobile-actions.md @@ -2,4 +2,4 @@ "@qawolf/cli": minor --- -`qawolf runner actions` takes the same actions as `qawolf runner act`, including the mobile `tap`, `swipe` and `fill`. A browser runner refuses a mobile action in a sequence with `action-not-supported-on-browser`; with `--continue-on-failure` the rest of the sequence still runs. Pins `@qawolf/api-contracts` 0.78.0. +`qawolf runner actions` takes the same actions as `qawolf runner act`, including the mobile `tap`, `swipe` and `fill`. A browser runner refuses a mobile action in a sequence with `action-not-supported-on-browser`; with `--continue-on-failure` the rest of the sequence still runs. Pins `@qawolf/api-contracts` 0.81.0. diff --git a/bun.lock b/bun.lock index 23af5af44..efe64d03e 100644 --- a/bun.lock +++ b/bun.lock @@ -8,7 +8,7 @@ "@clack/prompts": "1.5.1", "@napi-rs/keyring": "1.3.0", "@oxc-node/core": "0.1.0", - "@qawolf/api-contracts": "0.77.0", + "@qawolf/api-contracts": "0.81.0", "@qawolf/emails": "1.1.1", "@qawolf/flow-targets": "1.0.0", "@qawolf/flows": "0.1.4", @@ -462,7 +462,7 @@ "@puppeteer/browsers": ["@puppeteer/browsers@2.13.2", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw=="], - "@qawolf/api-contracts": ["@qawolf/api-contracts@0.77.0", "", { "dependencies": { "tslib": "^2.5.3" }, "peerDependencies": { "zod": "^4.1.11" } }, "sha512-i4bsVCNuaUZKvmP8WvNM6JrHiCPv/tiFgWYUZK6z9ChuEdHmHE+7tt3q9s8Ix+JHCNq5Df8miUzy5/C3K4xztg=="], + "@qawolf/api-contracts": ["@qawolf/api-contracts@0.81.0", "", { "dependencies": { "tslib": "^2.5.3" }, "peerDependencies": { "zod": "^4.1.11" } }, "sha512-qER3FCKt2G9R739rG/K1DH/LbZCJP+qU/4NMQWDVtDTIcb9vNKxkBy+FB+Z7Ls7fzuS3hIWh0qRVU6/FflsBEg=="], "@qawolf/emailer-types": ["@qawolf/emailer-types@1.1.0", "", { "dependencies": { "email-addresses": "^5.0.0", "tslib": "^2.5.3", "zod": "^4.1.11", "zod-form-data": "^3.0.1" } }, "sha512-7cX+e31L9dpO8fntQ2cfSMt/1Lla/yUjaLUr/KLpXuscBSajy7CgNzjAGURa1KsCcxz7n+9PGQw6cM3RlC1bSg=="], diff --git a/package.json b/package.json index 280d0ffa7..a59875ef6 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@clack/prompts": "1.5.1", "@napi-rs/keyring": "1.3.0", "@oxc-node/core": "0.1.0", - "@qawolf/api-contracts": "0.78.0", + "@qawolf/api-contracts": "0.81.0", "@qawolf/emails": "1.1.1", "@qawolf/flow-targets": "1.0.0", "@qawolf/flows": "0.1.4", diff --git a/skills/qawolf-cli/references/run-results.md b/skills/qawolf-cli/references/run-results.md index 297349ca0..cce017a3d 100644 --- a/skills/qawolf-cli/references/run-results.md +++ b/skills/qawolf-cli/references/run-results.md @@ -201,6 +201,7 @@ Every documented field of the `run.get` response. `[]` marks an array, so - `flows[].attempts[].kind` — One of: automated, manual - `flows[].attempts[].startedAt` — Absent when the attempt failed before it could start. - `flows[].attempts[].status` — One of: passed, failed, canceled +- `flows[].attempts[].canceledReason` — Why the attempt was canceled. userCanceled means a person stopped it; do not immediately retry that activity. - `flows[].failure.diagnosis` — QA Wolf's investigation verdict for the failure: `bug` means the application is broken, `maintenance` means the test needed an update and the failure does not indicate an application problem. Absent until the investigation reaches a verdict, and absent when the flow was opted out of investigation. Pass issueId to issue.get for details. - `flows[].failure.diagnosis.issueId` — The id of the issue. - `flows[].failure.diagnosis.type` — One of: bug, maintenance diff --git a/src/commands/publicApi/index.environment.test.ts b/src/commands/publicApi/index.environment.test.ts index ec7bae54a..a7610c16e 100644 --- a/src/commands/publicApi/index.environment.test.ts +++ b/src/commands/publicApi/index.environment.test.ts @@ -10,6 +10,7 @@ import { import { createSignalRegistry } from "~/shell/signals/createSignalRegistry.js"; import { registerPublicApiCommands } from "./index.js"; +import { standInAnnotationJustifications } from "~/domains/publicApi/contract.fixtures.js"; const originalAiTaskId = process.env["QAWOLF_AI_TASK_ID"]; const originalChatSessionId = process.env["QAWOLF_CHAT_SESSION_ID"]; @@ -45,6 +46,7 @@ function makeNotificationContract() { openWorldHint: true, readOnlyHint: false, }, + annotationJustifications: standInAnnotationJustifications, description: "Create a run.", input: z.object({ aiTaskId: z.string().optional(), @@ -86,6 +88,7 @@ it("defaults public API options from their environment variables", async () => { openWorldHint: true, readOnlyHint: false, }, + annotationJustifications: standInAnnotationJustifications, description: "Create a run.", input: z.object({ aiTaskId: z.string().optional(), diff --git a/src/commands/publicApi/index.nested.test.ts b/src/commands/publicApi/index.nested.test.ts index 64029a0cd..79a45e4b5 100644 --- a/src/commands/publicApi/index.nested.test.ts +++ b/src/commands/publicApi/index.nested.test.ts @@ -9,6 +9,7 @@ import { import { createSignalRegistry } from "~/shell/signals/createSignalRegistry.js"; import { registerPublicApiCommands } from "./index.js"; +import { standInAnnotationJustifications } from "~/domains/publicApi/contract.fixtures.js"; // `process.exitCode` is global state and persists across tests; reset to 0 // (not undefined — bun ignores an undefined assignment) so leftover state @@ -33,6 +34,7 @@ const contract = { openWorldHint: false, readOnlyHint: false, }, + annotationJustifications: standInAnnotationJustifications, name: "fake.reportStatus", kind: "write" as const, description: "Synthetic nested contract.", diff --git a/src/commands/publicApi/index.test.ts b/src/commands/publicApi/index.test.ts index 9f41267bb..cf3be453f 100644 --- a/src/commands/publicApi/index.test.ts +++ b/src/commands/publicApi/index.test.ts @@ -10,6 +10,7 @@ import { import { createSignalRegistry } from "~/shell/signals/createSignalRegistry.js"; import { registerPublicApiCommands } from "./index.js"; +import { standInAnnotationJustifications } from "~/domains/publicApi/contract.fixtures.js"; afterEach(() => { mock.restore(); @@ -162,6 +163,7 @@ describe("registerPublicApiCommands", () => { openWorldHint: false, readOnlyHint: true, }, + annotationJustifications: standInAnnotationJustifications, description: "List the flows of an environment.", input: z.object({ environmentId: z.string() }), kind: "read", @@ -174,6 +176,7 @@ describe("registerPublicApiCommands", () => { openWorldHint: false, readOnlyHint: true, }, + annotationJustifications: standInAnnotationJustifications, description: "Look up a run.", input: z.object({ runId: z.string() }), kind: "read", @@ -215,6 +218,7 @@ describe("registerPublicApiCommands", () => { openWorldHint: false, readOnlyHint: true, }, + annotationJustifications: standInAnnotationJustifications, description: "Look up a run attempt.", input: z.object({ runId: z.string() }), kind: "read", diff --git a/src/commands/skillRunResults.test.ts b/src/commands/skillRunResults.test.ts index ca15b5240..ef1661a05 100644 --- a/src/commands/skillRunResults.test.ts +++ b/src/commands/skillRunResults.test.ts @@ -7,6 +7,7 @@ import { renderResponseFields, spliceResponseFields, } from "./skillRunResults.js"; +import { standInAnnotationJustifications } from "~/domains/publicApi/contract.fixtures.js"; const runResultsMdPath = join( import.meta.dirname, @@ -25,6 +26,7 @@ describe("renderResponseFields", () => { openWorldHint: false, readOnlyHint: true, }, + annotationJustifications: standInAnnotationJustifications, description: "Look up a run.", input: z.object({ runId: z.string() }), kind: "read", @@ -47,6 +49,7 @@ describe("renderResponseFields", () => { openWorldHint: false, readOnlyHint: true, }, + annotationJustifications: standInAnnotationJustifications, description: "Look up a run.", input: z.object({ runId: z.string() }), kind: "read", diff --git a/src/core/publicApi/flagSpecs.test.ts b/src/core/publicApi/flagSpecs.test.ts index c184a7f58..04516ad29 100644 --- a/src/core/publicApi/flagSpecs.test.ts +++ b/src/core/publicApi/flagSpecs.test.ts @@ -181,7 +181,7 @@ describe("buildFlagSpecs", () => { "One of: unprioritized, low, medium, high, urgent", ); expect(described("status")).toBe( - "One of: pending, inProgress, paused, resolved, canceled, archived", + "One of: backlog, pending, inProgress, paused, resolved, canceled, archived", ); }); diff --git a/src/domains/publicApi/commandSpecs.test.ts b/src/domains/publicApi/commandSpecs.test.ts index a69d997a1..847581510 100644 --- a/src/domains/publicApi/commandSpecs.test.ts +++ b/src/domains/publicApi/commandSpecs.test.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { buildCommandSpecs } from "./commandSpecs.js"; import { skippedContractNames } from "./skippedContracts.js"; +import { standInAnnotationJustifications } from "./contract.fixtures.js"; describe("buildCommandSpecs", () => { it("flattens the published contract tree into command specs", () => { @@ -44,6 +45,7 @@ describe("buildCommandSpecs", () => { openWorldHint: false, readOnlyHint: true, }, + annotationJustifications: standInAnnotationJustifications, description: "Hand-written elsewhere", input: z.object({ config: z.object({ counts: z.array(z.number()) }) }), kind: "read", @@ -56,6 +58,7 @@ describe("buildCommandSpecs", () => { openWorldHint: false, readOnlyHint: true, }, + annotationJustifications: standInAnnotationJustifications, description: "Generated", input: z.object({ runId: z.string() }), kind: "read", @@ -78,6 +81,7 @@ describe("buildCommandSpecs", () => { openWorldHint: false, readOnlyHint: true, }, + annotationJustifications: standInAnnotationJustifications, description: "Mismatched", input: z.object({}), kind: "read", @@ -97,6 +101,7 @@ describe("buildCommandSpecs", () => { openWorldHint: false, readOnlyHint: true, }, + annotationJustifications: standInAnnotationJustifications, description: "Unmappable", input: z.object({ config: z.object({ counts: z.array(z.number()) }) }), kind: "read", diff --git a/src/domains/publicApi/contract.fixtures.ts b/src/domains/publicApi/contract.fixtures.ts new file mode 100644 index 000000000..08420ce78 --- /dev/null +++ b/src/domains/publicApi/contract.fixtures.ts @@ -0,0 +1,9 @@ +import type { AnyPublicApiContract } from "@qawolf/api-contracts/v1"; + +/** Every published contract justifies its hints; a stand-in contract only has to carry them. */ +export const standInAnnotationJustifications: AnyPublicApiContract["annotationJustifications"] = + { + destructiveHint: "Stand-in contract for a test.", + openWorldHint: "Stand-in contract for a test.", + readOnlyHint: "Stand-in contract for a test.", + }; diff --git a/src/domains/publicApi/handle.testUtils.ts b/src/domains/publicApi/handle.testUtils.ts index d3a929319..48a809b3c 100644 --- a/src/domains/publicApi/handle.testUtils.ts +++ b/src/domains/publicApi/handle.testUtils.ts @@ -7,6 +7,7 @@ import type { PlatformClient } from "~/shell/platform/createPlatformClient.js"; import type { UI } from "~/shell/ui/index.js"; import { buildCommandSpecs, type CommandSpec } from "./commandSpecs.js"; +import { standInAnnotationJustifications } from "./contract.fixtures.js"; export const runCreateSpec = (): CommandSpec => { const spec = buildCommandSpecs({ @@ -25,6 +26,7 @@ export const countSpec = (): CommandSpec => { openWorldHint: false, readOnlyHint: false, }, + annotationJustifications: standInAnnotationJustifications, name: "fake.count", kind: "write" as const, description: "synthetic number-flag contract",