@qawolf/cli 1.33.0 → 1.34.0
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.
- package/README.md +25 -1
- package/dist/cli.js +212 -5
- package/dist/runner-sdk.js +60 -4
- package/dist/types/runnerSdk/index.d.ts +3 -1
- package/dist/types/runnerSdk/types.d.ts +7 -1
- package/package.json +1 -1
- package/skills/qawolf-cli/SKILL.md +7 -1
- package/skills/qawolf-cli/references/runner.md +38 -0
package/README.md
CHANGED
|
@@ -113,7 +113,31 @@ const submitted = await runner.run({
|
|
|
113
113
|
if (submitted.ok) console.log(submitted.value.runId, submitted.value.fileSync);
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
`ok: false`
|
|
116
|
+
`ok: false` reports a request failure, including transport, auth, or local validation. A runner that refused the request comes back as `ok: true` with an outcome and `failureReason` from the published contract, so a caller can switch on it exhaustively. Most verbs expose `value.outcome`; recording controls expose `value.result.outcome` and a history `value.url`.
|
|
117
|
+
|
|
118
|
+
Control video recording on a Playwright runner after a flow has started its screen:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const recordingId = crypto.randomUUID();
|
|
122
|
+
const recording = await runner.record({
|
|
123
|
+
runnerId: "agent-1",
|
|
124
|
+
command: { action: "start", recordingId },
|
|
125
|
+
});
|
|
126
|
+
if (recording.ok && recording.value.result.outcome === "success") {
|
|
127
|
+
// Drive the browser, then stop this specific capture.
|
|
128
|
+
await runner.record({
|
|
129
|
+
runnerId: "agent-1",
|
|
130
|
+
command: { action: "stop", recordingId },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// History remains readable after the runner terminates.
|
|
135
|
+
const history = await runner.listRecordings({ runnerId: "agent-1" });
|
|
136
|
+
if (history.ok)
|
|
137
|
+
console.log(history.value.recordings, history.value.nextPageToken);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`record` also accepts `{ action: "status" }` and `{ action: "auto", enabled: true }` (or `false`). Keep the UUID when retrying start or stop. `listRecordings` accepts optional `recordingId` and `pageToken` filters and returns one page, with stable platform URLs and expiring video URLs. The equivalent CLI commands are `qawolf runner record start|stop|status|auto` and `qawolf runner list-recordings`; see the [runner reference](skills/qawolf-cli/references/runner.md#video-recording).
|
|
117
141
|
|
|
118
142
|
## Reference
|
|
119
143
|
|
package/dist/cli.js
CHANGED
|
@@ -15210,9 +15210,10 @@ function createLoggingSystem(opts) {
|
|
|
15210
15210
|
}
|
|
15211
15211
|
|
|
15212
15212
|
// src/core/publicApi/notFoundSubject.ts
|
|
15213
|
-
var
|
|
15213
|
+
var runnerRoutesWithoutLivePod = new Set([
|
|
15214
15214
|
"runner.launch",
|
|
15215
|
-
"runner.list"
|
|
15215
|
+
"runner.list",
|
|
15216
|
+
"runner.recordings"
|
|
15216
15217
|
]);
|
|
15217
15218
|
var runRoutesThatResolveOneRun = new Set([
|
|
15218
15219
|
"run.diagnose",
|
|
@@ -15234,7 +15235,7 @@ function field(input, name) {
|
|
|
15234
15235
|
return typeof value === "string" ? value : undefined;
|
|
15235
15236
|
}
|
|
15236
15237
|
function notFoundSubject(contractName, input) {
|
|
15237
|
-
if (contractName.startsWith("runner.") && !
|
|
15238
|
+
if (contractName.startsWith("runner.") && !runnerRoutesWithoutLivePod.has(contractName)) {
|
|
15238
15239
|
return { kind: "runner", runnerId: field(input, "id") };
|
|
15239
15240
|
}
|
|
15240
15241
|
if (runRoutesThatResolveOneRun.has(contractName)) {
|
|
@@ -22389,7 +22390,7 @@ function startUpdateCheck(deps) {
|
|
|
22389
22390
|
// package.json
|
|
22390
22391
|
var package_default = {
|
|
22391
22392
|
name: "@qawolf/cli",
|
|
22392
|
-
version: "1.
|
|
22393
|
+
version: "1.34.0",
|
|
22393
22394
|
description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
22394
22395
|
keywords: [
|
|
22395
22396
|
"automation",
|
|
@@ -36271,6 +36272,9 @@ async function handleRunnerActions(ctx, options, deps) {
|
|
|
36271
36272
|
return framesNote === undefined ? failure : { ...failure, error: appendSentence(failure.error, framesNote) };
|
|
36272
36273
|
}
|
|
36273
36274
|
|
|
36275
|
+
// src/domains/interactiveRunner/deps.ts
|
|
36276
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
36277
|
+
|
|
36274
36278
|
// src/shell/interactiveRunner/collectRunFiles.ts
|
|
36275
36279
|
import { join as join53 } from "node:path";
|
|
36276
36280
|
|
|
@@ -36538,6 +36542,7 @@ function makeInteractiveRunnerDeps(options) {
|
|
|
36538
36542
|
cwd: options.cwd,
|
|
36539
36543
|
env: options.env,
|
|
36540
36544
|
makeRunnerId,
|
|
36545
|
+
makeRecordingId: randomUUID3,
|
|
36541
36546
|
readFile: (path) => options.fs.readFile(path),
|
|
36542
36547
|
readStdin: readStdin2,
|
|
36543
36548
|
runFilesManifest: makeRunFilesManifestStore({
|
|
@@ -38415,6 +38420,207 @@ function registerRunCommand(runner, signals) {
|
|
|
38415
38420
|
}, runnerDeps(ctx)))(opts, command));
|
|
38416
38421
|
}
|
|
38417
38422
|
|
|
38423
|
+
// src/core/messages/interactiveRunner/recording.ts
|
|
38424
|
+
var recordingMessages = {
|
|
38425
|
+
noRunnerIdForHistory: "Name the runner whose recordings to read with --runner <id> or QAWOLF_RUNNER_ID. History stays readable after the runner terminates.",
|
|
38426
|
+
unsupported: "Video recording requires a Playwright runner.",
|
|
38427
|
+
inProgress: "A recording is already in progress. Use runner record status to check its mode and id. Stop a manual recording by id, or wait for the run to finish an automatic recording.",
|
|
38428
|
+
notFound: "That recording was not found on the runner. Use runner record status for the active recording or runner list-recordings for published history.",
|
|
38429
|
+
idUsed: "That recording id has already been used. Start a new recording with a new UUID.",
|
|
38430
|
+
failed: "The recording operation failed. If this was a stop, retry it with the same recording id to finish stopping or publishing it. Check runner list-recordings for its published status.",
|
|
38431
|
+
failedCapture: "The recording failed. Check runner list-recordings for its published status.",
|
|
38432
|
+
recordingId: (id) => `Recording id: ${id}.`
|
|
38433
|
+
};
|
|
38434
|
+
|
|
38435
|
+
// src/domains/interactiveRunner/recordingOutput.ts
|
|
38436
|
+
function recordingFailure(reason) {
|
|
38437
|
+
switch (reason) {
|
|
38438
|
+
case "unsupported":
|
|
38439
|
+
return {
|
|
38440
|
+
error: recordingMessages.unsupported,
|
|
38441
|
+
exitCode: exitCodes.invalidArgs
|
|
38442
|
+
};
|
|
38443
|
+
case "screen-not-ready":
|
|
38444
|
+
return {
|
|
38445
|
+
error: interactiveRunnerMessages.screenNotReady,
|
|
38446
|
+
exitCode: exitCodes.network
|
|
38447
|
+
};
|
|
38448
|
+
case "recording-in-progress":
|
|
38449
|
+
return {
|
|
38450
|
+
error: recordingMessages.inProgress,
|
|
38451
|
+
exitCode: exitCodes.invalidArgs
|
|
38452
|
+
};
|
|
38453
|
+
case "recording-not-found":
|
|
38454
|
+
return {
|
|
38455
|
+
error: recordingMessages.notFound,
|
|
38456
|
+
exitCode: exitCodes.notFound
|
|
38457
|
+
};
|
|
38458
|
+
case "recording-id-used":
|
|
38459
|
+
return {
|
|
38460
|
+
error: recordingMessages.idUsed,
|
|
38461
|
+
exitCode: exitCodes.invalidArgs
|
|
38462
|
+
};
|
|
38463
|
+
case "runner-unreachable":
|
|
38464
|
+
return {
|
|
38465
|
+
error: interactiveRunnerMessages.runnerUnreachable,
|
|
38466
|
+
exitCode: exitCodes.network
|
|
38467
|
+
};
|
|
38468
|
+
case "recording-failed":
|
|
38469
|
+
return {
|
|
38470
|
+
error: recordingMessages.failed,
|
|
38471
|
+
exitCode: exitCodes.testFailure
|
|
38472
|
+
};
|
|
38473
|
+
}
|
|
38474
|
+
}
|
|
38475
|
+
function formatRecordingState(answer) {
|
|
38476
|
+
const { result, url } = answer;
|
|
38477
|
+
if (result.outcome === "failure")
|
|
38478
|
+
return recordingFailure(result.failureReason).error;
|
|
38479
|
+
const { active, auto } = result.state;
|
|
38480
|
+
const lines = [
|
|
38481
|
+
active ? `Recording ${active.id} (${active.mode}), started ${active.startedAt}.` : "No active recording.",
|
|
38482
|
+
`Automatic recording: ${auto}.`
|
|
38483
|
+
];
|
|
38484
|
+
if (result.recording) {
|
|
38485
|
+
lines.push(`Recording ${result.recording.id}: ${result.recording.status}.`);
|
|
38486
|
+
}
|
|
38487
|
+
return [...lines, url].join(`
|
|
38488
|
+
`);
|
|
38489
|
+
}
|
|
38490
|
+
function formatRecordings(answer) {
|
|
38491
|
+
const lines = answer.recordings.map((recording) => [
|
|
38492
|
+
`${recording.id} (${recording.mode}, ${recording.status})`,
|
|
38493
|
+
` ${recording.startedAt} to ${recording.endedAt}`,
|
|
38494
|
+
` ${recording.url}`,
|
|
38495
|
+
...recording.videoUrl ? [` Video: ${recording.videoUrl}`] : []
|
|
38496
|
+
].join(`
|
|
38497
|
+
`));
|
|
38498
|
+
if (lines.length === 0)
|
|
38499
|
+
lines.push("No published recordings found.");
|
|
38500
|
+
if (answer.nextPageToken !== undefined) {
|
|
38501
|
+
lines.push(`Next page token (pass with --page-token): ${answer.nextPageToken}`);
|
|
38502
|
+
}
|
|
38503
|
+
return lines.join(`
|
|
38504
|
+
`);
|
|
38505
|
+
}
|
|
38506
|
+
|
|
38507
|
+
// src/domains/interactiveRunner/recordingRequests.ts
|
|
38508
|
+
async function recordOnRunner(ctx, runner, command) {
|
|
38509
|
+
const contract = publicContractsV1.runner.record;
|
|
38510
|
+
const parsed = contract.input.safeParse({ id: runner.runnerId, command });
|
|
38511
|
+
if (!parsed.success) {
|
|
38512
|
+
return {
|
|
38513
|
+
ok: false,
|
|
38514
|
+
error: "Invalid recording request. Runner ids must be valid and recording ids must be UUIDs.",
|
|
38515
|
+
exitCode: exitCodes.invalidArgs
|
|
38516
|
+
};
|
|
38517
|
+
}
|
|
38518
|
+
const result = await ctx.platformClient.callPublicApi(contract, parsed.data, runnerCallOptions);
|
|
38519
|
+
return result.ok ? result : { ok: false, ...runnerRequestFailure(result, runner) };
|
|
38520
|
+
}
|
|
38521
|
+
async function readRunnerRecordings(ctx, runner, query) {
|
|
38522
|
+
const contract = publicContractsV1.runner.recordings;
|
|
38523
|
+
const parsed = contract.input.safeParse({ id: runner.runnerId, ...query });
|
|
38524
|
+
if (!parsed.success) {
|
|
38525
|
+
return {
|
|
38526
|
+
ok: false,
|
|
38527
|
+
error: "Invalid recordings request. Use a valid runner id, a UUID recording id, and a page token of at most 4096 characters.",
|
|
38528
|
+
exitCode: exitCodes.invalidArgs
|
|
38529
|
+
};
|
|
38530
|
+
}
|
|
38531
|
+
const result = await ctx.platformClient.callPublicApi(contract, parsed.data);
|
|
38532
|
+
return result.ok ? result : { ok: false, ...runnerRequestFailure(result, runner) };
|
|
38533
|
+
}
|
|
38534
|
+
|
|
38535
|
+
// src/domains/interactiveRunner/recording.ts
|
|
38536
|
+
async function handleRunnerRecord(ctx, options, deps) {
|
|
38537
|
+
const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
|
|
38538
|
+
if (resolved.type === "failed")
|
|
38539
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
38540
|
+
const command = options.command.action === "start" ? {
|
|
38541
|
+
action: "start",
|
|
38542
|
+
recordingId: options.command.recordingId ?? deps.makeRecordingId()
|
|
38543
|
+
} : options.command;
|
|
38544
|
+
const response = await recordOnRunner(ctx, resolved, command);
|
|
38545
|
+
const idNote = "recordingId" in command ? recordingMessages.recordingId(command.recordingId) : undefined;
|
|
38546
|
+
if (!response.ok) {
|
|
38547
|
+
const fields = failureFields(response);
|
|
38548
|
+
return {
|
|
38549
|
+
...fields,
|
|
38550
|
+
...idNote ? { errorBody: [fields.errorBody, idNote].filter(Boolean).join(`
|
|
38551
|
+
`) } : {},
|
|
38552
|
+
exitCode: response.exitCode
|
|
38553
|
+
};
|
|
38554
|
+
}
|
|
38555
|
+
const answer = response.value;
|
|
38556
|
+
if (answer.result.outcome === "failure") {
|
|
38557
|
+
return {
|
|
38558
|
+
...recordingFailure(answer.result.failureReason),
|
|
38559
|
+
errorBody: [idNote, answer.url].filter(Boolean).join(`
|
|
38560
|
+
`)
|
|
38561
|
+
};
|
|
38562
|
+
}
|
|
38563
|
+
ctx.ui.output(answer, formatRecordingState(answer));
|
|
38564
|
+
if (answer.result.recording?.status === "failed") {
|
|
38565
|
+
return {
|
|
38566
|
+
error: recordingMessages.failedCapture,
|
|
38567
|
+
exitCode: exitCodes.testFailure,
|
|
38568
|
+
errorBody: [
|
|
38569
|
+
recordingMessages.recordingId(answer.result.recording.id),
|
|
38570
|
+
answer.url
|
|
38571
|
+
].join(`
|
|
38572
|
+
`)
|
|
38573
|
+
};
|
|
38574
|
+
}
|
|
38575
|
+
return;
|
|
38576
|
+
}
|
|
38577
|
+
async function handleRunnerRecordings(ctx, options, deps) {
|
|
38578
|
+
const resolved = await resolveRunner(ctx, {
|
|
38579
|
+
autoLaunch: false,
|
|
38580
|
+
noRunnerIdMessage: recordingMessages.noRunnerIdForHistory,
|
|
38581
|
+
runner: options.runner
|
|
38582
|
+
}, deps);
|
|
38583
|
+
if (resolved.type === "failed")
|
|
38584
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
38585
|
+
const response = await readRunnerRecordings(ctx, resolved, {
|
|
38586
|
+
...options.recordingId === undefined ? {} : { recordingId: options.recordingId },
|
|
38587
|
+
...options.pageToken === undefined ? {} : { pageToken: options.pageToken }
|
|
38588
|
+
});
|
|
38589
|
+
if (!response.ok)
|
|
38590
|
+
return { ...failureFields(response), exitCode: response.exitCode };
|
|
38591
|
+
ctx.ui.output(response.value, formatRecordings(response.value));
|
|
38592
|
+
return;
|
|
38593
|
+
}
|
|
38594
|
+
|
|
38595
|
+
// src/commands/runner/recording.register.ts
|
|
38596
|
+
function registerRunnerRecordingCommands(runner, signals) {
|
|
38597
|
+
const record = runner.command("record").description("Control video recording on a Playwright runner");
|
|
38598
|
+
declareCommandKind(record.command("start"), "write").description("Start manual video capture across runs. Requires a ready screen and suppresses automatic capture until stopped").option("--recording-id <uuid>", "Recording UUID. Generated when omitted; reuse it when retrying this start").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecord(ctx, {
|
|
38599
|
+
command: { action: "start", recordingId: opts.recordingId },
|
|
38600
|
+
runner: opts.runner
|
|
38601
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
38602
|
+
declareCommandKind(record.command("stop <recording-id>"), "write").description("Stop the recording with this UUID and publish it. Retrying cannot stop a later recording").option("--runner <id>", runnerFlagDescription).action((recordingId, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecord(ctx, {
|
|
38603
|
+
command: { action: "stop", recordingId },
|
|
38604
|
+
runner: opts.runner
|
|
38605
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
38606
|
+
declareCommandKind(record.command("status"), "read").description("Show the active recording and automatic recording setting").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecord(ctx, {
|
|
38607
|
+
command: { action: "status" },
|
|
38608
|
+
runner: opts.runner
|
|
38609
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
38610
|
+
declareCommandKind(record.command("auto"), "write").addArgument(new Argument2("<setting>", "Automatic recording setting").choices([
|
|
38611
|
+
"on",
|
|
38612
|
+
"off"
|
|
38613
|
+
])).description("Enable or disable automatic recording for subsequent full runs").option("--runner <id>", runnerFlagDescription).action((setting, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecord(ctx, {
|
|
38614
|
+
command: { action: "auto", enabled: setting === "on" },
|
|
38615
|
+
runner: opts.runner
|
|
38616
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
38617
|
+
declareCommandKind(runner.command("list-recordings"), "read").description("Read a page of published video recordings, including after the runner terminates. Platform URLs persist; video URLs expire").option("--runner <id>", runnerFlagDescription).option("--recording-id <uuid>", "Look up one recording; an empty result means it is not published or does not exist").option("--page-token <token>", "Continue from nextPageToken returned by the previous page").action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerRecordings(ctx, {
|
|
38618
|
+
runner: opts.runner,
|
|
38619
|
+
...opts.recordingId === undefined ? {} : { recordingId: opts.recordingId },
|
|
38620
|
+
...opts.pageToken === undefined ? {} : { pageToken: opts.pageToken }
|
|
38621
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
38622
|
+
}
|
|
38623
|
+
|
|
38418
38624
|
// src/commands/runner/index.ts
|
|
38419
38625
|
function registerRunnerCommand(program, signals) {
|
|
38420
38626
|
const runner = program.command("runner").description("Drive an interactive runner on the QA Wolf platform");
|
|
@@ -38428,6 +38634,7 @@ function registerRunnerCommand(program, signals) {
|
|
|
38428
38634
|
registerRunnerImportPackageCommand(runner, signals);
|
|
38429
38635
|
registerRunnerHighlightSelectorCommand(runner, signals);
|
|
38430
38636
|
registerRunnerPromoteSnapshotCommand(runner, signals);
|
|
38637
|
+
registerRunnerRecordingCommands(runner, signals);
|
|
38431
38638
|
}
|
|
38432
38639
|
|
|
38433
38640
|
// src/commands/program.ts
|
|
@@ -38458,4 +38665,4 @@ createProgram({ signals }).parseAsync().catch(() => {
|
|
|
38458
38665
|
process.exitCode = 1;
|
|
38459
38666
|
}).finally(() => exitWhenIdle(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
38460
38667
|
|
|
38461
|
-
//# debugId=
|
|
38668
|
+
//# debugId=5DFA383F9EE30BDB64756E2164756E21
|
package/dist/runner-sdk.js
CHANGED
|
@@ -10868,6 +10868,9 @@ async function listRunners(ctx, deps) {
|
|
|
10868
10868
|
};
|
|
10869
10869
|
}
|
|
10870
10870
|
|
|
10871
|
+
// src/domains/interactiveRunner/deps.ts
|
|
10872
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10873
|
+
|
|
10871
10874
|
// src/shell/interactiveRunner/collectRunFiles.ts
|
|
10872
10875
|
import { join as join2 } from "node:path";
|
|
10873
10876
|
|
|
@@ -12122,6 +12125,7 @@ function makeInteractiveRunnerDeps(options) {
|
|
|
12122
12125
|
cwd: options.cwd,
|
|
12123
12126
|
env: options.env,
|
|
12124
12127
|
makeRunnerId,
|
|
12128
|
+
makeRecordingId: randomUUID2,
|
|
12125
12129
|
readFile: (path) => options.fs.readFile(path),
|
|
12126
12130
|
readStdin,
|
|
12127
12131
|
runFilesManifest: makeRunFilesManifestStore({
|
|
@@ -12238,9 +12242,10 @@ function makeDefaultFs() {
|
|
|
12238
12242
|
}
|
|
12239
12243
|
|
|
12240
12244
|
// src/core/publicApi/notFoundSubject.ts
|
|
12241
|
-
var
|
|
12245
|
+
var runnerRoutesWithoutLivePod = new Set([
|
|
12242
12246
|
"runner.launch",
|
|
12243
|
-
"runner.list"
|
|
12247
|
+
"runner.list",
|
|
12248
|
+
"runner.recordings"
|
|
12244
12249
|
]);
|
|
12245
12250
|
var runRoutesThatResolveOneRun = new Set([
|
|
12246
12251
|
"run.diagnose",
|
|
@@ -12262,7 +12267,7 @@ function field(input, name) {
|
|
|
12262
12267
|
return typeof value === "string" ? value : undefined;
|
|
12263
12268
|
}
|
|
12264
12269
|
function notFoundSubject(contractName, input) {
|
|
12265
|
-
if (contractName.startsWith("runner.") && !
|
|
12270
|
+
if (contractName.startsWith("runner.") && !runnerRoutesWithoutLivePod.has(contractName)) {
|
|
12266
12271
|
return { kind: "runner", runnerId: field(input, "id") };
|
|
12267
12272
|
}
|
|
12268
12273
|
if (runRoutesThatResolveOneRun.has(contractName)) {
|
|
@@ -14802,12 +14807,63 @@ function createRunVerbs({ deps, platformClient }) {
|
|
|
14802
14807
|
};
|
|
14803
14808
|
}
|
|
14804
14809
|
|
|
14810
|
+
// src/domains/interactiveRunner/recordingRequests.ts
|
|
14811
|
+
async function recordOnRunner(ctx, runner, command) {
|
|
14812
|
+
const contract = publicContractsV1.runner.record;
|
|
14813
|
+
const parsed = contract.input.safeParse({ id: runner.runnerId, command });
|
|
14814
|
+
if (!parsed.success) {
|
|
14815
|
+
return {
|
|
14816
|
+
ok: false,
|
|
14817
|
+
error: "Invalid recording request. Runner ids must be valid and recording ids must be UUIDs.",
|
|
14818
|
+
exitCode: exitCodes.invalidArgs
|
|
14819
|
+
};
|
|
14820
|
+
}
|
|
14821
|
+
const result = await ctx.platformClient.callPublicApi(contract, parsed.data, runnerCallOptions);
|
|
14822
|
+
return result.ok ? result : { ok: false, ...runnerRequestFailure(result, runner) };
|
|
14823
|
+
}
|
|
14824
|
+
async function readRunnerRecordings(ctx, runner, query) {
|
|
14825
|
+
const contract = publicContractsV1.runner.recordings;
|
|
14826
|
+
const parsed = contract.input.safeParse({ id: runner.runnerId, ...query });
|
|
14827
|
+
if (!parsed.success) {
|
|
14828
|
+
return {
|
|
14829
|
+
ok: false,
|
|
14830
|
+
error: "Invalid recordings request. Use a valid runner id, a UUID recording id, and a page token of at most 4096 characters.",
|
|
14831
|
+
exitCode: exitCodes.invalidArgs
|
|
14832
|
+
};
|
|
14833
|
+
}
|
|
14834
|
+
const result = await ctx.platformClient.callPublicApi(contract, parsed.data);
|
|
14835
|
+
return result.ok ? result : { ok: false, ...runnerRequestFailure(result, runner) };
|
|
14836
|
+
}
|
|
14837
|
+
|
|
14838
|
+
// src/runnerSdk/recordingVerbs.ts
|
|
14839
|
+
function createRecordingVerbs(context) {
|
|
14840
|
+
return {
|
|
14841
|
+
async record({
|
|
14842
|
+
runnerId,
|
|
14843
|
+
command
|
|
14844
|
+
}) {
|
|
14845
|
+
return toSdkResult(await recordOnRunner(context, givenRunner(runnerId), command));
|
|
14846
|
+
},
|
|
14847
|
+
async listRecordings({
|
|
14848
|
+
runnerId,
|
|
14849
|
+
recordingId,
|
|
14850
|
+
pageToken
|
|
14851
|
+
}) {
|
|
14852
|
+
return toSdkResult(await readRunnerRecordings(context, givenRunner(runnerId), {
|
|
14853
|
+
...recordingId === undefined ? {} : { recordingId },
|
|
14854
|
+
...pageToken === undefined ? {} : { pageToken }
|
|
14855
|
+
}));
|
|
14856
|
+
}
|
|
14857
|
+
};
|
|
14858
|
+
}
|
|
14859
|
+
|
|
14805
14860
|
// src/runnerSdk/index.ts
|
|
14806
14861
|
function createRunnerSdk(options) {
|
|
14807
14862
|
const context = createSdkContext(options);
|
|
14808
14863
|
return {
|
|
14809
14864
|
...createLifecycleVerbs(context),
|
|
14810
14865
|
...createRunVerbs(context),
|
|
14866
|
+
...createRecordingVerbs(context),
|
|
14811
14867
|
...createPageVerbs(context),
|
|
14812
14868
|
...createProjectVerbs(context),
|
|
14813
14869
|
async list() {
|
|
@@ -14820,4 +14876,4 @@ export {
|
|
|
14820
14876
|
createRunnerSdk
|
|
14821
14877
|
};
|
|
14822
14878
|
|
|
14823
|
-
//# debugId=
|
|
14879
|
+
//# debugId=A7A37F8150999CFE64756E2164756E21
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ListedRunner, RunnerSdkOptions, SdkResult } from "./types.js";
|
|
2
|
-
export type { ActRequest, EvaluateSnippetRequest, EvaluatedSnippet, EventsRequest, FileSync, HighlightRequest, HighlightSelectorRequest, HighlightedSelector, ImportPackageRequest, ImportedPackage, InspectRequest, Inspected, Journal, JournalWindow, KeptAlive, LaunchRequest, LaunchedRunner, LinesLocation, ListedRunner, PackageVersion, PerformedAction, PromoteSnapshotRequest, PromotedSnapshot, RunEnvironment, RunFilter, RunRequest, RunSelection, RunnerFamily, RunnerRequest, RunnerSdkOptions, Screenshot, SdkResult, SnippetScope, StoppedRun, SubmittedRun, TerminatedRunner, } from "./types.js";
|
|
2
|
+
export type { ActRequest, EvaluateSnippetRequest, EvaluatedSnippet, EventsRequest, FileSync, HighlightRequest, HighlightSelectorRequest, HighlightedSelector, ImportPackageRequest, ImportedPackage, InspectRequest, Inspected, Journal, JournalWindow, KeptAlive, LaunchRequest, LaunchedRunner, LinesLocation, ListedRunner, PackageVersion, PerformedAction, PromoteSnapshotRequest, PromotedSnapshot, RecordRequest, RecordResponse, RecordingsRequest, Recordings, RunEnvironment, RunFilter, RunRequest, RunSelection, RunnerFamily, RunnerRequest, RunnerSdkOptions, Screenshot, SdkResult, SnippetScope, StoppedRun, SubmittedRun, TerminatedRunner, } from "./types.js";
|
|
3
3
|
export type RunnerSdk = ReturnType<typeof createRunnerSdk>;
|
|
4
4
|
/**
|
|
5
5
|
* Drives interactive runners on the caller's team through the QA Wolf public
|
|
@@ -15,6 +15,8 @@ export declare function createRunnerSdk(options: RunnerSdkOptions): {
|
|
|
15
15
|
inspect({ request, runnerId, }: import("./types.js").InspectRequest): Promise<SdkResult<import("./types.js").Inspected>>;
|
|
16
16
|
promoteSnapshot({ baselinePath, runnerId, screenshotPath, }: import("./types.js").PromoteSnapshotRequest): Promise<SdkResult<import("./types.js").PromotedSnapshot>>;
|
|
17
17
|
screenshot({ runnerId, }: import("./types.js").RunnerRequest): Promise<SdkResult<import("./types.js").Screenshot>>;
|
|
18
|
+
record({ runnerId, command, }: import("./types.js").RecordRequest): Promise<SdkResult<import("./types.js").RecordResponse>>;
|
|
19
|
+
listRecordings({ runnerId, recordingId, pageToken, }: import("./types.js").RecordingsRequest): Promise<SdkResult<import("./types.js").Recordings>>;
|
|
18
20
|
events({ runFilter, runnerId, stream, window, }: import("./types.js").EventsRequest): Promise<SdkResult<import("./types.js").Journal>>;
|
|
19
21
|
run({ entryPointPath, environment, runnerId, selection, }: import("./types.js").RunRequest): Promise<SdkResult<import("./types.js").SubmittedRun>>;
|
|
20
22
|
keepalive({ runnerId, }: import("./types.js").RunnerRequest): Promise<SdkResult<import("./types.js").KeptAlive>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowserAction, InspectOnRunnerRequest, JournalStream, PublicApiOutput, ReadJournalResponse, RunnerNameForPublicApi, publicContractsV1 } from "@qawolf/api-contracts/v1";
|
|
1
|
+
import type { BrowserAction, InspectOnRunnerRequest, JournalStream, PublicApiInput, PublicApiOutput, ReadJournalResponse, RunnerNameForPublicApi, publicContractsV1 } from "@qawolf/api-contracts/v1";
|
|
2
2
|
type Runner = typeof publicContractsV1.runner;
|
|
3
3
|
export type RunnerSdkOptions = {
|
|
4
4
|
/** A QA Wolf team API key. The SDK never reads the CLI's stored credentials. */
|
|
@@ -60,6 +60,12 @@ export type LaunchRequest = {
|
|
|
60
60
|
export type RunnerRequest = {
|
|
61
61
|
runnerId: string;
|
|
62
62
|
};
|
|
63
|
+
/** Start and stop name the same UUID, so a retry cannot target a later capture. */
|
|
64
|
+
export type RecordRequest = RunnerRequest & Pick<PublicApiInput<Runner["record"]>, "command">;
|
|
65
|
+
export type RecordingsRequest = RunnerRequest & Pick<PublicApiInput<Runner["recordings"]>, "recordingId" | "pageToken">;
|
|
66
|
+
/** Runner refusal is carried on result.outcome, inside the platform answer. */
|
|
67
|
+
export type RecordResponse = PublicApiOutput<Runner["record"]>;
|
|
68
|
+
export type Recordings = PublicApiOutput<Runner["recordings"]>;
|
|
63
69
|
export type RunRequest = RunnerRequest & {
|
|
64
70
|
entryPointPath: string;
|
|
65
71
|
environment: RunEnvironment;
|
package/package.json
CHANGED
|
@@ -196,7 +196,12 @@ that `url`; never guess a route and never send a repository link in its place.
|
|
|
196
196
|
| `qawolf runner keepalive` | read | Reset a runner's inactivity clock, for a caller that pauses between actions |
|
|
197
197
|
| `qawolf runner launch` | write | Launch an interactive runner and make it this directory's default |
|
|
198
198
|
| `qawolf runner list` | read | List the runners running on your team |
|
|
199
|
+
| `qawolf runner list-recordings` | read | Read a page of published video recordings, including after the runner terminates. Platform URLs persist; video URLs expire |
|
|
199
200
|
| `qawolf runner promote-snapshot` | write | Accept a run's screenshot as the new baseline for an image diff, on the runner that produced it |
|
|
201
|
+
| `qawolf runner record auto` | write | Enable or disable automatic recording for subsequent full runs |
|
|
202
|
+
| `qawolf runner record start` | write | Start manual video capture across runs. Requires a ready screen and suppresses automatic capture until stopped |
|
|
203
|
+
| `qawolf runner record status` | read | Show the active recording and automatic recording setting |
|
|
204
|
+
| `qawolf runner record stop` | write | Stop the recording with this UUID and publish it. Retrying cannot stop a later recording |
|
|
200
205
|
| `qawolf runner run` | write | Run a flow on an interactive runner, shipping the flow and what it imports |
|
|
201
206
|
| `qawolf runner screenshot` | read | Save a JPEG of an interactive runner's screen to a file, or write it to stdout with --out - |
|
|
202
207
|
| `qawolf runner stop-run` | write | Stop what a runner is currently executing, leaving the runner up |
|
|
@@ -278,7 +283,8 @@ by default.
|
|
|
278
283
|
|
|
279
284
|
The `runner` commands drive a live cloud browser: `launch` one, `screenshot` to
|
|
280
285
|
see it, `act` to click and type, `run` a flow on it, `exec` a snippet against its
|
|
281
|
-
page, `
|
|
286
|
+
page, `record` to control video capture, `list-recordings` to read published video
|
|
287
|
+
history even after termination, `events` to read its journal (including the `recorder` stream, which turns
|
|
282
288
|
your actions into Playwright locators), `keepalive` to hold it open, and
|
|
283
289
|
`terminate`
|
|
284
290
|
when done. Everything is a plain request to one host, so a shell with an API key
|
|
@@ -195,6 +195,44 @@ The answer holds one entry per action reached, and the field to read on each is
|
|
|
195
195
|
|
|
196
196
|
The sequence stops at the first action that fails. `--continue-on-failure` carries on past one that reached the runner and did not take effect, which is only safe for actions that do not depend on each other: a `type` after a failed `click` goes to whatever has focus. A runner that cannot be reached, a screen that cannot serve, or running out of time ends the sequence either way, and the message names every action that did not succeed.
|
|
197
197
|
|
|
198
|
+
## Video recording
|
|
199
|
+
|
|
200
|
+
Use `record` to control video capture on a Playwright runner. Start a flow first
|
|
201
|
+
so the runner has a screen, then start a manual capture:
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
qawolf runner record start --runner ci --json
|
|
205
|
+
# Keep result.state.active.id from the response and use it to stop this capture:
|
|
206
|
+
qawolf runner record stop <recording-id> --runner ci
|
|
207
|
+
qawolf runner record status --runner ci
|
|
208
|
+
qawolf runner record auto on --runner ci
|
|
209
|
+
qawolf runner record auto off --runner ci
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Start generates a UUID unless you pass `--recording-id <uuid>`. If a start
|
|
213
|
+
response is lost, the error includes that UUID: check `record status` and reuse
|
|
214
|
+
the UUID when retrying. Stop always names a specific recording, so retrying it
|
|
215
|
+
cannot stop a later capture. Manual recordings span runs and suppress automatic
|
|
216
|
+
capture until stopped. The auto setting affects subsequent full runs.
|
|
217
|
+
An active automatic recording ends with its run; `record stop` cannot interrupt
|
|
218
|
+
it. If a stop publishes a recording with `status: "failed"`, the CLI returns
|
|
219
|
+
exit code 1 and still prints the manifest so callers can inspect it.
|
|
220
|
+
|
|
221
|
+
Published recordings remain accessible after the runner terminates:
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
qawolf runner list-recordings --runner ci --json
|
|
225
|
+
qawolf runner list-recordings --runner ci --recording-id <uuid> --json
|
|
226
|
+
qawolf runner list-recordings --runner ci --page-token '<nextPageToken>' --json
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Each response is one page with `recordings` and an optional `nextPageToken`.
|
|
230
|
+
Follow that token for more; the order is storage-key order, not newest-first.
|
|
231
|
+
Entries include status, run ids, a stable platform `url`, and an expiring
|
|
232
|
+
`videoUrl` when video is available. An empty lookup means the recording has not
|
|
233
|
+
been published or does not exist. Abrupt runner loss may leave video unavailable.
|
|
234
|
+
Keep the runner id for history lookups after termination clears the local default.
|
|
235
|
+
|
|
198
236
|
## The recorder: what you cannot get from pixels
|
|
199
237
|
|
|
200
238
|
`qawolf runner events recorder` is the capability that has no equivalent in a
|