@qawolf/cli 1.9.0 → 1.10.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/dist/cli.js +143 -67
- package/package.json +2 -2
- package/skills/qawolf-cli/SKILL.md +34 -2
- package/skills/qawolf-cli/references/runner.md +290 -0
package/dist/cli.js
CHANGED
|
@@ -20363,7 +20363,7 @@ var require_readdir_glob = __commonJS((exports, module) => {
|
|
|
20363
20363
|
readdirGlob.ReaddirGlob = ReaddirGlob;
|
|
20364
20364
|
});
|
|
20365
20365
|
|
|
20366
|
-
// node_modules/async/dist/async.js
|
|
20366
|
+
// node_modules/archiver/node_modules/async/dist/async.js
|
|
20367
20367
|
var require_async = __commonJS((exports, module) => {
|
|
20368
20368
|
(function(global2, factory) {
|
|
20369
20369
|
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global2 = typeof globalThis !== "undefined" ? globalThis : global2 || self, factory(global2.async = {}));
|
|
@@ -87848,7 +87848,7 @@ var require_src2 = __commonJS((exports, module) => {
|
|
|
87848
87848
|
}
|
|
87849
87849
|
});
|
|
87850
87850
|
|
|
87851
|
-
// node_modules/agent-base/dist/helpers.js
|
|
87851
|
+
// node_modules/webdriver/node_modules/https-proxy-agent/node_modules/agent-base/dist/helpers.js
|
|
87852
87852
|
var require_helpers = __commonJS((exports) => {
|
|
87853
87853
|
var __createBinding = exports && exports.__createBinding || (Object.create ? function(o2, m4, k2, k22) {
|
|
87854
87854
|
if (k22 === undefined)
|
|
@@ -87920,7 +87920,7 @@ var require_helpers = __commonJS((exports) => {
|
|
|
87920
87920
|
exports.req = req;
|
|
87921
87921
|
});
|
|
87922
87922
|
|
|
87923
|
-
// node_modules/agent-base/dist/index.js
|
|
87923
|
+
// node_modules/webdriver/node_modules/https-proxy-agent/node_modules/agent-base/dist/index.js
|
|
87924
87924
|
var require_dist3 = __commonJS((exports) => {
|
|
87925
87925
|
var __createBinding = exports && exports.__createBinding || (Object.create ? function(o2, m4, k2, k22) {
|
|
87926
87926
|
if (k22 === undefined)
|
|
@@ -88071,7 +88071,7 @@ var require_dist3 = __commonJS((exports) => {
|
|
|
88071
88071
|
exports.Agent = Agent2;
|
|
88072
88072
|
});
|
|
88073
88073
|
|
|
88074
|
-
// node_modules/https-proxy-agent/dist/parse-proxy-response.js
|
|
88074
|
+
// node_modules/webdriver/node_modules/https-proxy-agent/dist/parse-proxy-response.js
|
|
88075
88075
|
var require_parse_proxy_response = __commonJS((exports) => {
|
|
88076
88076
|
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
88077
88077
|
return mod && mod.__esModule ? mod : { default: mod };
|
|
@@ -88167,7 +88167,7 @@ var require_parse_proxy_response = __commonJS((exports) => {
|
|
|
88167
88167
|
exports.parseProxyResponse = parseProxyResponse;
|
|
88168
88168
|
});
|
|
88169
88169
|
|
|
88170
|
-
// node_modules/https-proxy-agent/dist/index.js
|
|
88170
|
+
// node_modules/webdriver/node_modules/https-proxy-agent/dist/index.js
|
|
88171
88171
|
var require_dist4 = __commonJS((exports) => {
|
|
88172
88172
|
var __createBinding = exports && exports.__createBinding || (Object.create ? function(o2, m4, k2, k22) {
|
|
88173
88173
|
if (k22 === undefined)
|
|
@@ -156412,12 +156412,14 @@ var interactiveRunnerMessages = {
|
|
|
156412
156412
|
launched: (id) => `Launched runner ${id}.`,
|
|
156413
156413
|
launchedForCommand: (id) => `No runner was given, so launched ${id} for this command. Its browser is fresh: nothing has been run on it and nothing is signed in. It bills until it is stopped or idles out, so stop it with qawolf runner stop --runner ${id} when you are done.`,
|
|
156414
156414
|
followEventsTimedOut: (stream, seconds) => `Stopped following ${stream} after ${formatSeconds(seconds * 1000)}: reading keeps the runner alive and billing, so a follow does not run unbounded. Pass --timeout to wait longer, or follow again to continue.`,
|
|
156415
|
+
followEndCutShort: "The run settled, but the last window of its followed streams could not be read, so the output above may be missing its final lines.",
|
|
156415
156416
|
followTimedOut: (runId, runnerId, seconds) => `Stopped following run ${runId} after ${formatSeconds(seconds * 1000)}. The run may still be going: read it with qawolf runner events run-status --run ${runId}, and stop the runner with qawolf runner stop --runner ${runnerId} when you are done. Pass --timeout to wait longer.`,
|
|
156416
156417
|
missingPackageJson: "No package.json in the current directory. A run reads its npm dependencies from one, so it has to travel with the flow.",
|
|
156417
156418
|
noRunnerIdForScreenshot: `${noRunnerId} A screenshot also needs a screen, which a runner gets from its first run: run a flow on it with qawolf runner run.`,
|
|
156418
156419
|
noRunnerId,
|
|
156419
156420
|
notRunning: (id) => `Runner ${id} was not running.`,
|
|
156420
156421
|
runFailed: (errorMessage2) => errorMessage2 === undefined ? "The run failed and reported no reason." : `The run failed: ${errorMessage2}`,
|
|
156422
|
+
runInProgress: "The run is in progress.",
|
|
156421
156423
|
runPassed: "The run passed.",
|
|
156422
156424
|
runSettledUnknown: (status) => `The run settled as "${status}", which this version of the CLI does not recognize. Upgrade to read it.`,
|
|
156423
156425
|
runSubmitAnsweredUnknown: (outcome) => `The runner answered the submission with "${outcome}", which this version of the CLI does not recognize. Upgrade to read it.`,
|
|
@@ -175909,7 +175911,7 @@ function startUpdateCheck(deps) {
|
|
|
175909
175911
|
// package.json
|
|
175910
175912
|
var package_default = {
|
|
175911
175913
|
name: "@qawolf/cli",
|
|
175912
|
-
version: "1.
|
|
175914
|
+
version: "1.10.0",
|
|
175913
175915
|
description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
175914
175916
|
keywords: [
|
|
175915
175917
|
"automation",
|
|
@@ -175975,7 +175977,6 @@ var package_default = {
|
|
|
175975
175977
|
"@qawolf/flows": "0.1.4",
|
|
175976
175978
|
"@qawolf/testkit": "1.1.1",
|
|
175977
175979
|
appium: "2.11.3",
|
|
175978
|
-
"appium-uiautomator2-driver": "3.7.0",
|
|
175979
175980
|
commander: "14.0.3",
|
|
175980
175981
|
"env-paths": "4.0.0",
|
|
175981
175982
|
"expect-webdriverio": "5.6.5",
|
|
@@ -175994,6 +175995,7 @@ var package_default = {
|
|
|
175994
175995
|
"@tsconfig/strictest": "2.0.8",
|
|
175995
175996
|
"@types/bun": "1.3.14",
|
|
175996
175997
|
"@types/picomatch": "4.0.3",
|
|
175998
|
+
"appium-uiautomator2-driver": "4.2.9",
|
|
175997
175999
|
knip: "6.16.1",
|
|
175998
176000
|
oxfmt: "0.54.0",
|
|
175999
176001
|
oxlint: "1.69.0",
|
|
@@ -176719,7 +176721,7 @@ var playwrightVersion = "1.62.0";
|
|
|
176719
176721
|
var emailsVersion = "1.1.1";
|
|
176720
176722
|
var testkitVersion = "1.1.1";
|
|
176721
176723
|
var appiumVersion = "2.11.3";
|
|
176722
|
-
var appiumUiautomator2DriverVersion = "
|
|
176724
|
+
var appiumUiautomator2DriverVersion = "4.2.9";
|
|
176723
176725
|
var expectWebdriverioVersion = "5.6.5";
|
|
176724
176726
|
|
|
176725
176727
|
// src/domains/doctor/checks/playwright.ts
|
|
@@ -177813,10 +177815,6 @@ var pinnedPackages = [
|
|
|
177813
177815
|
{ name: "@qawolf/emails", version: emailsVersion },
|
|
177814
177816
|
{ name: "@qawolf/testkit", version: testkitVersion },
|
|
177815
177817
|
{ name: "appium", version: appiumVersion },
|
|
177816
|
-
{
|
|
177817
|
-
name: "appium-uiautomator2-driver",
|
|
177818
|
-
version: appiumUiautomator2DriverVersion
|
|
177819
|
-
},
|
|
177820
177818
|
{ name: "expect-webdriverio", version: expectWebdriverioVersion }
|
|
177821
177819
|
];
|
|
177822
177820
|
|
|
@@ -186223,6 +186221,9 @@ function describeRunFilesCheck(check2) {
|
|
|
186223
186221
|
}
|
|
186224
186222
|
}
|
|
186225
186223
|
|
|
186224
|
+
// src/domains/interactiveRunner/runnerCallOptions.ts
|
|
186225
|
+
var runnerCallOptions = { timeoutMs: 60000 };
|
|
186226
|
+
|
|
186226
186227
|
// src/domains/interactiveRunner/runnerIds.ts
|
|
186227
186228
|
function parseRunnerId(id) {
|
|
186228
186229
|
const parsed = runnerIdSchema.safeParse(id);
|
|
@@ -186242,10 +186243,10 @@ async function launchRunner(ctx, options) {
|
|
|
186242
186243
|
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.launch, {
|
|
186243
186244
|
id: options.id,
|
|
186244
186245
|
...options.runnerName ? { runnerName: options.runnerName } : {}
|
|
186245
|
-
});
|
|
186246
|
+
}, runnerCallOptions);
|
|
186246
186247
|
if (!result.ok) {
|
|
186247
186248
|
return {
|
|
186248
|
-
|
|
186249
|
+
...failureFields(result),
|
|
186249
186250
|
exitCode: exitCodes.network,
|
|
186250
186251
|
mayHaveArrived: result.mayHaveArrived ?? false,
|
|
186251
186252
|
ok: false
|
|
@@ -186270,10 +186271,8 @@ async function launchAndRemember(ctx, options, deps) {
|
|
|
186270
186271
|
});
|
|
186271
186272
|
}
|
|
186272
186273
|
return {
|
|
186273
|
-
|
|
186274
|
-
|
|
186275
|
-
mayHaveArrived: launched.mayHaveArrived,
|
|
186276
|
-
ok: false
|
|
186274
|
+
...launched,
|
|
186275
|
+
error: launched.mayHaveArrived ? interactiveRunnerMessages.launchLost(options.id, launched.error) : interactiveRunnerMessages.launchFailed(options.id, launched.error)
|
|
186277
186276
|
};
|
|
186278
186277
|
}
|
|
186279
186278
|
async function handleRunnerLaunch(ctx, options, deps) {
|
|
@@ -186287,7 +186286,7 @@ async function handleRunnerLaunch(ctx, options, deps) {
|
|
|
186287
186286
|
}
|
|
186288
186287
|
const launched = await launchAndRemember(ctx, { id: id.id, runnerName: runnerName?.runnerName }, deps);
|
|
186289
186288
|
if (!launched.ok) {
|
|
186290
|
-
return {
|
|
186289
|
+
return { ...failureFields(launched), exitCode: launched.exitCode };
|
|
186291
186290
|
}
|
|
186292
186291
|
ctx.ui.output(launched.value, launched.value.outcome === "launched" ? interactiveRunnerMessages.launched(launched.value.id) : interactiveRunnerMessages.alreadyRunning(launched.value.id));
|
|
186293
186292
|
return;
|
|
@@ -186323,7 +186322,7 @@ async function resolveRunner(ctx, options, deps) {
|
|
|
186323
186322
|
const launched = await launchAndRemember(ctx, { id: deps.makeRunnerId(), runnerName: undefined }, deps);
|
|
186324
186323
|
if (!launched.ok) {
|
|
186325
186324
|
return {
|
|
186326
|
-
|
|
186325
|
+
...failureFields(launched),
|
|
186327
186326
|
exitCode: launched.exitCode,
|
|
186328
186327
|
type: "failed"
|
|
186329
186328
|
};
|
|
@@ -186374,7 +186373,7 @@ async function handleRunnerExec(ctx, options, deps) {
|
|
|
186374
186373
|
return { error: scope.error, exitCode: exitCodes.invalidArgs };
|
|
186375
186374
|
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
186376
186375
|
if (resolved.type === "failed") {
|
|
186377
|
-
return {
|
|
186376
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
186378
186377
|
}
|
|
186379
186378
|
announceRunner(ctx, resolved);
|
|
186380
186379
|
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.evaluateSnippet, {
|
|
@@ -186382,7 +186381,7 @@ async function handleRunnerExec(ctx, options, deps) {
|
|
|
186382
186381
|
id: resolved.runnerId,
|
|
186383
186382
|
...scope.filePath === undefined ? {} : { filePath: scope.filePath },
|
|
186384
186383
|
...scope.files === undefined ? {} : { files: scope.files }
|
|
186385
|
-
});
|
|
186384
|
+
}, runnerCallOptions);
|
|
186386
186385
|
if (!result.ok) {
|
|
186387
186386
|
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
186388
186387
|
}
|
|
@@ -186473,10 +186472,10 @@ async function handleRunnerAct(ctx, options, deps) {
|
|
|
186473
186472
|
return { error: built.error, exitCode: exitCodes.invalidArgs };
|
|
186474
186473
|
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
186475
186474
|
if (resolved.type === "failed") {
|
|
186476
|
-
return {
|
|
186475
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
186477
186476
|
}
|
|
186478
186477
|
announceRunner(ctx, resolved);
|
|
186479
|
-
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.performAction, { action: built.action, id: resolved.runnerId });
|
|
186478
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.performAction, { action: built.action, id: resolved.runnerId }, runnerCallOptions);
|
|
186480
186479
|
if (!result.ok) {
|
|
186481
186480
|
const fields = failureFields(result);
|
|
186482
186481
|
return {
|
|
@@ -186527,9 +186526,9 @@ async function handleRunnerScreenshot(ctx, options, deps) {
|
|
|
186527
186526
|
runner: options.runner
|
|
186528
186527
|
}, deps);
|
|
186529
186528
|
if (resolved.type === "failed") {
|
|
186530
|
-
return {
|
|
186529
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
186531
186530
|
}
|
|
186532
|
-
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.takeScreenshot, { id: resolved.runnerId });
|
|
186531
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.takeScreenshot, { id: resolved.runnerId }, runnerCallOptions);
|
|
186533
186532
|
if (!result.ok) {
|
|
186534
186533
|
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
186535
186534
|
}
|
|
@@ -186745,7 +186744,7 @@ async function readJournal(ctx, runnerId, request) {
|
|
|
186745
186744
|
...request.runId === undefined ? {} : { runId: request.runId },
|
|
186746
186745
|
...request.sinceSequence === undefined ? {} : { sinceSequence: request.sinceSequence },
|
|
186747
186746
|
...request.tail === undefined ? {} : { tail: request.tail }
|
|
186748
|
-
});
|
|
186747
|
+
}, runnerCallOptions);
|
|
186749
186748
|
if (!result.ok) {
|
|
186750
186749
|
return {
|
|
186751
186750
|
...failureFields(result),
|
|
@@ -186763,7 +186762,7 @@ async function readJournal(ctx, runnerId, request) {
|
|
|
186763
186762
|
async function handleRunnerKeepalive(ctx, options, deps) {
|
|
186764
186763
|
const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
|
|
186765
186764
|
if (resolved.type === "failed") {
|
|
186766
|
-
return {
|
|
186765
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
186767
186766
|
}
|
|
186768
186767
|
const window2 = await readJournal(ctx, resolved.runnerId, {
|
|
186769
186768
|
stream: "run-status",
|
|
@@ -186781,11 +186780,11 @@ async function handleRunnerKeepalive(ctx, options, deps) {
|
|
|
186781
186780
|
async function handleRunnerStop(ctx, options, deps) {
|
|
186782
186781
|
const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
|
|
186783
186782
|
if (resolved.type === "failed") {
|
|
186784
|
-
return {
|
|
186783
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
186785
186784
|
}
|
|
186786
186785
|
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.stop, {
|
|
186787
186786
|
id: resolved.runnerId
|
|
186788
|
-
});
|
|
186787
|
+
}, runnerCallOptions);
|
|
186789
186788
|
if (!result.ok) {
|
|
186790
186789
|
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
186791
186790
|
}
|
|
@@ -186856,6 +186855,20 @@ function readRunSettlement(payload) {
|
|
|
186856
186855
|
type: "settled"
|
|
186857
186856
|
};
|
|
186858
186857
|
}
|
|
186858
|
+
function findSettlement(entries) {
|
|
186859
|
+
for (const entry of entries) {
|
|
186860
|
+
const settlement = readRunSettlement(entry.payload);
|
|
186861
|
+
if (settlement.type !== "settled")
|
|
186862
|
+
continue;
|
|
186863
|
+
if (settlement.status === "passed")
|
|
186864
|
+
return { type: "passed" };
|
|
186865
|
+
if (settlement.status === "failed") {
|
|
186866
|
+
return { errorMessage: settlement.errorMessage, type: "failed" };
|
|
186867
|
+
}
|
|
186868
|
+
return { status: settlement.status, type: "unrecognized" };
|
|
186869
|
+
}
|
|
186870
|
+
return;
|
|
186871
|
+
}
|
|
186859
186872
|
var runLogPayloadSchema = exports_external.object({ message: exports_external.string() });
|
|
186860
186873
|
function formatRunLogLine(payload) {
|
|
186861
186874
|
const parsed = runLogPayloadSchema.safeParse(payload);
|
|
@@ -186918,6 +186931,18 @@ function createJournalCursor(ctx, runnerId, request) {
|
|
|
186918
186931
|
return { entries: window2.value.entries, type: "entries" };
|
|
186919
186932
|
};
|
|
186920
186933
|
}
|
|
186934
|
+
function createPrintingCursor(ctx, runnerId, request, format) {
|
|
186935
|
+
const read = createJournalCursor(ctx, runnerId, request);
|
|
186936
|
+
return async () => {
|
|
186937
|
+
const window2 = await read();
|
|
186938
|
+
if (window2.type !== "entries")
|
|
186939
|
+
return window2;
|
|
186940
|
+
for (const entry of window2.entries) {
|
|
186941
|
+
ctx.ui.stream(entry, format(entry.payload));
|
|
186942
|
+
}
|
|
186943
|
+
return window2;
|
|
186944
|
+
};
|
|
186945
|
+
}
|
|
186921
186946
|
var unreachableGraceMs = 60000;
|
|
186922
186947
|
function createUnreachableBudget(pollIntervalMs2) {
|
|
186923
186948
|
const limit = Math.ceil(unreachableGraceMs / pollIntervalMs2);
|
|
@@ -186942,7 +186967,7 @@ async function handleRunnerEvents(ctx, options, deps) {
|
|
|
186942
186967
|
}
|
|
186943
186968
|
const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
|
|
186944
186969
|
if (resolved.type === "failed") {
|
|
186945
|
-
return {
|
|
186970
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
186946
186971
|
}
|
|
186947
186972
|
const known = knownJournalStreams;
|
|
186948
186973
|
if (!known.includes(parsed.value.stream)) {
|
|
@@ -186999,22 +187024,49 @@ function readErrorPath(error51) {
|
|
|
186999
187024
|
return typeof path9 === "string" ? path9 : undefined;
|
|
187000
187025
|
}
|
|
187001
187026
|
|
|
187002
|
-
// src/domains/interactiveRunner/
|
|
187003
|
-
var
|
|
187004
|
-
function
|
|
187005
|
-
|
|
187006
|
-
|
|
187007
|
-
|
|
187008
|
-
|
|
187009
|
-
|
|
187010
|
-
|
|
187011
|
-
|
|
187012
|
-
|
|
187027
|
+
// src/domains/interactiveRunner/followPrinters.ts
|
|
187028
|
+
var anchorPollIntervalMs = 1000;
|
|
187029
|
+
async function resolveRecorderAnchor(ctx, resolved, deps) {
|
|
187030
|
+
if (resolved.type === "launched")
|
|
187031
|
+
return { ok: true, sinceSequence: 0 };
|
|
187032
|
+
return anchorRecorderCursor(ctx, resolved.runnerId, deps);
|
|
187033
|
+
}
|
|
187034
|
+
async function anchorRecorderCursor(ctx, runnerId, deps) {
|
|
187035
|
+
const unreachable = createUnreachableBudget(anchorPollIntervalMs);
|
|
187036
|
+
for (;; ) {
|
|
187037
|
+
const anchor = await readJournal(ctx, runnerId, {
|
|
187038
|
+
stream: "recorder",
|
|
187039
|
+
tail: 1
|
|
187040
|
+
});
|
|
187041
|
+
if (anchor.type === "read") {
|
|
187042
|
+
return { ok: true, sinceSequence: anchor.value.nextSequence };
|
|
187013
187043
|
}
|
|
187014
|
-
|
|
187044
|
+
if (anchor.type === "failed") {
|
|
187045
|
+
return { failure: journalReadFailure(anchor), ok: false };
|
|
187046
|
+
}
|
|
187047
|
+
if (unreachable.exhausted()) {
|
|
187048
|
+
return { failure: { ...unreachableFailure }, ok: false };
|
|
187049
|
+
}
|
|
187050
|
+
await deps.sleep(anchorPollIntervalMs);
|
|
187015
187051
|
}
|
|
187016
|
-
return;
|
|
187017
187052
|
}
|
|
187053
|
+
function createFollowPrinters(ctx, options) {
|
|
187054
|
+
const jsonLine = (payload) => JSON.stringify(payload);
|
|
187055
|
+
const printers = [];
|
|
187056
|
+
if (options.logs) {
|
|
187057
|
+
printers.push(createPrintingCursor(ctx, options.runnerId, { runId: options.runId, stream: "run-logs" }, formatRunLogLine));
|
|
187058
|
+
}
|
|
187059
|
+
if (options.runEvents) {
|
|
187060
|
+
printers.push(createPrintingCursor(ctx, options.runnerId, { runId: options.runId, stream: "run-events" }, jsonLine));
|
|
187061
|
+
}
|
|
187062
|
+
if (options.recorderSinceSequence !== undefined) {
|
|
187063
|
+
printers.push(createPrintingCursor(ctx, options.runnerId, { sinceSequence: options.recorderSinceSequence, stream: "recorder" }, jsonLine));
|
|
187064
|
+
}
|
|
187065
|
+
return printers;
|
|
187066
|
+
}
|
|
187067
|
+
|
|
187068
|
+
// src/domains/interactiveRunner/followRun.ts
|
|
187069
|
+
var pollIntervalMs3 = 1000;
|
|
187018
187070
|
function reportSettlement(ctx, settlement) {
|
|
187019
187071
|
if (settlement.type === "passed") {
|
|
187020
187072
|
ctx.ui.success(interactiveRunnerMessages.runPassed);
|
|
@@ -187026,40 +187078,49 @@ function reportSettlement(ctx, settlement) {
|
|
|
187026
187078
|
};
|
|
187027
187079
|
}
|
|
187028
187080
|
async function followRun(ctx, options, deps) {
|
|
187029
|
-
const
|
|
187030
|
-
runId: options.runId,
|
|
187031
|
-
stream: "run-logs"
|
|
187032
|
-
});
|
|
187081
|
+
const printers = createFollowPrinters(ctx, options);
|
|
187033
187082
|
const readStatus = createJournalCursor(ctx, options.runnerId, {
|
|
187034
187083
|
runId: options.runId,
|
|
187035
187084
|
stream: "run-status"
|
|
187036
187085
|
});
|
|
187037
187086
|
const unreachable = createUnreachableBudget(pollIntervalMs3);
|
|
187038
|
-
const
|
|
187039
|
-
const
|
|
187040
|
-
|
|
187041
|
-
|
|
187042
|
-
|
|
187043
|
-
ctx.ui.stream(entry, formatRunLogLine(entry.payload));
|
|
187087
|
+
const printAll = async () => {
|
|
187088
|
+
for (const print of printers) {
|
|
187089
|
+
const window2 = await print();
|
|
187090
|
+
if (window2.type !== "entries")
|
|
187091
|
+
return window2;
|
|
187044
187092
|
}
|
|
187045
|
-
return
|
|
187093
|
+
return;
|
|
187094
|
+
};
|
|
187095
|
+
let progressReported = false;
|
|
187096
|
+
const printProgress = (entries) => {
|
|
187097
|
+
if (printers.length > 0 || progressReported)
|
|
187098
|
+
return;
|
|
187099
|
+
const entry = entries.find((e) => readRunSettlement(e.payload).type === "in-progress");
|
|
187100
|
+
if (entry === undefined)
|
|
187101
|
+
return;
|
|
187102
|
+
progressReported = true;
|
|
187103
|
+
ctx.ui.stream(entry, interactiveRunnerMessages.runInProgress);
|
|
187046
187104
|
};
|
|
187047
187105
|
const maxPolls = Math.max(1, Math.ceil(options.timeoutSeconds * 1000 / pollIntervalMs3));
|
|
187048
187106
|
for (let poll = 1;; poll++) {
|
|
187049
|
-
const
|
|
187050
|
-
if (
|
|
187051
|
-
return journalReadFailure(
|
|
187052
|
-
const status =
|
|
187107
|
+
const interrupted = await printAll();
|
|
187108
|
+
if (interrupted?.type === "failed")
|
|
187109
|
+
return journalReadFailure(interrupted);
|
|
187110
|
+
const status = interrupted?.type === "unreachable" ? interrupted : await readStatus();
|
|
187053
187111
|
if (status.type === "failed")
|
|
187054
187112
|
return journalReadFailure(status);
|
|
187055
|
-
if (
|
|
187113
|
+
if (status.type === "unreachable") {
|
|
187056
187114
|
if (unreachable.exhausted())
|
|
187057
187115
|
return { ...unreachableFailure };
|
|
187058
187116
|
} else {
|
|
187059
187117
|
unreachable.reset();
|
|
187118
|
+
printProgress(status.entries);
|
|
187060
187119
|
const settlement = findSettlement(status.entries);
|
|
187061
187120
|
if (settlement !== undefined) {
|
|
187062
|
-
await
|
|
187121
|
+
if (await printAll() !== undefined) {
|
|
187122
|
+
ctx.ui.warn(interactiveRunnerMessages.followEndCutShort);
|
|
187123
|
+
}
|
|
187063
187124
|
return reportSettlement(ctx, settlement);
|
|
187064
187125
|
}
|
|
187065
187126
|
}
|
|
@@ -187094,10 +187155,17 @@ async function handleRunnerRun(ctx, options, deps) {
|
|
|
187094
187155
|
}
|
|
187095
187156
|
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
187096
187157
|
if (resolved.type === "failed") {
|
|
187097
|
-
return {
|
|
187158
|
+
return { ...failureFields(resolved), exitCode: resolved.exitCode };
|
|
187098
187159
|
}
|
|
187099
187160
|
announceRunner(ctx, resolved);
|
|
187100
|
-
|
|
187161
|
+
let recorderSinceSequence;
|
|
187162
|
+
if (options.recorderEvents) {
|
|
187163
|
+
const anchor = await resolveRecorderAnchor(ctx, resolved, deps);
|
|
187164
|
+
if (!anchor.ok)
|
|
187165
|
+
return { ...anchor.failure };
|
|
187166
|
+
recorderSinceSequence = anchor.sinceSequence;
|
|
187167
|
+
}
|
|
187168
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.runFlow, { entryPointPath, files, id: resolved.runnerId }, runnerCallOptions);
|
|
187101
187169
|
if (!result.ok) {
|
|
187102
187170
|
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
187103
187171
|
}
|
|
@@ -187114,12 +187182,16 @@ async function handleRunnerRun(ctx, options, deps) {
|
|
|
187114
187182
|
};
|
|
187115
187183
|
case "submitted": {
|
|
187116
187184
|
const runId = result.value.runId;
|
|
187117
|
-
|
|
187185
|
+
const follow = options.follow || options.logs || options.runEvents || options.recorderEvents;
|
|
187186
|
+
if (!follow) {
|
|
187118
187187
|
ctx.ui.output({ runId, runnerId: resolved.runnerId }, interactiveRunnerMessages.runSubmitted(runId));
|
|
187119
187188
|
return;
|
|
187120
187189
|
}
|
|
187121
187190
|
ctx.ui.info(interactiveRunnerMessages.runSubmitted(runId));
|
|
187122
187191
|
return followRun(ctx, {
|
|
187192
|
+
logs: options.logs,
|
|
187193
|
+
recorderSinceSequence,
|
|
187194
|
+
runEvents: options.runEvents,
|
|
187123
187195
|
runId,
|
|
187124
187196
|
runnerId: resolved.runnerId,
|
|
187125
187197
|
timeoutSeconds: timeout.seconds
|
|
@@ -187139,16 +187211,20 @@ async function handleRunnerRun(ctx, options, deps) {
|
|
|
187139
187211
|
var runExamples2 = `
|
|
187140
187212
|
Examples:
|
|
187141
187213
|
$ qawolf runner run flows/checkout.flow.ts
|
|
187142
|
-
$ qawolf runner run flows/checkout.flow.ts --follow
|
|
187214
|
+
$ qawolf runner run flows/checkout.flow.ts --follow
|
|
187215
|
+
$ qawolf runner run flows/checkout.flow.ts --follow --logs`;
|
|
187143
187216
|
var eventsExamples = `
|
|
187144
187217
|
Examples:
|
|
187145
187218
|
$ qawolf runner events recorder --tail 5
|
|
187146
187219
|
$ qawolf runner events run-logs --run <runId> --follow
|
|
187147
187220
|
$ qawolf runner events console --since 120 --json`;
|
|
187148
187221
|
function registerRunnerRunCommands(runner, signals) {
|
|
187149
|
-
declareCommandKind(runner.command("run <file>"), "write").description("Run a flow on an interactive runner, shipping the current directory's files with it").option("--follow", "
|
|
187222
|
+
declareCommandKind(runner.command("run <file>"), "write").description("Run a flow on an interactive runner, shipping the current directory's files with it").option("--follow", "Report the run's status until it settles: in progress, then passed or failed", false).option("--logs", "Stream every log line the run produces while following. Implies --follow", false).option("--run-events", "Stream the run's progress events as JSON lines while following. Implies --follow", false).option("--recorder-events", "Stream the browser actions the runner records as JSON lines while following, from an anchor taken just before submission: the recorder is runner-wide, not run-scoped. Implies --follow", false).option("--runner <id>", runnerFlagDescription).option("--timeout <seconds>", "Give up following after this long. Following keeps the runner alive, so a run that never settles would otherwise bill until the terminal closed", String(defaultFollowTimeoutSeconds)).addHelpText("after", runExamples2).action((file2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerRun(ctx, {
|
|
187150
187223
|
entryPoint: file2,
|
|
187151
187224
|
follow: opts.follow,
|
|
187225
|
+
logs: opts.logs,
|
|
187226
|
+
recorderEvents: opts.recorderEvents,
|
|
187227
|
+
runEvents: opts.runEvents,
|
|
187152
187228
|
runner: opts.runner,
|
|
187153
187229
|
timeout: opts.timeout
|
|
187154
187230
|
}, runnerDeps(ctx)))(opts, command));
|
|
@@ -187208,4 +187284,4 @@ createProgram({ signals }).parseAsync().catch(() => {
|
|
|
187208
187284
|
process.exitCode = 1;
|
|
187209
187285
|
}).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
187210
187286
|
|
|
187211
|
-
//# debugId=
|
|
187287
|
+
//# debugId=8CB687F9691544E564756E2164756E21
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qawolf/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -66,7 +66,6 @@
|
|
|
66
66
|
"@qawolf/flows": "0.1.4",
|
|
67
67
|
"@qawolf/testkit": "1.1.1",
|
|
68
68
|
"appium": "2.11.3",
|
|
69
|
-
"appium-uiautomator2-driver": "3.7.0",
|
|
70
69
|
"commander": "14.0.3",
|
|
71
70
|
"env-paths": "4.0.0",
|
|
72
71
|
"expect-webdriverio": "5.6.5",
|
|
@@ -85,6 +84,7 @@
|
|
|
85
84
|
"@tsconfig/strictest": "2.0.8",
|
|
86
85
|
"@types/bun": "1.3.14",
|
|
87
86
|
"@types/picomatch": "4.0.3",
|
|
87
|
+
"appium-uiautomator2-driver": "4.2.9",
|
|
88
88
|
"knip": "6.16.1",
|
|
89
89
|
"oxfmt": "0.54.0",
|
|
90
90
|
"oxlint": "1.69.0",
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qawolf-cli
|
|
3
|
-
description: Manage QA Wolf through the qawolf CLI. Use when asked which QA Wolf environment variables are available or to list, set, or delete them; manage environments, flows, runs, tags, or issues; authenticate; install; or
|
|
3
|
+
description: Manage QA Wolf through the qawolf CLI. Use when asked which QA Wolf environment variables are available or to list, set, or delete them; manage environments, flows, runs, tags, or issues; authenticate; install; run or list flows; or drive a live cloud browser (launch a runner, screenshot it, click and type on it, read its recorder) from a shell.
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
compatibility: Requires the qawolf CLI on PATH. Install it from @qawolf/cli or use a standalone binary from GitHub Releases.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# QA Wolf CLI
|
|
9
9
|
|
|
10
|
-
`qawolf` runs QA Wolf flows locally
|
|
10
|
+
`qawolf` runs QA Wolf flows locally, calls the QA Wolf public API, and drives
|
|
11
|
+
interactive runners: live cloud pods holding a browser you can see and act on.
|
|
11
12
|
|
|
12
13
|
This file is an overview, not a reference. Before first using a command whose
|
|
13
14
|
flags are not shown here, run `qawolf <command> --help` once. The installed CLI
|
|
@@ -22,6 +23,11 @@ environment variable (or stored credentials from `qawolf auth login`).
|
|
|
22
23
|
their table entry notes a flag that switches them to `read`.
|
|
23
24
|
Verify with `qawolf auth whoami`. Never print or log the key.
|
|
24
25
|
|
|
26
|
+
A team API key in the environment is the whole credential, including for the
|
|
27
|
+
`runner` group. Nothing needs a browser login, a session token or a held
|
|
28
|
+
connection, so a sandbox that can set one environment variable and make
|
|
29
|
+
requests to one host can do everything below.
|
|
30
|
+
|
|
25
31
|
Commands use `https://app.qawolf.com` by default. Set `QAWOLF_HOST_URL` to
|
|
26
32
|
target another deployment host, for example
|
|
27
33
|
`https://app.staging.example.com`. `QAWOLF_API_URL` is a separate API endpoint
|
|
@@ -61,6 +67,11 @@ Human-formatted output is not stable across versions. Errors go to stderr;
|
|
|
61
67
|
a non-zero exit code means the command failed. Reuse successful read results
|
|
62
68
|
within a task unless a relevant write or target change could make them stale.
|
|
63
69
|
|
|
70
|
+
One exception to know about: on `qawolf runner events`, `--json` also switches
|
|
71
|
+
each printed line from the payload alone to the whole envelope (`sequence`,
|
|
72
|
+
`recordedAt`, `payload`). Both are JSON. Pass it when you want to page by
|
|
73
|
+
sequence, omit it when you want the payloads themselves.
|
|
74
|
+
|
|
64
75
|
## Safety: reads vs writes
|
|
65
76
|
|
|
66
77
|
Read commands do not change team data, but some have operational effects noted
|
|
@@ -71,6 +82,12 @@ successful write response is confirmation, so do not read immediately only to
|
|
|
71
82
|
verify it. Never blind-retry a write on timeout: it may have reached the server
|
|
72
83
|
the first time.
|
|
73
84
|
|
|
85
|
+
Two runner-specific costs to keep in mind. Launching a runner starts a billed
|
|
86
|
+
pod, so reuse one id rather than minting new ones per step, and stop a runner
|
|
87
|
+
when you are done. And `run`, `act` and `exec` may all have taken effect even
|
|
88
|
+
when their answer never arrives, so none of them is safe to blind-retry; `run`
|
|
89
|
+
is the expensive one, because a second submission bills a second run.
|
|
90
|
+
|
|
74
91
|
## Git-backed workflows
|
|
75
92
|
|
|
76
93
|
Inspect `git status` before publishing. Stage and commit only files changed for
|
|
@@ -130,3 +147,18 @@ Kinds: `read` calls the QA Wolf API without changing anything; `write`
|
|
|
130
147
|
changes team state; `local` only affects this machine. A parenthesized
|
|
131
148
|
note like `local (read with --remote)` means that flag makes the command
|
|
132
149
|
call the QA Wolf API and require auth.
|
|
150
|
+
|
|
151
|
+
## Driving a browser: the `runner` group
|
|
152
|
+
|
|
153
|
+
The `runner` commands drive a live cloud browser: `launch` one, `screenshot` to
|
|
154
|
+
see it, `act` to click and type, `run` a flow on it, `exec` a snippet against its
|
|
155
|
+
page, `events` to read its journal (including the `recorder` stream, which turns
|
|
156
|
+
your actions into Playwright locators), `keepalive` to hold it open, and `stop`
|
|
157
|
+
when done. Everything is a plain request to one host, so a shell with an API key
|
|
158
|
+
and its own vision model can close the see-and-act loop with no other tooling.
|
|
159
|
+
|
|
160
|
+
The full workflow is its own guide: how a runner is billed, why the first call
|
|
161
|
+
must be a run, the order the commands go in, the see-and-act loop, `exec`, the
|
|
162
|
+
recorder, reading history, staying alive, and an end-to-end example. **Read
|
|
163
|
+
[`references/runner.md`](references/runner.md) before driving a runner for the
|
|
164
|
+
first time.**
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Driving a runner from the terminal
|
|
2
|
+
|
|
3
|
+
An interactive runner is a live pod with a browser in it. You launch one, look
|
|
4
|
+
at it, act on it, run flows on it, and read what it recorded. Everything is a
|
|
5
|
+
plain request to one host, so there is no connection to hold open.
|
|
6
|
+
|
|
7
|
+
## Getting one
|
|
8
|
+
|
|
9
|
+
Runner ids are yours to choose and are scoped to your team, so `agent-1` is a
|
|
10
|
+
fine id. Launching an id that is already running attaches to that runner instead
|
|
11
|
+
of starting and billing a second one, and the answer says which happened: read
|
|
12
|
+
`outcome` for `launched` or `already-running`. Reusing one id is therefore the
|
|
13
|
+
cheap and safe pattern, and the same id with a different `--name` is refused
|
|
14
|
+
rather than silently ignored.
|
|
15
|
+
|
|
16
|
+
Commands that target a runner find one in this order: `--runner`, then
|
|
17
|
+
`QAWOLF_RUNNER_ID`, then the runner stored for the current directory (which
|
|
18
|
+
`qawolf runner launch` sets). Setting the environment variable once is the most
|
|
19
|
+
robust for a harness whose working directory may not be stable, but it comes
|
|
20
|
+
with two catches worth knowing before you rely on it.
|
|
21
|
+
|
|
22
|
+
`qawolf runner launch` is not in that order: it takes its id from `--id` and
|
|
23
|
+
never reads `QAWOLF_RUNNER_ID`. Bare `qawolf runner launch` invents a random id,
|
|
24
|
+
bills a pod under it and stores it, so a harness that exported the variable and
|
|
25
|
+
then launched without `--id` ends up with a pod it is not addressing. Pass
|
|
26
|
+
`--id` whenever you have an id in mind.
|
|
27
|
+
|
|
28
|
+
And a runner id that is set is treated as found, whether or not anything is
|
|
29
|
+
running under it. So exporting `QAWOLF_RUNNER_ID=agent-1` turns off the
|
|
30
|
+
auto-launch described next: instead of starting `agent-1`, commands try to reach
|
|
31
|
+
it and fail with exit code `4`, which reads as "retry" and never succeeds.
|
|
32
|
+
Launch that id once yourself and the rest follows.
|
|
33
|
+
|
|
34
|
+
If nothing names a runner, the commands that change something will launch one
|
|
35
|
+
and say so on stderr, naming it: `run`, `act` and `exec`. **Read that
|
|
36
|
+
announcement.** The browser it just started is fresh: nothing has been run on it,
|
|
37
|
+
nothing is signed in, and no page is open. Acting as though your earlier setup
|
|
38
|
+
survived is the single most likely way to drive the wrong page.
|
|
39
|
+
|
|
40
|
+
No `read` command ever launches a runner. `screenshot`, `events` and `keepalive`
|
|
41
|
+
tell you there is no runner rather than quietly billing one, and so does `stop`,
|
|
42
|
+
since starting a pod in order to stop it would be absurd.
|
|
43
|
+
|
|
44
|
+
## The order that matters
|
|
45
|
+
|
|
46
|
+
A freshly launched runner has no screen. The virtual desktop starts with the
|
|
47
|
+
runner's **first run** and nothing else starts it, so until you have run
|
|
48
|
+
something:
|
|
49
|
+
|
|
50
|
+
- `screenshot` and `act` fail with exit code `2`, except `navigate`, which
|
|
51
|
+
fails with exit code `1` (`action-failed`): it skips the screen but still
|
|
52
|
+
needs the runner to have run something
|
|
53
|
+
- `exec` fails with exit code `4`
|
|
54
|
+
- `events recorder` reads as empty
|
|
55
|
+
|
|
56
|
+
None of that is a fault, and none of it clears on its own. **Only
|
|
57
|
+
`qawolf runner run <flow>` starts the screen.** A bare navigate does not: it
|
|
58
|
+
fails until the first run, however long you wait.
|
|
59
|
+
|
|
60
|
+
So the first call on a new runner has to be a run. That means a flow file and a
|
|
61
|
+
`package.json` on disk, even if all you want is to drive the browser by hand;
|
|
62
|
+
there is no "just give me a screen" call. Once one run has happened, the
|
|
63
|
+
screenshot-and-act loop below works for the rest of the runner's life.
|
|
64
|
+
|
|
65
|
+
Retry on the exit code, not on the message text:
|
|
66
|
+
|
|
67
|
+
- `4` is usually transient. The screen is up but cannot serve this instant:
|
|
68
|
+
restarting after a display-size change, or busy with another request. Retry in
|
|
69
|
+
a second or two — but bound the retries, because `4` also covers a runner that
|
|
70
|
+
was reaped after inactivity, which no amount of retrying brings back. If `4`
|
|
71
|
+
persists past a few tries, relaunch the id.
|
|
72
|
+
- `2` will not clear on its own. Either nothing has run on this runner yet, so
|
|
73
|
+
run a flow, or the runner has no browser at all, so launch with
|
|
74
|
+
`--name node20WithPlaywright` instead. The message says which.
|
|
75
|
+
|
|
76
|
+
The one exception is `exec`, which reports both as `4`; read its message to tell
|
|
77
|
+
them apart.
|
|
78
|
+
|
|
79
|
+
## Seeing and acting: the loop is yours
|
|
80
|
+
|
|
81
|
+
Two primitives, and you close the loop with your own model. There is no hosted
|
|
82
|
+
vision loop on this surface.
|
|
83
|
+
|
|
84
|
+
`qawolf runner screenshot --out page.jpg` writes a real JPEG to disk, decoded,
|
|
85
|
+
because every coding harness can open an image file. Read it with whatever
|
|
86
|
+
vision you have.
|
|
87
|
+
|
|
88
|
+
`qawolf runner act <action>` performs exactly one action per call, in the
|
|
89
|
+
computer-use tool vocabulary a vision model already emits: `click`,
|
|
90
|
+
`double_click`, `scroll`, `move`, `drag`, `keypress`, `navigate`, `type`. The
|
|
91
|
+
names and the field names are unchanged from that vocabulary on purpose, so you
|
|
92
|
+
can forward a tool call rather than translate it:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
echo '{"type":"click","button":"left","x":480,"y":260}' | qawolf runner act -
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Coordinates are pixels on the same screenshot you just read. The runner serves
|
|
99
|
+
one see-or-act request at a time, so decide what to do next from each answer
|
|
100
|
+
rather than firing several. Bounds are checked before anything is sent, so an
|
|
101
|
+
over-long `--text` or an out-of-range coordinate comes back immediately naming
|
|
102
|
+
the limit instead of occupying the runner and then failing.
|
|
103
|
+
|
|
104
|
+
`act`, `run` and `exec` are the three commands whose lost answer may still have
|
|
105
|
+
taken effect. On a `4` from `act`, take a screenshot before repeating a click.
|
|
106
|
+
`exec`'s message says the snippet could not be evaluated, but a lost answer
|
|
107
|
+
looks the same from outside, so treat a `4` from a snippet that changes something
|
|
108
|
+
as "may have run" rather than "did not run".
|
|
109
|
+
|
|
110
|
+
## The recorder: what you cannot get from pixels
|
|
111
|
+
|
|
112
|
+
`qawolf runner events recorder` is the capability that has no equivalent in a
|
|
113
|
+
screenshot. As you drive the browser, the runner records each interaction and
|
|
114
|
+
publishes `locator` (the real Playwright locator it resolved), `alternates` (the
|
|
115
|
+
others that matched the same element) and `code` (the generated Playwright call),
|
|
116
|
+
alongside `type`, `sourceUrl` and `timestamp`.
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
qawolf runner events recorder --tail 5 | jq -r '.code // .type' # what happened
|
|
120
|
+
qawolf runner events recorder --tail 5 | jq -r '[.locator] + (.alternates // []) | @tsv'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`code` is absent on events with no call of their own, such as a navigation, which
|
|
124
|
+
is why the first line falls back to `type`. Use these to turn a session you drove
|
|
125
|
+
by pixel coordinates into durable selectors, and to check that a click landed on
|
|
126
|
+
the element you meant rather than near it. The stream is empty until the session
|
|
127
|
+
has a browser context, so an early empty answer means "not yet", not "broken".
|
|
128
|
+
Do not add `--json` here: it wraps each line in an envelope and these field paths
|
|
129
|
+
stop matching.
|
|
130
|
+
|
|
131
|
+
## Reading the page: `exec`
|
|
132
|
+
|
|
133
|
+
`qawolf runner exec <file>` evaluates a snippet against whatever the runner's
|
|
134
|
+
browser is showing, which is how you read a value out of the page rather than
|
|
135
|
+
looking at it. Two things to know, because neither is guessable:
|
|
136
|
+
|
|
137
|
+
It does not return what the snippet evaluated to, only whether it ran. To get a
|
|
138
|
+
value back, print it and read the `console` stream. Print it behind a marker you
|
|
139
|
+
chose, and match on that rather than taking the newest line: the page logs to the
|
|
140
|
+
same stream, so anything it prints after your snippet would be what `--tail 1`
|
|
141
|
+
hands back. Entries carry `source`, which is `serverConsole` for your snippet and
|
|
142
|
+
`browserConsole` for the page, so filtering on both is what pins the value down.
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
echo 'console.log("qw-title:", await page.title())' | qawolf runner exec -
|
|
146
|
+
qawolf runner events console --tail 20 \
|
|
147
|
+
| jq -r 'select(.source == "serverConsole" and (.message | contains("qw-title:"))) | .message'
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
And the snippet imports nothing of yours by default. Pass `--file <path>` to
|
|
151
|
+
evaluate it in that file's scope, which also ships the directory's other files,
|
|
152
|
+
so the snippet can use your own page objects and helpers.
|
|
153
|
+
|
|
154
|
+
## Running a flow
|
|
155
|
+
|
|
156
|
+
`qawolf runner run <file>` ships the current directory's runnable files with the
|
|
157
|
+
request. The runner holds no copy of your project, so what runs is exactly what
|
|
158
|
+
is on disk at that moment, uncommitted edits included. A `package.json` has to
|
|
159
|
+
be there, since the run reads its npm dependencies from it, and the files may
|
|
160
|
+
carry at most 4 MiB in total: run from a directory holding the flow and what it
|
|
161
|
+
imports rather than from the root of a large monorepo. A missing file, a missing
|
|
162
|
+
`package.json` and files over the cap are all refused before any runner is
|
|
163
|
+
resolved or launched, so a typo costs nothing.
|
|
164
|
+
|
|
165
|
+
The call answers with a run id as soon as the run is accepted. **The outcome is
|
|
166
|
+
not in that answer**, it is in the `run-status` stream, whose entries carry
|
|
167
|
+
`runId`, `status` and an `errorMessage` when there is one.
|
|
168
|
+
|
|
169
|
+
**Pass `--follow` to `run` and let it wait for you.** It reports the run's
|
|
170
|
+
status — in progress, then passed or failed — and ends on the settled status.
|
|
171
|
+
Exit code `1` means the run did not pass. Three flags mirror more streams into
|
|
172
|
+
the follow, and each implies `--follow` on its own: `--logs` streams every log
|
|
173
|
+
line the run produces, `--run-events` streams the run's progress events as JSON
|
|
174
|
+
lines, and `--recorder-events` streams the browser actions the runner records
|
|
175
|
+
as JSON lines — the recorder is runner-wide rather than run-scoped, so that one
|
|
176
|
+
carries whatever is recorded after an anchor taken just before submission. Whatever mirrors are on, the
|
|
177
|
+
follow still ends on the status, never on them, so a run that prints nothing
|
|
178
|
+
still terminates the follow and a run that dies mid-sentence still reports how.
|
|
179
|
+
Combining mirror flags interleaves their lines with nothing saying which stream
|
|
180
|
+
a line came from — fine for eyeballs; when parsing, follow one stream at a time.
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
qawolf runner run flows/checkout.flow.ts --follow
|
|
184
|
+
qawolf runner run flows/checkout.flow.ts --follow --logs
|
|
185
|
+
qawolf runner run flows/checkout.flow.ts --follow --recorder-events
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
If you would rather submit and come back later, note that `--follow` on `events`
|
|
189
|
+
does not end when the run settles — it runs until its own `--timeout`, an hour
|
|
190
|
+
by default — so it cannot be used to wait for a run. Poll instead, and decide
|
|
191
|
+
with the same rule the CLI uses: `status` is `in-progress` while the run is
|
|
192
|
+
going, and any other value means it has settled.
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
qawolf runner run flows/checkout.flow.ts --json # -> {"runId":"...","runnerId":"..."}
|
|
196
|
+
qawolf runner events run-status --run <runId> --tail 1 | jq -r '.status'
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The one expensive mistake on this surface: **if `run` reports that the runner
|
|
200
|
+
could not be reached, that does not mean the run did not start.** The runner may
|
|
201
|
+
have accepted it and been too slow to answer, and resubmitting bills and journals
|
|
202
|
+
a second run.
|
|
203
|
+
|
|
204
|
+
There is no clean recovery here, so it is worth being plain about it. The journal
|
|
205
|
+
lives on the same pod, so while the runner stays unreachable a `run-status` read
|
|
206
|
+
fails the same way and cannot tell you whether a run is going. Wait for the
|
|
207
|
+
runner to answer again, then read `run-status` without `--run` and look at the
|
|
208
|
+
newest `runId`. Nothing ties that id back to your submission: `run` never
|
|
209
|
+
answered, so you have no id to match it against, and a runner takes work from
|
|
210
|
+
anyone addressing it. Treat the newest id as your run only if you know nothing
|
|
211
|
+
else submits to this runner; otherwise follow it to see what it is before acting
|
|
212
|
+
on it. An empty read is not proof the run did not start, though:
|
|
213
|
+
`run` returns the moment the run is accepted, and its first `run-status` entry
|
|
214
|
+
may not be written yet, so a run accepted just before the runner went quiet can
|
|
215
|
+
still be in flight with nothing to show. `runFlow` has no idempotency key, so a
|
|
216
|
+
resubmit always risks a second billed run. Prefer polling `run-status` a while
|
|
217
|
+
longer over resubmitting; only submit again once you are willing to accept that
|
|
218
|
+
risk.
|
|
219
|
+
|
|
220
|
+
## Reading history
|
|
221
|
+
|
|
222
|
+
Everything observable is an append-only stream on the pod, read by cursor or
|
|
223
|
+
tail rather than subscribed to, so attaching late still gets you the history that
|
|
224
|
+
is still there. It is not unbounded: a size cap drops the oldest entries on a
|
|
225
|
+
long-lived runner, and a `--tail N` read can stop early and hand back fewer than
|
|
226
|
+
N even when more matched. Both are warned about on stderr — dropped entries only
|
|
227
|
+
once a read holds a cursor, a stopped-early read with a pointer at `--since` —
|
|
228
|
+
so watch stderr, treat a short answer as "at least this" rather than "all there
|
|
229
|
+
was", and read what you care about as you go rather than at the end. QA Wolf writes `recorder`, `console`, `run-events`,
|
|
230
|
+
`run-logs` and `run-status`; a stream nobody has written reads as empty rather
|
|
231
|
+
than as an error, and a stream this CLI version does not know about is still
|
|
232
|
+
readable by name.
|
|
233
|
+
|
|
234
|
+
One payload per line, so shell tools compose:
|
|
235
|
+
|
|
236
|
+
```sh
|
|
237
|
+
qawolf runner events console --tail 20 | jq -r '.message'
|
|
238
|
+
qawolf runner events run-logs --run <runId> --follow > run.log
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`--tail N` takes the newest N, `--since <sequence>` reads everything after a
|
|
242
|
+
cursor, and `--run <id>` narrows the run-scoped streams.
|
|
243
|
+
|
|
244
|
+
`--follow` polls and prints as entries arrive. It is `tail -f` with a bound: it
|
|
245
|
+
ends only at its `--timeout` (an hour by default, exit `6`), because reading
|
|
246
|
+
keeps the runner alive and billing. Redirect it to a file and stop it yourself,
|
|
247
|
+
or use repeated `--since` reads when you need the command to end sooner.
|
|
248
|
+
|
|
249
|
+
Where it does win is the cursor. The pod reports how far a read scanned rather
|
|
250
|
+
than how far it matched, and `--follow` carries that number, so a filtered read
|
|
251
|
+
that matched nothing still moves forward. A caller paging by hand cannot see it,
|
|
252
|
+
because the CLI does not print it, and the best available substitute is the
|
|
253
|
+
highest `sequence` you actually saw. So a narrow `--run` filter over a busy
|
|
254
|
+
stream stalls: with nothing matching, there is no new `sequence` to move on to,
|
|
255
|
+
and you re-read the same window until something matches (NOVA-1397).
|
|
256
|
+
|
|
257
|
+
## Staying alive
|
|
258
|
+
|
|
259
|
+
A runner is reaped after a period of inactivity, and every command that talks to
|
|
260
|
+
the runner counts as activity, including a journal read.
|
|
261
|
+
`qawolf runner keepalive` exists for the gap that creates: a harness that thinks,
|
|
262
|
+
or waits on a human, for minutes between actions would otherwise come back to a
|
|
263
|
+
pod that is gone. It resets the clock and tells you the runner is still there.
|
|
264
|
+
|
|
265
|
+
It is listed as a `read`, but it is the one read with a cost: keeping the clock
|
|
266
|
+
reset keeps a billed pod alive. Call it while you are genuinely still working, not
|
|
267
|
+
on a timer you forget, and call `qawolf runner stop` when you are done rather
|
|
268
|
+
than leaving a pod to time out. A loop that keeps a runner alive and never stops
|
|
269
|
+
it bills until someone notices.
|
|
270
|
+
|
|
271
|
+
## End to end
|
|
272
|
+
|
|
273
|
+
Run from a directory holding a flow and a `package.json`. The run is what starts
|
|
274
|
+
the screen, so it is not optional even though the goal here is to drive by hand.
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
export QAWOLF_API_KEY=... # the only credential
|
|
278
|
+
export QAWOLF_RUNNER_ID=agent-1 # so no command below needs --runner
|
|
279
|
+
|
|
280
|
+
qawolf runner launch --id agent-1 --json # --id, not the variable; read .outcome
|
|
281
|
+
qawolf runner run flows/smoke.flow.ts --follow # starts the screen; exit 1 if it failed
|
|
282
|
+
|
|
283
|
+
qawolf runner act navigate --url https://example.com/login
|
|
284
|
+
qawolf runner screenshot --out page.jpg # then read page.jpg yourself
|
|
285
|
+
qawolf runner act click --button left --x 480 --y 260
|
|
286
|
+
qawolf runner act type --text "someone@example.com"
|
|
287
|
+
|
|
288
|
+
qawolf runner events recorder --tail 5 | jq -r '[.locator] + (.alternates // []) | @tsv'
|
|
289
|
+
qawolf runner stop
|
|
290
|
+
```
|