@qawolf/cli 1.8.1 → 1.9.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 +1146 -17
- package/package.json +2 -2
- package/skills/qawolf-cli/SKILL.md +55 -39
package/dist/cli.js
CHANGED
|
@@ -156391,6 +156391,60 @@ var installMessages = {
|
|
|
156391
156391
|
appiumNotFound: (appiumPath) => toolNotInstalled("Appium", appiumPath)
|
|
156392
156392
|
}
|
|
156393
156393
|
};
|
|
156394
|
+
// src/core/messages/interactiveRunner.ts
|
|
156395
|
+
var noRunnerId = "No runner id. Pass --runner, set QAWOLF_RUNNER_ID, or run qawolf runner launch first.";
|
|
156396
|
+
var interactiveRunnerMessages = {
|
|
156397
|
+
actionFailed: (errorMessage2) => `The action reached the runner and did not take effect: ${errorMessage2}`,
|
|
156398
|
+
actionFlagsWithStdin: '"-" reads the whole action from stdin, so an action flag passed alongside it would be ignored. Pipe the action in on its own, or name the action type and use flags.',
|
|
156399
|
+
actionMayHaveHappened: "The runner could not be reached, which does not mean the action was not performed: it may have stopped answering mid-action. Take a screenshot before repeating it.",
|
|
156400
|
+
actionNotJson: `Stdin did not hold a JSON action. Pipe one object, for example '{"type":"click","button":"left","x":480,"y":260}'.`,
|
|
156401
|
+
actionPerformed: (type) => `Performed ${type}.`,
|
|
156402
|
+
alreadyRunning: (id) => `Runner ${id} was already running.`,
|
|
156403
|
+
defaultNotRemembered: (id) => `Runner ${id} could not be written to .qawolf as this directory's default, so later commands will not find it on their own. Pass --runner ${id}, or set QAWOLF_RUNNER_ID=${id}.`,
|
|
156404
|
+
defaultNotForgotten: (id) => `Runner ${id} was stopped, but it could not be removed from .qawolf as this directory's default, so later commands will still be sent to it. Pass --runner, or launch a new runner.`,
|
|
156405
|
+
fileNotCollected: (path) => `"${path}" is not one of the files that travel to a runner. It must be inside the current directory and end in .ts, .tsx, .js, .mjs, .cjs or .json.`,
|
|
156406
|
+
fileUnreadable: (path, reason) => `${path} is one of the files that travel to a runner, but it could not be read: ${reason}. Move it out of the current directory, or make it readable.`,
|
|
156407
|
+
filesUnreadable: (reason) => `One of the files that travel to a runner could not be read: ${reason}.`,
|
|
156408
|
+
filesTooLarge: (byteLength, maxByteLength, largest) => `The collected files carry ${String(byteLength)} bytes, over the ${String(maxByteLength)} a single run may ship. The largest are ${largest.map((file) => `${file.path} (${String(file.byteLength)} bytes)`).join(", ")}. Every .ts, .tsx, .js, .mjs, .cjs and .json file under the current directory travels, build output included, so run from a directory holding only the flow and what it imports.`,
|
|
156409
|
+
keptAlive: (id) => `Runner ${id} is alive, and its inactivity clock has been reset.`,
|
|
156410
|
+
launchFailed: (id, error) => `Launching runner ${id} failed. ${error}`,
|
|
156411
|
+
launchLost: (id, error) => `Launching runner ${id} failed. ${error}. The request may still have reached QA Wolf and started a runner, so relaunch the same id with qawolf runner launch --id ${id} to attach to it rather than start a second one, or stop it with qawolf runner stop --runner ${id}.`,
|
|
156412
|
+
launched: (id) => `Launched runner ${id}.`,
|
|
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
noRunnerId,
|
|
156419
|
+
notRunning: (id) => `Runner ${id} was not running.`,
|
|
156420
|
+
runFailed: (errorMessage2) => errorMessage2 === undefined ? "The run failed and reported no reason." : `The run failed: ${errorMessage2}`,
|
|
156421
|
+
runPassed: "The run passed.",
|
|
156422
|
+
runSettledUnknown: (status) => `The run settled as "${status}", which this version of the CLI does not recognize. Upgrade to read it.`,
|
|
156423
|
+
runSubmitAnsweredUnknown: (outcome) => `The runner answered the submission with "${outcome}", which this version of the CLI does not recognize. Upgrade to read it.`,
|
|
156424
|
+
runSubmitted: (runId) => `Submitted run ${runId}.`,
|
|
156425
|
+
runnerHasNoScreen: "This runner does not run a browser on a virtual desktop, so there is nothing about it to see or drive. Retrying will never help: launch a node20WithPlaywright runner instead.",
|
|
156426
|
+
runnerHasNoScreenToEvaluate: "The runner could not evaluate the snippet. This covers a runner that is still starting or busy, and also one with no live page to evaluate against: a freshly launched runner has no page until a run opens one, so run a flow on it with qawolf runner run first. If it is not a runner that runs a browser at all, this will never clear. It is not proof the snippet did not run, so do not resubmit one that mutates the page without reading qawolf runner events console first.",
|
|
156427
|
+
runnerUnreachable: "The runner could not be reached. It may still be starting, or it may have terminated after inactivity. Retry, or launch it again.",
|
|
156428
|
+
screenNeedsARun: "This runner has not run anything yet, so its screen has never started. Waiting will not clear this and there is nothing to retry: run a flow on it with qawolf runner run, then ask again. Evaluating a snippet does not start a screen.",
|
|
156429
|
+
screenNotReady: "The runner has a screen and cannot serve this yet. Its virtual desktop restarts when a run changes the display size, and it serves one request at a time, so something already in flight is the usual reason. Retry in a second or two.",
|
|
156430
|
+
screenshotNotAnImage: "The screen was captured but did not arrive as a JPEG, so nothing was written. Nothing about the command needs changing: try it again, and report it if it keeps happening.",
|
|
156431
|
+
screenshotUnwritable: (path, detail) => `The screen was captured but could not be written to "${path}": ${detail}. Give --out a path this process can write to.`,
|
|
156432
|
+
screenshotWritten: (path) => `Wrote the runner's screen to ${path}.`,
|
|
156433
|
+
skippedEntries: (stream, count) => `${pluralize(count, "entry", "entries")} of ${stream} were dropped before this read: the runner keeps a bounded history and this follow has been overtaken.`,
|
|
156434
|
+
snippetEmpty: (path) => `"${path}" holds no code to evaluate.`,
|
|
156435
|
+
snippetErrored: (errorMessage2) => errorMessage2 === undefined ? "The snippet threw and reported no message." : `The snippet threw: ${errorMessage2}`,
|
|
156436
|
+
snippetFileUnreadable: (path) => `Could not read "${path}". Name a readable file, or "-" to read the snippet from stdin.`,
|
|
156437
|
+
snippetRan: "The snippet ran. Its value is not returned: read anything it printed with qawolf runner events console.",
|
|
156438
|
+
snippetStopped: "The snippet was interrupted before it finished. Anything it printed first is in qawolf runner events console.",
|
|
156439
|
+
stdinEmptyAction: 'Nothing arrived on stdin. Pipe the action in, or name the action type instead of "-".',
|
|
156440
|
+
stdinEmptySnippet: 'Nothing arrived on stdin. Pipe the snippet in, or name a file instead of "-".',
|
|
156441
|
+
stopped: (id) => `Stopped runner ${id}.`,
|
|
156442
|
+
requestTooLarge: (byteLength, maxByteLength) => `The run request encodes to ${String(byteLength)} bytes, over the ${String(maxByteLength)} a runner accepts. Escaping inflates file content, so a set of files inside the content cap can still encode to more than this. Run from a directory holding only the flow and what it imports.`,
|
|
156443
|
+
unknownStream: (stream, known) => `"${stream}" is not a stream QA Wolf writes, so this read has nothing to return. QA Wolf writes ${known.join(", ")}.`,
|
|
156444
|
+
unsearchedHistory: (stream) => `This read of ${stream} stopped before searching all of the runner's history, so older entries may exist that it did not look at. Page forwards with --since to reach them.`,
|
|
156445
|
+
submitMayHaveStarted: "The runner did not answer, which does not mean the run did not start. Read qawolf runner events run-status before running the flow again, and use the newest run id there if one appeared.",
|
|
156446
|
+
targetMismatch: (runnerName, requiredRunnerName) => `This flow needs a ${requiredRunnerName} runner and ${runnerName} is running. Launch one with --name ${requiredRunnerName}.`
|
|
156447
|
+
};
|
|
156394
156448
|
// src/core/messages/runner.ts
|
|
156395
156449
|
var runnerMessages = {
|
|
156396
156450
|
playwrightLoadFailed: (envDir, detail) => packageLoadFailed("Playwright", envDir, detail),
|
|
@@ -156543,7 +156597,8 @@ var exitCodes = {
|
|
|
156543
156597
|
invalidArgs: 2,
|
|
156544
156598
|
auth: 3,
|
|
156545
156599
|
network: 4,
|
|
156546
|
-
config: 5
|
|
156600
|
+
config: 5,
|
|
156601
|
+
timeout: 6
|
|
156547
156602
|
};
|
|
156548
156603
|
function exit(code, message, proc = process) {
|
|
156549
156604
|
if (message !== undefined && message.length > 0) {
|
|
@@ -171279,7 +171334,12 @@ async function requestWithRetry(args) {
|
|
|
171279
171334
|
const backoff = args.backoffMs[attempt];
|
|
171280
171335
|
const retryable = result.error.kind === "network" || result.error.kind === "timeout";
|
|
171281
171336
|
if (backoff === undefined || !retryable) {
|
|
171282
|
-
|
|
171337
|
+
const mayHaveArrived = result.error.kind !== "http";
|
|
171338
|
+
return {
|
|
171339
|
+
ok: false,
|
|
171340
|
+
...args.describe(result.error),
|
|
171341
|
+
...mayHaveArrived ? { mayHaveArrived } : {}
|
|
171342
|
+
};
|
|
171283
171343
|
}
|
|
171284
171344
|
await sleep2(backoff);
|
|
171285
171345
|
}
|
|
@@ -172427,7 +172487,6 @@ var browserActionSchema = exports_external.discriminatedUnion("type", [
|
|
|
172427
172487
|
type: exports_external.literal("type")
|
|
172428
172488
|
})
|
|
172429
172489
|
]);
|
|
172430
|
-
|
|
172431
172490
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/environment.js
|
|
172432
172491
|
var reservedRunEnvironmentVariablePrefix = "QAWOLF_";
|
|
172433
172492
|
function refusalForRunEnvironmentVariableName(name) {
|
|
@@ -172488,7 +172547,6 @@ var runFilePathSchema = exports_external.string().refine(isShippableRunFilePath,
|
|
|
172488
172547
|
error: runFilePathRefusal
|
|
172489
172548
|
});
|
|
172490
172549
|
var runPackageJsonPath = "package.json";
|
|
172491
|
-
|
|
172492
172550
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/identity.js
|
|
172493
172551
|
var runnerNameSchema = exports_external.enum([
|
|
172494
172552
|
"node20Basic",
|
|
@@ -172502,7 +172560,6 @@ var makeRunnerSchema = () => exports_external.object({
|
|
|
172502
172560
|
id: runnerIdSchema.describe("The runner's id, as supplied when launching it."),
|
|
172503
172561
|
runnerName: runnerNameSchema.describe("The image requested when the runner was launched. QA Wolf may place it on faster hardware, reported in gpuAccelerated.")
|
|
172504
172562
|
});
|
|
172505
|
-
|
|
172506
172563
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/journal.js
|
|
172507
172564
|
var journalEntrySchema = exports_external.object({
|
|
172508
172565
|
payload: exports_external.unknown(),
|
|
@@ -172537,7 +172594,6 @@ var readJournalResponseSchema = exports_external.object({
|
|
|
172537
172594
|
nextSequence: exports_external.number().int().nonnegative(),
|
|
172538
172595
|
oldestAvailableSequence: exports_external.number().int().nonnegative()
|
|
172539
172596
|
});
|
|
172540
|
-
|
|
172541
172597
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/payloadSize.js
|
|
172542
172598
|
var maxRunFilesByteLength = 4 * 1024 * 1024;
|
|
172543
172599
|
var maxRunnerRequestEncodedByteLength = 9 * 1024 * 1024;
|
|
@@ -172560,7 +172616,6 @@ var runFilesSchema = exports_external.record(exports_external.string(), exports_
|
|
|
172560
172616
|
}).refine((files) => runFilesByteLength(files) <= maxRunFilesByteLength, {
|
|
172561
172617
|
error: `The files in one request may carry at most ${maxRunFilesByteLength} bytes in total.`
|
|
172562
172618
|
});
|
|
172563
|
-
|
|
172564
172619
|
// node_modules/@qawolf/api-contracts/dist/v1/runner/screen.js
|
|
172565
172620
|
var runnerScreenNotReadySchema = exports_external.object({
|
|
172566
172621
|
outcome: exports_external.literal("screen-not-ready")
|
|
@@ -172666,6 +172721,7 @@ var makeEnvironmentResourceSchema = (ids) => resource({
|
|
|
172666
172721
|
})
|
|
172667
172722
|
]).optional().describe("The environment's flow code branch and its git reconciliation state, if it has one."),
|
|
172668
172723
|
id: ids.environment,
|
|
172724
|
+
isDefault: exports_external.boolean().describe("Whether this is the workspace's default environment."),
|
|
172669
172725
|
kind: exports_external.enum(["static", "preview"]).describe("Whether the environment is static or a preview."),
|
|
172670
172726
|
name: exports_external.string(),
|
|
172671
172727
|
runConcurrencyLimit: exports_external.string().describe('Maximum number of concurrent runs: a whole number, "unlimited", or a concurrency expression.'),
|
|
@@ -172681,6 +172737,7 @@ var makeFindEnvironmentsContract = (ids) => {
|
|
|
172681
172737
|
...makePaginationInputFields({ defaultLimit: 20, maxLimit: 100 })
|
|
172682
172738
|
});
|
|
172683
172739
|
const output = exports_external.object({
|
|
172740
|
+
defaultEnvironmentId: ids.environment.describe("The id of the workspace's default environment."),
|
|
172684
172741
|
environments: exports_external.array(makeEnvironmentResourceSchema(ids)).describe("The team's environments, newest first."),
|
|
172685
172742
|
nextCursor: nextCursorSchema
|
|
172686
172743
|
});
|
|
@@ -172754,7 +172811,7 @@ var makeSetEnvironmentVariableContract = (ids) => {
|
|
|
172754
172811
|
name: exports_external.string().describe("The stored variable name after whitespace is replaced with underscores and letters are uppercased.")
|
|
172755
172812
|
});
|
|
172756
172813
|
return {
|
|
172757
|
-
description: 'Create or replace an environment variable. If the user asks to create one for "my email" without naming it, use
|
|
172814
|
+
description: 'Create or replace an environment variable. If the user asks to create one for "my email" without naming it, use DEFAULT_EMAIL. The value is never returned.',
|
|
172758
172815
|
input,
|
|
172759
172816
|
kind: "write",
|
|
172760
172817
|
name: "environment.setVariable",
|
|
@@ -173056,8 +173113,50 @@ var makeGetRunContract = (ids) => {
|
|
|
173056
173113
|
const input = exports_external.object({
|
|
173057
173114
|
runId: ids.run
|
|
173058
173115
|
});
|
|
173116
|
+
const artifactUrls = {
|
|
173117
|
+
logsUrl: exports_external.url().optional().describe("Signed URL for the attempt's execution logs."),
|
|
173118
|
+
traceUrl: exports_external.url().optional().describe("Signed URL for the attempt's Playwright trace (a trace.zip; open it with `npx playwright show-trace`)."),
|
|
173119
|
+
videoUrl: exports_external.url().optional().describe("Signed URL for the attempt's screen recording.")
|
|
173120
|
+
};
|
|
173121
|
+
const automatedKind = exports_external.literal("automated").describe("The attempt ran on a QA Wolf runner.");
|
|
173122
|
+
const automatedAttempt = exports_external.discriminatedUnion("status", [
|
|
173123
|
+
exports_external.object({
|
|
173124
|
+
...artifactUrls,
|
|
173125
|
+
completedAt: exports_external.iso.datetime(),
|
|
173126
|
+
kind: automatedKind,
|
|
173127
|
+
startedAt: exports_external.iso.datetime(),
|
|
173128
|
+
status: exports_external.literal("passed")
|
|
173129
|
+
}),
|
|
173130
|
+
exports_external.object({
|
|
173131
|
+
...artifactUrls,
|
|
173132
|
+
completedAt: exports_external.iso.datetime(),
|
|
173133
|
+
kind: automatedKind,
|
|
173134
|
+
startedAt: exports_external.iso.datetime().optional().describe("Absent when the attempt failed before it could start."),
|
|
173135
|
+
status: exports_external.literal("failed")
|
|
173136
|
+
}),
|
|
173137
|
+
exports_external.object({
|
|
173138
|
+
completedAt: exports_external.iso.datetime().optional(),
|
|
173139
|
+
kind: automatedKind,
|
|
173140
|
+
startedAt: exports_external.iso.datetime().optional(),
|
|
173141
|
+
status: exports_external.literal("canceled").describe("The attempt was terminated without reaching a verdict.")
|
|
173142
|
+
})
|
|
173143
|
+
]);
|
|
173144
|
+
const manualAttempt = exports_external.object({
|
|
173145
|
+
completedAt: exports_external.iso.datetime(),
|
|
173146
|
+
kind: exports_external.literal("manual").describe("The attempt ran by hand in the Wolf Browser."),
|
|
173147
|
+
startedAt: exports_external.iso.datetime(),
|
|
173148
|
+
status: exports_external.literal("passed")
|
|
173149
|
+
});
|
|
173150
|
+
const attempt = exports_external.union([automatedAttempt, manualAttempt]);
|
|
173151
|
+
const attempts = exports_external.array(attempt).optional().describe("The flow's finished execution attempts, oldest first, including " + "manual Wolf Browser attempts. Present once at least one attempt has " + "finished, so a flow that passed after retries also lists its failed " + "attempts. Artifact URLs appear only on automated attempts that " + "reached a verdict, stay valid for at least a day (call run.get " + "again for fresh ones), and can return 404 when the attempt did not " + "produce that artifact.");
|
|
173152
|
+
const diagnosis = exports_external.object({
|
|
173153
|
+
issueId: ids.issue,
|
|
173154
|
+
type: exports_external.enum(["bug", "maintenance"])
|
|
173155
|
+
}).optional().describe("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. Pass issueId to " + "issue.get for details.");
|
|
173059
173156
|
const failedFlow = exports_external.object({
|
|
173157
|
+
attempts,
|
|
173060
173158
|
failure: exports_external.object({
|
|
173159
|
+
diagnosis,
|
|
173061
173160
|
error: exports_external.string()
|
|
173062
173161
|
}),
|
|
173063
173162
|
flowId: ids.flow,
|
|
@@ -173065,6 +173164,7 @@ var makeGetRunContract = (ids) => {
|
|
|
173065
173164
|
status: exports_external.literal("failed")
|
|
173066
173165
|
});
|
|
173067
173166
|
const nonFailedFlow = exports_external.object({
|
|
173167
|
+
attempts,
|
|
173068
173168
|
flowId: ids.flow,
|
|
173069
173169
|
name: exports_external.string(),
|
|
173070
173170
|
status: exports_external.enum(["queued", "running", "passed", "canceled"])
|
|
@@ -175510,6 +175610,8 @@ function createAgentRenderers() {
|
|
|
175510
175610
|
writeStderrLine(humanMessage);
|
|
175511
175611
|
},
|
|
175512
175612
|
gap: () => writeStderrLine(""),
|
|
175613
|
+
stream: (_data, line) => writeStdoutRaw(`${line}
|
|
175614
|
+
`),
|
|
175513
175615
|
write: (text) => writeStderrRaw(text),
|
|
175514
175616
|
withProgress: async (steps, done) => {
|
|
175515
175617
|
const results = [];
|
|
@@ -175545,6 +175647,8 @@ ${body}` : ""));
|
|
|
175545
175647
|
},
|
|
175546
175648
|
output: (_data, humanMessage) => clack.log.info(humanMessage),
|
|
175547
175649
|
gap: () => process.stderr.write(`
|
|
175650
|
+
`),
|
|
175651
|
+
stream: (_data, line) => writeStdoutRaw(`${line}
|
|
175548
175652
|
`),
|
|
175549
175653
|
write: (text) => writeStdoutRaw(text),
|
|
175550
175654
|
withProgress: async (steps, done) => {
|
|
@@ -175618,6 +175722,7 @@ function createJsonRenderers() {
|
|
|
175618
175722
|
error: (title, body) => writeJsonDiagnostic({ type: "error", title, body }),
|
|
175619
175723
|
output: (data, _humanMessage) => writeJsonLine(data),
|
|
175620
175724
|
gap: () => {},
|
|
175725
|
+
stream: (data, _line) => writeJsonLine(data),
|
|
175621
175726
|
write: () => {},
|
|
175622
175727
|
withProgress: async (steps, done) => {
|
|
175623
175728
|
const results = [];
|
|
@@ -175804,7 +175909,7 @@ function startUpdateCheck(deps) {
|
|
|
175804
175909
|
// package.json
|
|
175805
175910
|
var package_default = {
|
|
175806
175911
|
name: "@qawolf/cli",
|
|
175807
|
-
version: "1.
|
|
175912
|
+
version: "1.9.0",
|
|
175808
175913
|
description: "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
175809
175914
|
keywords: [
|
|
175810
175915
|
"automation",
|
|
@@ -175864,7 +175969,7 @@ var package_default = {
|
|
|
175864
175969
|
"@clack/prompts": "1.5.1",
|
|
175865
175970
|
"@napi-rs/keyring": "1.3.0",
|
|
175866
175971
|
"@oxc-node/core": "0.1.0",
|
|
175867
|
-
"@qawolf/api-contracts": "0.
|
|
175972
|
+
"@qawolf/api-contracts": "0.25.0",
|
|
175868
175973
|
"@qawolf/emails": "1.1.1",
|
|
175869
175974
|
"@qawolf/flow-targets": "1.0.0",
|
|
175870
175975
|
"@qawolf/flows": "0.1.4",
|
|
@@ -185988,16 +186093,29 @@ async function handlePublicApiCommand(ctx, spec, options) {
|
|
|
185988
186093
|
return;
|
|
185989
186094
|
}
|
|
185990
186095
|
|
|
186096
|
+
// src/domains/publicApi/skippedContracts.ts
|
|
186097
|
+
var handWrittenContractNames = new Set([
|
|
186098
|
+
"flow.list",
|
|
186099
|
+
"runner.evaluateSnippet",
|
|
186100
|
+
"runner.launch",
|
|
186101
|
+
"runner.performAction",
|
|
186102
|
+
"runner.readJournal",
|
|
186103
|
+
"runner.runFlow",
|
|
186104
|
+
"runner.stop",
|
|
186105
|
+
"runner.takeScreenshot"
|
|
186106
|
+
]);
|
|
186107
|
+
var unexpressibleContractNames = new Set([
|
|
186108
|
+
"issue.update"
|
|
186109
|
+
]);
|
|
186110
|
+
var skippedContractNames = new Set([
|
|
186111
|
+
...handWrittenContractNames,
|
|
186112
|
+
...unexpressibleContractNames
|
|
186113
|
+
]);
|
|
186114
|
+
|
|
185991
186115
|
// src/commands/publicApi/index.ts
|
|
185992
186116
|
var groupDescriptions = {
|
|
185993
186117
|
run: "Trigger and manage QA Wolf runs on the platform"
|
|
185994
186118
|
};
|
|
185995
|
-
var handWrittenContractNames = new Set(["flow.list"]);
|
|
185996
|
-
var skippedContractNames = new Set([
|
|
185997
|
-
...handWrittenContractNames,
|
|
185998
|
-
"issue.update",
|
|
185999
|
-
"runner.performAction"
|
|
186000
|
-
]);
|
|
186001
186119
|
var optionEnvironmentVariables = new Map([
|
|
186002
186120
|
["environmentId", "QAWOLF_ENVIRONMENT"],
|
|
186003
186121
|
["public.run.create.aiTaskId", "QAWOLF_AI_TASK_ID"]
|
|
@@ -186044,6 +186162,1016 @@ function registerPublicApiCommands(program2, signals, options = {}) {
|
|
|
186044
186162
|
}
|
|
186045
186163
|
}
|
|
186046
186164
|
|
|
186165
|
+
// src/core/interactiveRunner/runFiles.ts
|
|
186166
|
+
import { isAbsolute as isAbsolute5, relative as relative8, resolve as resolve11, sep as sep5 } from "node:path";
|
|
186167
|
+
function toCollectedPath(cwd, path9) {
|
|
186168
|
+
const absolute = isAbsolute5(path9) ? path9 : resolve11(cwd, path9);
|
|
186169
|
+
return relative8(cwd, absolute).split(sep5).join("/");
|
|
186170
|
+
}
|
|
186171
|
+
var namedLargestFileCount = 3;
|
|
186172
|
+
function largestFiles(files) {
|
|
186173
|
+
return Object.entries(files).map(([path9, content]) => ({
|
|
186174
|
+
byteLength: runFilesByteLength({ [path9]: content }),
|
|
186175
|
+
path: path9
|
|
186176
|
+
})).sort((a3, b4) => b4.byteLength - a3.byteLength).slice(0, namedLargestFileCount);
|
|
186177
|
+
}
|
|
186178
|
+
function checkPresenceAndSize(files, requiredPath) {
|
|
186179
|
+
if (!Object.hasOwn(files, requiredPath)) {
|
|
186180
|
+
return { path: requiredPath, type: "missing-file" };
|
|
186181
|
+
}
|
|
186182
|
+
const byteLength = runFilesByteLength(files);
|
|
186183
|
+
if (byteLength > maxRunFilesByteLength) {
|
|
186184
|
+
return {
|
|
186185
|
+
byteLength,
|
|
186186
|
+
largest: largestFiles(files),
|
|
186187
|
+
maxByteLength: maxRunFilesByteLength,
|
|
186188
|
+
type: "too-large"
|
|
186189
|
+
};
|
|
186190
|
+
}
|
|
186191
|
+
const encodedByteLength = Buffer.byteLength(JSON.stringify({ files, path: requiredPath }), "utf8");
|
|
186192
|
+
if (encodedByteLength > maxRunnerRequestEncodedByteLength) {
|
|
186193
|
+
return {
|
|
186194
|
+
byteLength: encodedByteLength,
|
|
186195
|
+
maxByteLength: maxRunnerRequestEncodedByteLength,
|
|
186196
|
+
type: "request-too-large"
|
|
186197
|
+
};
|
|
186198
|
+
}
|
|
186199
|
+
return { type: "ok" };
|
|
186200
|
+
}
|
|
186201
|
+
function checkRunFiles(files, entryPointPath) {
|
|
186202
|
+
const present = checkPresenceAndSize(files, entryPointPath);
|
|
186203
|
+
if (present.type !== "ok")
|
|
186204
|
+
return present;
|
|
186205
|
+
if (!Object.hasOwn(files, runPackageJsonPath)) {
|
|
186206
|
+
return { type: "missing-package-json" };
|
|
186207
|
+
}
|
|
186208
|
+
return { type: "ok" };
|
|
186209
|
+
}
|
|
186210
|
+
function checkSnippetFiles(files, contextFilePath) {
|
|
186211
|
+
return checkPresenceAndSize(files, contextFilePath);
|
|
186212
|
+
}
|
|
186213
|
+
function describeRunFilesCheck(check2) {
|
|
186214
|
+
switch (check2.type) {
|
|
186215
|
+
case "missing-file":
|
|
186216
|
+
return interactiveRunnerMessages.fileNotCollected(check2.path);
|
|
186217
|
+
case "missing-package-json":
|
|
186218
|
+
return interactiveRunnerMessages.missingPackageJson;
|
|
186219
|
+
case "too-large":
|
|
186220
|
+
return interactiveRunnerMessages.filesTooLarge(check2.byteLength, check2.maxByteLength, check2.largest);
|
|
186221
|
+
case "request-too-large":
|
|
186222
|
+
return interactiveRunnerMessages.requestTooLarge(check2.byteLength, check2.maxByteLength);
|
|
186223
|
+
}
|
|
186224
|
+
}
|
|
186225
|
+
|
|
186226
|
+
// src/domains/interactiveRunner/runnerIds.ts
|
|
186227
|
+
function parseRunnerId(id) {
|
|
186228
|
+
const parsed = runnerIdSchema.safeParse(id);
|
|
186229
|
+
if (!parsed.success)
|
|
186230
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
186231
|
+
return { id: parsed.data, ok: true };
|
|
186232
|
+
}
|
|
186233
|
+
function parseRunnerName(runnerName) {
|
|
186234
|
+
const parsed = runnerNameSchema.safeParse(runnerName);
|
|
186235
|
+
if (!parsed.success)
|
|
186236
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
186237
|
+
return { ok: true, runnerName: parsed.data };
|
|
186238
|
+
}
|
|
186239
|
+
|
|
186240
|
+
// src/domains/interactiveRunner/launch.ts
|
|
186241
|
+
async function launchRunner(ctx, options) {
|
|
186242
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.launch, {
|
|
186243
|
+
id: options.id,
|
|
186244
|
+
...options.runnerName ? { runnerName: options.runnerName } : {}
|
|
186245
|
+
});
|
|
186246
|
+
if (!result.ok) {
|
|
186247
|
+
return {
|
|
186248
|
+
error: result.error,
|
|
186249
|
+
exitCode: exitCodes.network,
|
|
186250
|
+
mayHaveArrived: result.mayHaveArrived ?? false,
|
|
186251
|
+
ok: false
|
|
186252
|
+
};
|
|
186253
|
+
}
|
|
186254
|
+
return { ok: true, value: result.value };
|
|
186255
|
+
}
|
|
186256
|
+
async function launchAndRemember(ctx, options, deps) {
|
|
186257
|
+
const priorDefault = await deps.store.readDefaultRunnerId().catch(() => {
|
|
186258
|
+
return;
|
|
186259
|
+
});
|
|
186260
|
+
const remembered = await deps.store.writeDefaultRunnerId(options.id).then(() => true).catch(() => {
|
|
186261
|
+
ctx.ui.warn(interactiveRunnerMessages.defaultNotRemembered(options.id));
|
|
186262
|
+
return false;
|
|
186263
|
+
});
|
|
186264
|
+
const launched = await launchRunner(ctx, options);
|
|
186265
|
+
if (launched.ok)
|
|
186266
|
+
return launched;
|
|
186267
|
+
if (remembered && !launched.mayHaveArrived) {
|
|
186268
|
+
await (priorDefault === undefined ? deps.store.clearDefaultRunnerId() : deps.store.writeDefaultRunnerId(priorDefault)).catch(() => {
|
|
186269
|
+
return;
|
|
186270
|
+
});
|
|
186271
|
+
}
|
|
186272
|
+
return {
|
|
186273
|
+
error: launched.mayHaveArrived ? interactiveRunnerMessages.launchLost(options.id, launched.error) : interactiveRunnerMessages.launchFailed(options.id, launched.error),
|
|
186274
|
+
exitCode: launched.exitCode,
|
|
186275
|
+
mayHaveArrived: launched.mayHaveArrived,
|
|
186276
|
+
ok: false
|
|
186277
|
+
};
|
|
186278
|
+
}
|
|
186279
|
+
async function handleRunnerLaunch(ctx, options, deps) {
|
|
186280
|
+
const chosenId = options.id ?? deps.makeRunnerId();
|
|
186281
|
+
const id = parseRunnerId(chosenId);
|
|
186282
|
+
if (!id.ok)
|
|
186283
|
+
return { error: id.error, exitCode: exitCodes.invalidArgs };
|
|
186284
|
+
const runnerName = options.name === undefined ? undefined : parseRunnerName(options.name);
|
|
186285
|
+
if (runnerName !== undefined && !runnerName.ok) {
|
|
186286
|
+
return { error: runnerName.error, exitCode: exitCodes.invalidArgs };
|
|
186287
|
+
}
|
|
186288
|
+
const launched = await launchAndRemember(ctx, { id: id.id, runnerName: runnerName?.runnerName }, deps);
|
|
186289
|
+
if (!launched.ok) {
|
|
186290
|
+
return { error: launched.error, exitCode: launched.exitCode };
|
|
186291
|
+
}
|
|
186292
|
+
ctx.ui.output(launched.value, launched.value.outcome === "launched" ? interactiveRunnerMessages.launched(launched.value.id) : interactiveRunnerMessages.alreadyRunning(launched.value.id));
|
|
186293
|
+
return;
|
|
186294
|
+
}
|
|
186295
|
+
|
|
186296
|
+
// src/domains/interactiveRunner/resolveRunner.ts
|
|
186297
|
+
var runnerIdEnvironmentVariable = "QAWOLF_RUNNER_ID";
|
|
186298
|
+
async function chooseRunnerId(runner, deps) {
|
|
186299
|
+
if (runner !== undefined)
|
|
186300
|
+
return runner;
|
|
186301
|
+
const fromEnvironment = deps.env[runnerIdEnvironmentVariable]?.trim();
|
|
186302
|
+
if (fromEnvironment)
|
|
186303
|
+
return fromEnvironment;
|
|
186304
|
+
return deps.store.readDefaultRunnerId();
|
|
186305
|
+
}
|
|
186306
|
+
async function resolveRunner(ctx, options, deps) {
|
|
186307
|
+
const chosen = await chooseRunnerId(options.runner, deps);
|
|
186308
|
+
if (chosen !== undefined) {
|
|
186309
|
+
const parsed = parseRunnerId(chosen);
|
|
186310
|
+
return parsed.ok ? { runnerId: parsed.id, type: "resolved" } : {
|
|
186311
|
+
error: parsed.error,
|
|
186312
|
+
exitCode: exitCodes.invalidArgs,
|
|
186313
|
+
type: "failed"
|
|
186314
|
+
};
|
|
186315
|
+
}
|
|
186316
|
+
if (!options.autoLaunch) {
|
|
186317
|
+
return {
|
|
186318
|
+
error: options.noRunnerIdMessage ?? interactiveRunnerMessages.noRunnerId,
|
|
186319
|
+
exitCode: exitCodes.invalidArgs,
|
|
186320
|
+
type: "failed"
|
|
186321
|
+
};
|
|
186322
|
+
}
|
|
186323
|
+
const launched = await launchAndRemember(ctx, { id: deps.makeRunnerId(), runnerName: undefined }, deps);
|
|
186324
|
+
if (!launched.ok) {
|
|
186325
|
+
return {
|
|
186326
|
+
error: launched.error,
|
|
186327
|
+
exitCode: launched.exitCode,
|
|
186328
|
+
type: "failed"
|
|
186329
|
+
};
|
|
186330
|
+
}
|
|
186331
|
+
return { runnerId: launched.value.id, type: "launched" };
|
|
186332
|
+
}
|
|
186333
|
+
function announceRunner(ctx, resolved) {
|
|
186334
|
+
if (resolved.type === "launched") {
|
|
186335
|
+
ctx.ui.info(interactiveRunnerMessages.launchedForCommand(resolved.runnerId));
|
|
186336
|
+
}
|
|
186337
|
+
}
|
|
186338
|
+
|
|
186339
|
+
// src/domains/interactiveRunner/evaluateSnippet.ts
|
|
186340
|
+
var stdinArgument = "-";
|
|
186341
|
+
async function resolveScope(contextFile, deps) {
|
|
186342
|
+
if (contextFile === undefined) {
|
|
186343
|
+
return { filePath: undefined, files: undefined, ok: true };
|
|
186344
|
+
}
|
|
186345
|
+
const filePath = toCollectedPath(deps.cwd, contextFile);
|
|
186346
|
+
const files = await deps.collectRunFiles();
|
|
186347
|
+
const check2 = checkSnippetFiles(files, filePath);
|
|
186348
|
+
if (check2.type !== "ok") {
|
|
186349
|
+
return { error: describeRunFilesCheck(check2), ok: false };
|
|
186350
|
+
}
|
|
186351
|
+
return { filePath, files, ok: true };
|
|
186352
|
+
}
|
|
186353
|
+
async function readCode(source, deps) {
|
|
186354
|
+
const code = source === stdinArgument ? await deps.readStdin() : await deps.readFile(source).catch(() => {
|
|
186355
|
+
return;
|
|
186356
|
+
});
|
|
186357
|
+
if (code === undefined) {
|
|
186358
|
+
return {
|
|
186359
|
+
error: interactiveRunnerMessages.snippetFileUnreadable(source),
|
|
186360
|
+
ok: false
|
|
186361
|
+
};
|
|
186362
|
+
}
|
|
186363
|
+
if (code.trim() === "") {
|
|
186364
|
+
return source === stdinArgument ? { error: interactiveRunnerMessages.stdinEmptySnippet, ok: false } : { error: interactiveRunnerMessages.snippetEmpty(source), ok: false };
|
|
186365
|
+
}
|
|
186366
|
+
return { code, ok: true };
|
|
186367
|
+
}
|
|
186368
|
+
async function handleRunnerExec(ctx, options, deps) {
|
|
186369
|
+
const code = await readCode(options.source, deps);
|
|
186370
|
+
if (!code.ok)
|
|
186371
|
+
return { error: code.error, exitCode: exitCodes.invalidArgs };
|
|
186372
|
+
const scope = await resolveScope(options.contextFile, deps);
|
|
186373
|
+
if (!scope.ok)
|
|
186374
|
+
return { error: scope.error, exitCode: exitCodes.invalidArgs };
|
|
186375
|
+
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
186376
|
+
if (resolved.type === "failed") {
|
|
186377
|
+
return { error: resolved.error, exitCode: resolved.exitCode };
|
|
186378
|
+
}
|
|
186379
|
+
announceRunner(ctx, resolved);
|
|
186380
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.evaluateSnippet, {
|
|
186381
|
+
code: code.code,
|
|
186382
|
+
id: resolved.runnerId,
|
|
186383
|
+
...scope.filePath === undefined ? {} : { filePath: scope.filePath },
|
|
186384
|
+
...scope.files === undefined ? {} : { files: scope.files }
|
|
186385
|
+
});
|
|
186386
|
+
if (!result.ok) {
|
|
186387
|
+
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
186388
|
+
}
|
|
186389
|
+
if (result.value.outcome === "runner-unreachable") {
|
|
186390
|
+
return {
|
|
186391
|
+
error: interactiveRunnerMessages.runnerHasNoScreenToEvaluate,
|
|
186392
|
+
exitCode: exitCodes.network
|
|
186393
|
+
};
|
|
186394
|
+
}
|
|
186395
|
+
switch (result.value.result) {
|
|
186396
|
+
case "success":
|
|
186397
|
+
ctx.ui.output({ outcome: "evaluated", result: "success" }, interactiveRunnerMessages.snippetRan);
|
|
186398
|
+
return;
|
|
186399
|
+
case "stopped":
|
|
186400
|
+
return {
|
|
186401
|
+
error: interactiveRunnerMessages.snippetStopped,
|
|
186402
|
+
exitCode: exitCodes.testFailure
|
|
186403
|
+
};
|
|
186404
|
+
case "error":
|
|
186405
|
+
return {
|
|
186406
|
+
error: interactiveRunnerMessages.snippetErrored(result.value.errorMessage),
|
|
186407
|
+
exitCode: exitCodes.testFailure
|
|
186408
|
+
};
|
|
186409
|
+
}
|
|
186410
|
+
}
|
|
186411
|
+
|
|
186412
|
+
// src/core/interactiveRunner/browserAction.ts
|
|
186413
|
+
function parseJsonPath(path9) {
|
|
186414
|
+
try {
|
|
186415
|
+
return { ok: true, value: JSON.parse(path9) };
|
|
186416
|
+
} catch {
|
|
186417
|
+
return {
|
|
186418
|
+
error: `--path must be a JSON array of points, for example '[{"x":10,"y":20},{"x":80,"y":90}]'.`,
|
|
186419
|
+
ok: false
|
|
186420
|
+
};
|
|
186421
|
+
}
|
|
186422
|
+
}
|
|
186423
|
+
function toNumber(value) {
|
|
186424
|
+
return value.trim() === "" ? Number.NaN : Number(value);
|
|
186425
|
+
}
|
|
186426
|
+
function buildBrowserAction(type, flags) {
|
|
186427
|
+
const parsedPath = flags.path === undefined ? undefined : parseJsonPath(flags.path);
|
|
186428
|
+
if (parsedPath !== undefined && !parsedPath.ok)
|
|
186429
|
+
return parsedPath;
|
|
186430
|
+
const candidate = {
|
|
186431
|
+
type,
|
|
186432
|
+
...flags.button === undefined ? {} : { button: flags.button },
|
|
186433
|
+
...flags.keys === undefined ? {} : { keys: flags.keys },
|
|
186434
|
+
...parsedPath === undefined ? {} : { path: parsedPath.value },
|
|
186435
|
+
...flags.scrollX === undefined ? {} : { scroll_x: toNumber(flags.scrollX) },
|
|
186436
|
+
...flags.scrollY === undefined ? {} : { scroll_y: toNumber(flags.scrollY) },
|
|
186437
|
+
...flags.text === undefined ? {} : { text: flags.text },
|
|
186438
|
+
...flags.url === undefined ? {} : { url: flags.url },
|
|
186439
|
+
...flags.x === undefined ? {} : { x: toNumber(flags.x) },
|
|
186440
|
+
...flags.y === undefined ? {} : { y: toNumber(flags.y) }
|
|
186441
|
+
};
|
|
186442
|
+
return parseBrowserAction(candidate);
|
|
186443
|
+
}
|
|
186444
|
+
function parseBrowserAction(candidate) {
|
|
186445
|
+
const parsed = browserActionSchema.safeParse(candidate);
|
|
186446
|
+
if (!parsed.success) {
|
|
186447
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
186448
|
+
}
|
|
186449
|
+
return { action: parsed.data, ok: true };
|
|
186450
|
+
}
|
|
186451
|
+
|
|
186452
|
+
// src/domains/interactiveRunner/performAction.ts
|
|
186453
|
+
var stdinArgument2 = "-";
|
|
186454
|
+
async function readAction(type, flags, deps) {
|
|
186455
|
+
if (type !== stdinArgument2)
|
|
186456
|
+
return buildBrowserAction(type, flags);
|
|
186457
|
+
if (Object.values(flags).some((value) => value !== undefined)) {
|
|
186458
|
+
return { error: interactiveRunnerMessages.actionFlagsWithStdin, ok: false };
|
|
186459
|
+
}
|
|
186460
|
+
const piped = (await deps.readStdin()).trim();
|
|
186461
|
+
if (piped === "") {
|
|
186462
|
+
return { error: interactiveRunnerMessages.stdinEmptyAction, ok: false };
|
|
186463
|
+
}
|
|
186464
|
+
try {
|
|
186465
|
+
return parseBrowserAction(JSON.parse(piped));
|
|
186466
|
+
} catch {
|
|
186467
|
+
return { error: interactiveRunnerMessages.actionNotJson, ok: false };
|
|
186468
|
+
}
|
|
186469
|
+
}
|
|
186470
|
+
async function handleRunnerAct(ctx, options, deps) {
|
|
186471
|
+
const built = await readAction(options.type, options.flags, deps);
|
|
186472
|
+
if (!built.ok)
|
|
186473
|
+
return { error: built.error, exitCode: exitCodes.invalidArgs };
|
|
186474
|
+
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
186475
|
+
if (resolved.type === "failed") {
|
|
186476
|
+
return { error: resolved.error, exitCode: resolved.exitCode };
|
|
186477
|
+
}
|
|
186478
|
+
announceRunner(ctx, resolved);
|
|
186479
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.performAction, { action: built.action, id: resolved.runnerId });
|
|
186480
|
+
if (!result.ok) {
|
|
186481
|
+
const fields = failureFields(result);
|
|
186482
|
+
return {
|
|
186483
|
+
...fields,
|
|
186484
|
+
...result.mayHaveArrived ? {
|
|
186485
|
+
error: `${fields.error} ${interactiveRunnerMessages.actionMayHaveHappened}`
|
|
186486
|
+
} : {},
|
|
186487
|
+
exitCode: exitCodes.network
|
|
186488
|
+
};
|
|
186489
|
+
}
|
|
186490
|
+
switch (result.value.outcome) {
|
|
186491
|
+
case "performed":
|
|
186492
|
+
ctx.ui.output({ action: built.action, outcome: "performed" }, interactiveRunnerMessages.actionPerformed(built.action.type));
|
|
186493
|
+
return;
|
|
186494
|
+
case "action-failed":
|
|
186495
|
+
return {
|
|
186496
|
+
error: interactiveRunnerMessages.actionFailed(result.value.errorMessage),
|
|
186497
|
+
exitCode: exitCodes.testFailure
|
|
186498
|
+
};
|
|
186499
|
+
case "screen-needs-a-run":
|
|
186500
|
+
return {
|
|
186501
|
+
error: interactiveRunnerMessages.screenNeedsARun,
|
|
186502
|
+
exitCode: exitCodes.invalidArgs
|
|
186503
|
+
};
|
|
186504
|
+
case "screen-not-ready":
|
|
186505
|
+
return {
|
|
186506
|
+
error: interactiveRunnerMessages.screenNotReady,
|
|
186507
|
+
exitCode: exitCodes.network
|
|
186508
|
+
};
|
|
186509
|
+
case "runner-has-no-screen":
|
|
186510
|
+
return {
|
|
186511
|
+
error: interactiveRunnerMessages.runnerHasNoScreen,
|
|
186512
|
+
exitCode: exitCodes.invalidArgs
|
|
186513
|
+
};
|
|
186514
|
+
case "runner-unreachable":
|
|
186515
|
+
return {
|
|
186516
|
+
error: interactiveRunnerMessages.actionMayHaveHappened,
|
|
186517
|
+
exitCode: exitCodes.network
|
|
186518
|
+
};
|
|
186519
|
+
}
|
|
186520
|
+
}
|
|
186521
|
+
|
|
186522
|
+
// src/domains/interactiveRunner/takeScreenshot.ts
|
|
186523
|
+
async function handleRunnerScreenshot(ctx, options, deps) {
|
|
186524
|
+
const resolved = await resolveRunner(ctx, {
|
|
186525
|
+
autoLaunch: false,
|
|
186526
|
+
noRunnerIdMessage: interactiveRunnerMessages.noRunnerIdForScreenshot,
|
|
186527
|
+
runner: options.runner
|
|
186528
|
+
}, deps);
|
|
186529
|
+
if (resolved.type === "failed") {
|
|
186530
|
+
return { error: resolved.error, exitCode: resolved.exitCode };
|
|
186531
|
+
}
|
|
186532
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.takeScreenshot, { id: resolved.runnerId });
|
|
186533
|
+
if (!result.ok) {
|
|
186534
|
+
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
186535
|
+
}
|
|
186536
|
+
switch (result.value.outcome) {
|
|
186537
|
+
case "captured": {
|
|
186538
|
+
const written = await deps.writeScreenshot({
|
|
186539
|
+
imageJpegBase64: result.value.imageJpegBase64,
|
|
186540
|
+
path: options.out
|
|
186541
|
+
});
|
|
186542
|
+
if (!written.ok) {
|
|
186543
|
+
return written.reason === "not-a-jpeg" ? {
|
|
186544
|
+
error: interactiveRunnerMessages.screenshotNotAnImage,
|
|
186545
|
+
exitCode: exitCodes.network
|
|
186546
|
+
} : {
|
|
186547
|
+
error: interactiveRunnerMessages.screenshotUnwritable(options.out, written.detail),
|
|
186548
|
+
exitCode: exitCodes.invalidArgs
|
|
186549
|
+
};
|
|
186550
|
+
}
|
|
186551
|
+
ctx.ui.output({ outcome: "captured", path: options.out }, interactiveRunnerMessages.screenshotWritten(options.out));
|
|
186552
|
+
return;
|
|
186553
|
+
}
|
|
186554
|
+
case "screen-needs-a-run":
|
|
186555
|
+
return {
|
|
186556
|
+
error: interactiveRunnerMessages.screenNeedsARun,
|
|
186557
|
+
exitCode: exitCodes.invalidArgs
|
|
186558
|
+
};
|
|
186559
|
+
case "screen-not-ready":
|
|
186560
|
+
return {
|
|
186561
|
+
error: interactiveRunnerMessages.screenNotReady,
|
|
186562
|
+
exitCode: exitCodes.network
|
|
186563
|
+
};
|
|
186564
|
+
case "runner-has-no-screen":
|
|
186565
|
+
return {
|
|
186566
|
+
error: interactiveRunnerMessages.runnerHasNoScreen,
|
|
186567
|
+
exitCode: exitCodes.invalidArgs
|
|
186568
|
+
};
|
|
186569
|
+
case "runner-unreachable":
|
|
186570
|
+
return {
|
|
186571
|
+
error: interactiveRunnerMessages.runnerUnreachable,
|
|
186572
|
+
exitCode: exitCodes.network
|
|
186573
|
+
};
|
|
186574
|
+
}
|
|
186575
|
+
}
|
|
186576
|
+
|
|
186577
|
+
// src/shell/interactiveRunner/collectRunFiles.ts
|
|
186578
|
+
import { join as join42 } from "node:path";
|
|
186579
|
+
async function collectRunFiles(options) {
|
|
186580
|
+
const paths = await glob(shippableRunFileExtensions.map((extension) => `**/*${extension}`), {
|
|
186581
|
+
cwd: options.cwd,
|
|
186582
|
+
followSymbolicLinks: false,
|
|
186583
|
+
ignore: ["**/node_modules/**"]
|
|
186584
|
+
});
|
|
186585
|
+
const shippable = paths.filter(isShippableRunFilePath).sort();
|
|
186586
|
+
const files = {};
|
|
186587
|
+
const read = batchMap(shippable, async (path9) => ({
|
|
186588
|
+
content: await options.fs.readFile(join42(options.cwd, path9)),
|
|
186589
|
+
path: path9
|
|
186590
|
+
}), flowBatchSize);
|
|
186591
|
+
for await (const file2 of read)
|
|
186592
|
+
files[file2.path] = file2.content;
|
|
186593
|
+
return files;
|
|
186594
|
+
}
|
|
186595
|
+
|
|
186596
|
+
// src/shell/interactiveRunner/makeRunnerId.ts
|
|
186597
|
+
import { randomUUID } from "node:crypto";
|
|
186598
|
+
function makeRunnerId() {
|
|
186599
|
+
return `cli-${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
186600
|
+
}
|
|
186601
|
+
|
|
186602
|
+
// src/shell/interactiveRunner/runnerStore.ts
|
|
186603
|
+
import { join as join43 } from "node:path";
|
|
186604
|
+
var storeFileName = "runner.json";
|
|
186605
|
+
var storeSchema = exports_external.object({ defaultRunnerId: exports_external.string().optional() });
|
|
186606
|
+
function parseJson(text3) {
|
|
186607
|
+
try {
|
|
186608
|
+
return JSON.parse(text3);
|
|
186609
|
+
} catch {
|
|
186610
|
+
return;
|
|
186611
|
+
}
|
|
186612
|
+
}
|
|
186613
|
+
function makeRunnerStore(options) {
|
|
186614
|
+
const directory = join43(options.cwd, qawolfDir);
|
|
186615
|
+
const path9 = join43(directory, storeFileName);
|
|
186616
|
+
let pendingWrites = 0;
|
|
186617
|
+
const nextPendingPath = () => `${path9}.${process.pid}.${++pendingWrites}.tmp`;
|
|
186618
|
+
return {
|
|
186619
|
+
async clearDefaultRunnerId() {
|
|
186620
|
+
await options.fs.rm(path9, { force: true });
|
|
186621
|
+
},
|
|
186622
|
+
async readDefaultRunnerId() {
|
|
186623
|
+
const contents2 = await options.fs.readFile(path9).catch(() => {
|
|
186624
|
+
return;
|
|
186625
|
+
});
|
|
186626
|
+
if (contents2 === undefined)
|
|
186627
|
+
return;
|
|
186628
|
+
const parsed = storeSchema.safeParse(parseJson(contents2));
|
|
186629
|
+
return parsed.success ? parsed.data.defaultRunnerId : undefined;
|
|
186630
|
+
},
|
|
186631
|
+
async writeDefaultRunnerId(runnerId) {
|
|
186632
|
+
const pendingPath = nextPendingPath();
|
|
186633
|
+
await options.fs.mkdir(directory, { recursive: true });
|
|
186634
|
+
await options.fs.writeFile(pendingPath, `${JSON.stringify({ defaultRunnerId: runnerId }, undefined, 2)}
|
|
186635
|
+
`);
|
|
186636
|
+
await options.fs.rename(pendingPath, path9);
|
|
186637
|
+
}
|
|
186638
|
+
};
|
|
186639
|
+
}
|
|
186640
|
+
|
|
186641
|
+
// src/shell/interactiveRunner/writeScreenshot.ts
|
|
186642
|
+
import { dirname as dirname14 } from "node:path";
|
|
186643
|
+
var jpegStartOfImage = [255, 216, 255];
|
|
186644
|
+
async function writeScreenshot(options) {
|
|
186645
|
+
const bytes = Buffer.from(options.imageJpegBase64, "base64");
|
|
186646
|
+
if (jpegStartOfImage.some((byte, index2) => bytes[index2] !== byte)) {
|
|
186647
|
+
return { ok: false, reason: "not-a-jpeg" };
|
|
186648
|
+
}
|
|
186649
|
+
try {
|
|
186650
|
+
await options.fs.mkdir(dirname14(options.path), { recursive: true });
|
|
186651
|
+
await options.fs.writeFile(options.path, bytes);
|
|
186652
|
+
} catch (error51) {
|
|
186653
|
+
return { detail: errorMessage(error51), ok: false, reason: "unwritable" };
|
|
186654
|
+
}
|
|
186655
|
+
return { ok: true };
|
|
186656
|
+
}
|
|
186657
|
+
|
|
186658
|
+
// src/shell/stdin.ts
|
|
186659
|
+
async function readStdin2() {
|
|
186660
|
+
if (process.stdin.isTTY === true)
|
|
186661
|
+
return "";
|
|
186662
|
+
const chunks = [];
|
|
186663
|
+
for await (const chunk of process.stdin)
|
|
186664
|
+
chunks.push(chunk);
|
|
186665
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
186666
|
+
}
|
|
186667
|
+
|
|
186668
|
+
// src/domains/interactiveRunner/deps.ts
|
|
186669
|
+
function makeInteractiveRunnerDeps(options) {
|
|
186670
|
+
return {
|
|
186671
|
+
collectRunFiles: () => collectRunFiles({ cwd: options.cwd, fs: options.fs }),
|
|
186672
|
+
cwd: options.cwd,
|
|
186673
|
+
env: options.env,
|
|
186674
|
+
makeRunnerId,
|
|
186675
|
+
readFile: (path9) => options.fs.readFile(path9),
|
|
186676
|
+
readStdin: readStdin2,
|
|
186677
|
+
sleep,
|
|
186678
|
+
store: makeRunnerStore({ cwd: options.cwd, fs: options.fs }),
|
|
186679
|
+
writeScreenshot: (screenshot) => writeScreenshot({ ...screenshot, fs: options.fs })
|
|
186680
|
+
};
|
|
186681
|
+
}
|
|
186682
|
+
|
|
186683
|
+
// src/commands/runner/context.ts
|
|
186684
|
+
var runnerFlagDescription = "Runner to target. Defaults to QAWOLF_RUNNER_ID, then this directory's stored runner";
|
|
186685
|
+
function runnerDeps(ctx) {
|
|
186686
|
+
return makeInteractiveRunnerDeps({
|
|
186687
|
+
cwd: process.cwd(),
|
|
186688
|
+
env: process.env,
|
|
186689
|
+
fs: ctx.fs
|
|
186690
|
+
});
|
|
186691
|
+
}
|
|
186692
|
+
|
|
186693
|
+
// src/commands/runner/interact.register.ts
|
|
186694
|
+
var defaultScreenshotPath = "screenshot.jpg";
|
|
186695
|
+
var screenshotExamples = `
|
|
186696
|
+
Examples:
|
|
186697
|
+
$ qawolf runner screenshot
|
|
186698
|
+
$ qawolf runner screenshot --out screens/step-3.jpg`;
|
|
186699
|
+
var actExamples = `
|
|
186700
|
+
Examples:
|
|
186701
|
+
$ qawolf runner act click --button left --x 480 --y 260
|
|
186702
|
+
$ qawolf runner act type --text "hello@example.com"
|
|
186703
|
+
$ qawolf runner act keypress --keys Control a
|
|
186704
|
+
$ qawolf runner act navigate --url https://example.com
|
|
186705
|
+
$ qawolf runner act drag --path '[{"x":10,"y":20},{"x":80,"y":90}]'
|
|
186706
|
+
$ echo '{"type":"click","button":"left","x":1,"y":2}' | qawolf runner act -`;
|
|
186707
|
+
var execExamples = `
|
|
186708
|
+
Examples:
|
|
186709
|
+
$ qawolf runner exec snippet.ts
|
|
186710
|
+
$ echo 'console.log(await page.title())' | qawolf runner exec -
|
|
186711
|
+
$ qawolf runner exec snippet.ts --file flows/checkout.flow.ts`;
|
|
186712
|
+
function registerRunnerInteractCommands(runner, signals) {
|
|
186713
|
+
declareCommandKind(runner.command("screenshot"), "read").description("Save a JPEG of an interactive runner's screen to a file").option("--out <path>", "File to write the image to", defaultScreenshotPath).option("--runner <id>", runnerFlagDescription).addHelpText("after", screenshotExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerScreenshot(ctx, { out: opts.out, runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
186714
|
+
declareCommandKind(runner.command("act <action>"), "write").description("Perform one raw action on a runner's screen: click, double_click, scroll, move, drag, keypress, navigate or type. Use - to read a whole action as JSON from stdin").option("--button <button>", "click: left, right, wheel, back or forward").option("--keys <keys...>", "keypress: modifiers and the key, e.g. Control a").option("--path <json>", "drag: JSON array of points to drag through").option("--runner <id>", runnerFlagDescription).option("--scroll-x <delta>", "scroll: horizontal wheel delta").option("--scroll-y <delta>", "scroll: vertical wheel delta").option("--text <text>", "type: the text to type").option("--url <url>", "navigate: the http or https URL to go to").option("--x <pixels>", "pointer x, in screenshot pixels").option("--y <pixels>", "pointer y, in screenshot pixels").addHelpText("after", actExamples).action((action2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerAct(ctx, {
|
|
186715
|
+
flags: {
|
|
186716
|
+
button: opts.button,
|
|
186717
|
+
keys: opts.keys,
|
|
186718
|
+
path: opts.path,
|
|
186719
|
+
scrollX: opts.scrollX,
|
|
186720
|
+
scrollY: opts.scrollY,
|
|
186721
|
+
text: opts.text,
|
|
186722
|
+
url: opts.url,
|
|
186723
|
+
x: opts.x,
|
|
186724
|
+
y: opts.y
|
|
186725
|
+
},
|
|
186726
|
+
runner: opts.runner,
|
|
186727
|
+
type: action2
|
|
186728
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
186729
|
+
declareCommandKind(runner.command("exec <file>"), "write").description("Evaluate a snippet against a runner's live page. Use - to read the snippet from stdin").option("--file <path>", "File whose scope the snippet is evaluated in; it and the directory's other files travel with it").option("--runner <id>", runnerFlagDescription).addHelpText("after", execExamples).action((file2, opts, command) => withAuthContext(signals, (ctx) => handleRunnerExec(ctx, { contextFile: opts.file, runner: opts.runner, source: file2 }, runnerDeps(ctx)))(opts, command));
|
|
186730
|
+
}
|
|
186731
|
+
|
|
186732
|
+
// src/domains/interactiveRunner/readJournal.ts
|
|
186733
|
+
function journalReadFailure(failed) {
|
|
186734
|
+
const { type: _type, ...failure } = failed;
|
|
186735
|
+
return failure;
|
|
186736
|
+
}
|
|
186737
|
+
var unreachableFailure = {
|
|
186738
|
+
error: interactiveRunnerMessages.runnerUnreachable,
|
|
186739
|
+
exitCode: exitCodes.network
|
|
186740
|
+
};
|
|
186741
|
+
async function readJournal(ctx, runnerId, request) {
|
|
186742
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.readJournal, {
|
|
186743
|
+
id: runnerId,
|
|
186744
|
+
stream: request.stream,
|
|
186745
|
+
...request.runId === undefined ? {} : { runId: request.runId },
|
|
186746
|
+
...request.sinceSequence === undefined ? {} : { sinceSequence: request.sinceSequence },
|
|
186747
|
+
...request.tail === undefined ? {} : { tail: request.tail }
|
|
186748
|
+
});
|
|
186749
|
+
if (!result.ok) {
|
|
186750
|
+
return {
|
|
186751
|
+
...failureFields(result),
|
|
186752
|
+
exitCode: exitCodes.network,
|
|
186753
|
+
type: "failed"
|
|
186754
|
+
};
|
|
186755
|
+
}
|
|
186756
|
+
if (result.value.outcome === "runner-unreachable") {
|
|
186757
|
+
return { type: "unreachable" };
|
|
186758
|
+
}
|
|
186759
|
+
return { type: "read", value: result.value };
|
|
186760
|
+
}
|
|
186761
|
+
|
|
186762
|
+
// src/domains/interactiveRunner/keepalive.ts
|
|
186763
|
+
async function handleRunnerKeepalive(ctx, options, deps) {
|
|
186764
|
+
const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
|
|
186765
|
+
if (resolved.type === "failed") {
|
|
186766
|
+
return { error: resolved.error, exitCode: resolved.exitCode };
|
|
186767
|
+
}
|
|
186768
|
+
const window2 = await readJournal(ctx, resolved.runnerId, {
|
|
186769
|
+
stream: "run-status",
|
|
186770
|
+
tail: 1
|
|
186771
|
+
});
|
|
186772
|
+
if (window2.type === "unreachable")
|
|
186773
|
+
return { ...unreachableFailure };
|
|
186774
|
+
if (window2.type === "failed")
|
|
186775
|
+
return window2;
|
|
186776
|
+
ctx.ui.output({ id: resolved.runnerId, outcome: "alive" }, interactiveRunnerMessages.keptAlive(resolved.runnerId));
|
|
186777
|
+
return;
|
|
186778
|
+
}
|
|
186779
|
+
|
|
186780
|
+
// src/domains/interactiveRunner/stop.ts
|
|
186781
|
+
async function handleRunnerStop(ctx, options, deps) {
|
|
186782
|
+
const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
|
|
186783
|
+
if (resolved.type === "failed") {
|
|
186784
|
+
return { error: resolved.error, exitCode: resolved.exitCode };
|
|
186785
|
+
}
|
|
186786
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.stop, {
|
|
186787
|
+
id: resolved.runnerId
|
|
186788
|
+
});
|
|
186789
|
+
if (!result.ok) {
|
|
186790
|
+
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
186791
|
+
}
|
|
186792
|
+
await deps.store.readDefaultRunnerId().then(async (storedDefault) => {
|
|
186793
|
+
if (storedDefault === resolved.runnerId) {
|
|
186794
|
+
await deps.store.clearDefaultRunnerId();
|
|
186795
|
+
}
|
|
186796
|
+
}).catch(() => {
|
|
186797
|
+
ctx.ui.warn(interactiveRunnerMessages.defaultNotForgotten(resolved.runnerId));
|
|
186798
|
+
});
|
|
186799
|
+
ctx.ui.output(result.value, result.value.outcome === "stopped" ? interactiveRunnerMessages.stopped(result.value.id) : interactiveRunnerMessages.notRunning(result.value.id));
|
|
186800
|
+
return;
|
|
186801
|
+
}
|
|
186802
|
+
|
|
186803
|
+
// src/commands/runner/lifecycle.register.ts
|
|
186804
|
+
var launchExamples = `
|
|
186805
|
+
Examples:
|
|
186806
|
+
$ qawolf runner launch
|
|
186807
|
+
$ qawolf runner launch --name node20WithAndroid
|
|
186808
|
+
$ qawolf runner launch --id ci`;
|
|
186809
|
+
var keepaliveExamples = `
|
|
186810
|
+
Examples:
|
|
186811
|
+
$ qawolf runner keepalive
|
|
186812
|
+
$ qawolf runner keepalive --runner ci`;
|
|
186813
|
+
function registerRunnerLifecycleCommands(runner, signals) {
|
|
186814
|
+
declareCommandKind(runner.command("launch"), "write").description("Launch an interactive runner and make it this directory's default").option("--id <id>", "Id to launch under. Relaunching an id attaches to that runner").option("--name <image>", "Runner image to run, e.g. node20WithPlaywright").addHelpText("after", launchExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerLaunch(ctx, { id: opts.id, name: opts.name }, runnerDeps(ctx)))(opts, command));
|
|
186815
|
+
declareCommandKind(runner.command("stop"), "write").description("Stop an interactive runner").option("--runner <id>", runnerFlagDescription).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerStop(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
186816
|
+
declareCommandKind(runner.command("keepalive"), "read").description("Reset a runner's inactivity clock, for a caller that pauses between actions").option("--runner <id>", runnerFlagDescription).addHelpText("after", keepaliveExamples).action((opts, command) => withAuthContext(signals, (ctx) => handleRunnerKeepalive(ctx, { runner: opts.runner }, runnerDeps(ctx)))(opts, command));
|
|
186817
|
+
}
|
|
186818
|
+
|
|
186819
|
+
// src/core/interactiveRunner/followTimeout.ts
|
|
186820
|
+
var defaultFollowTimeoutSeconds = 3600;
|
|
186821
|
+
var followTimeoutSchema = exports_external.coerce.number().int().positive();
|
|
186822
|
+
function parseFollowTimeout(seconds) {
|
|
186823
|
+
if (seconds === undefined) {
|
|
186824
|
+
return { ok: true, seconds: defaultFollowTimeoutSeconds };
|
|
186825
|
+
}
|
|
186826
|
+
const parsed = followTimeoutSchema.safeParse(seconds);
|
|
186827
|
+
if (!parsed.success)
|
|
186828
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
186829
|
+
return { ok: true, seconds: parsed.data };
|
|
186830
|
+
}
|
|
186831
|
+
|
|
186832
|
+
// src/core/interactiveRunner/journal.ts
|
|
186833
|
+
function formatJournalLine(entry, options) {
|
|
186834
|
+
const data2 = options.envelope ? entry : entry.payload;
|
|
186835
|
+
return { data: data2, line: JSON.stringify(data2) };
|
|
186836
|
+
}
|
|
186837
|
+
function countSkippedEntries(cursor3, oldestAvailableSequence) {
|
|
186838
|
+
if (oldestAvailableSequence <= cursor3 + 1)
|
|
186839
|
+
return 0;
|
|
186840
|
+
return oldestAvailableSequence - cursor3 - 1;
|
|
186841
|
+
}
|
|
186842
|
+
var runStatusPayloadSchema = exports_external.object({
|
|
186843
|
+
errorMessage: exports_external.string().optional(),
|
|
186844
|
+
runId: exports_external.string(),
|
|
186845
|
+
status: exports_external.string()
|
|
186846
|
+
});
|
|
186847
|
+
function readRunSettlement(payload) {
|
|
186848
|
+
const parsed = runStatusPayloadSchema.safeParse(payload);
|
|
186849
|
+
if (!parsed.success)
|
|
186850
|
+
return { type: "unreadable" };
|
|
186851
|
+
if (parsed.data.status === "in-progress")
|
|
186852
|
+
return { type: "in-progress" };
|
|
186853
|
+
return {
|
|
186854
|
+
errorMessage: parsed.data.errorMessage,
|
|
186855
|
+
status: parsed.data.status,
|
|
186856
|
+
type: "settled"
|
|
186857
|
+
};
|
|
186858
|
+
}
|
|
186859
|
+
var runLogPayloadSchema = exports_external.object({ message: exports_external.string() });
|
|
186860
|
+
function formatRunLogLine(payload) {
|
|
186861
|
+
const parsed = runLogPayloadSchema.safeParse(payload);
|
|
186862
|
+
return parsed.success ? parsed.data.message : JSON.stringify(payload);
|
|
186863
|
+
}
|
|
186864
|
+
|
|
186865
|
+
// src/domains/interactiveRunner/eventsOptions.ts
|
|
186866
|
+
var countSchema = exports_external.coerce.number().int().positive();
|
|
186867
|
+
var sequenceSchema = exports_external.coerce.number().int().nonnegative();
|
|
186868
|
+
function parseEventsOptions(options) {
|
|
186869
|
+
const parsed = exports_external.object({
|
|
186870
|
+
run: readJournalRequestSchema.shape.runId,
|
|
186871
|
+
since: sequenceSchema.optional(),
|
|
186872
|
+
stream: journalStreamSchema,
|
|
186873
|
+
tail: countSchema.optional()
|
|
186874
|
+
}).safeParse({
|
|
186875
|
+
stream: options.stream,
|
|
186876
|
+
...options.run === undefined ? {} : { run: options.run },
|
|
186877
|
+
...options.since === undefined ? {} : { since: options.since },
|
|
186878
|
+
...options.tail === undefined ? {} : { tail: options.tail }
|
|
186879
|
+
});
|
|
186880
|
+
if (!parsed.success)
|
|
186881
|
+
return { error: exports_external.prettifyError(parsed.error), ok: false };
|
|
186882
|
+
return {
|
|
186883
|
+
ok: true,
|
|
186884
|
+
value: {
|
|
186885
|
+
runId: parsed.data.run,
|
|
186886
|
+
sinceSequence: parsed.data.since,
|
|
186887
|
+
stream: parsed.data.stream,
|
|
186888
|
+
tail: parsed.data.tail
|
|
186889
|
+
}
|
|
186890
|
+
};
|
|
186891
|
+
}
|
|
186892
|
+
|
|
186893
|
+
// src/domains/interactiveRunner/journalCursor.ts
|
|
186894
|
+
function createJournalCursor(ctx, runnerId, request) {
|
|
186895
|
+
let sinceSequence = request.sinceSequence;
|
|
186896
|
+
let tail = request.tail;
|
|
186897
|
+
let warnedUnsearchedHistory = false;
|
|
186898
|
+
return async function read() {
|
|
186899
|
+
const window2 = await readJournal(ctx, runnerId, {
|
|
186900
|
+
...request,
|
|
186901
|
+
sinceSequence,
|
|
186902
|
+
tail
|
|
186903
|
+
});
|
|
186904
|
+
if (window2.type !== "read")
|
|
186905
|
+
return window2;
|
|
186906
|
+
if (sinceSequence !== undefined) {
|
|
186907
|
+
const skipped = countSkippedEntries(sinceSequence, window2.value.oldestAvailableSequence);
|
|
186908
|
+
if (skipped > 0) {
|
|
186909
|
+
ctx.ui.warn(interactiveRunnerMessages.skippedEntries(request.stream, skipped));
|
|
186910
|
+
}
|
|
186911
|
+
}
|
|
186912
|
+
if (window2.value.hasUnsearchedHistory && !warnedUnsearchedHistory) {
|
|
186913
|
+
warnedUnsearchedHistory = true;
|
|
186914
|
+
ctx.ui.warn(interactiveRunnerMessages.unsearchedHistory(request.stream));
|
|
186915
|
+
}
|
|
186916
|
+
tail = undefined;
|
|
186917
|
+
sinceSequence = Math.max(sinceSequence ?? 0, window2.value.nextSequence);
|
|
186918
|
+
return { entries: window2.value.entries, type: "entries" };
|
|
186919
|
+
};
|
|
186920
|
+
}
|
|
186921
|
+
var unreachableGraceMs = 60000;
|
|
186922
|
+
function createUnreachableBudget(pollIntervalMs2) {
|
|
186923
|
+
const limit = Math.ceil(unreachableGraceMs / pollIntervalMs2);
|
|
186924
|
+
let consecutive = 0;
|
|
186925
|
+
return {
|
|
186926
|
+
exhausted: () => ++consecutive >= limit,
|
|
186927
|
+
reset: () => {
|
|
186928
|
+
consecutive = 0;
|
|
186929
|
+
}
|
|
186930
|
+
};
|
|
186931
|
+
}
|
|
186932
|
+
|
|
186933
|
+
// src/domains/interactiveRunner/events.ts
|
|
186934
|
+
var pollIntervalMs2 = 1000;
|
|
186935
|
+
async function handleRunnerEvents(ctx, options, deps) {
|
|
186936
|
+
const parsed = parseEventsOptions(options);
|
|
186937
|
+
if (!parsed.ok)
|
|
186938
|
+
return { error: parsed.error, exitCode: exitCodes.invalidArgs };
|
|
186939
|
+
const timeout = parseFollowTimeout(options.timeout);
|
|
186940
|
+
if (!timeout.ok) {
|
|
186941
|
+
return { error: timeout.error, exitCode: exitCodes.invalidArgs };
|
|
186942
|
+
}
|
|
186943
|
+
const resolved = await resolveRunner(ctx, { autoLaunch: false, runner: options.runner }, deps);
|
|
186944
|
+
if (resolved.type === "failed") {
|
|
186945
|
+
return { error: resolved.error, exitCode: resolved.exitCode };
|
|
186946
|
+
}
|
|
186947
|
+
const known = knownJournalStreams;
|
|
186948
|
+
if (!known.includes(parsed.value.stream)) {
|
|
186949
|
+
ctx.ui.warn(interactiveRunnerMessages.unknownStream(parsed.value.stream, known));
|
|
186950
|
+
}
|
|
186951
|
+
const read = createJournalCursor(ctx, resolved.runnerId, parsed.value);
|
|
186952
|
+
const unreachable = createUnreachableBudget(pollIntervalMs2);
|
|
186953
|
+
const maxPolls = Math.max(1, Math.ceil(timeout.seconds * 1000 / pollIntervalMs2));
|
|
186954
|
+
for (let poll = 1;; poll++) {
|
|
186955
|
+
const window2 = await read();
|
|
186956
|
+
if (window2.type === "failed")
|
|
186957
|
+
return journalReadFailure(window2);
|
|
186958
|
+
if (window2.type === "unreachable") {
|
|
186959
|
+
if (!options.follow || unreachable.exhausted()) {
|
|
186960
|
+
return { ...unreachableFailure };
|
|
186961
|
+
}
|
|
186962
|
+
} else {
|
|
186963
|
+
unreachable.reset();
|
|
186964
|
+
for (const entry of window2.entries) {
|
|
186965
|
+
const { data: data2, line } = formatJournalLine(entry, {
|
|
186966
|
+
envelope: options.envelope
|
|
186967
|
+
});
|
|
186968
|
+
ctx.ui.stream(data2, line);
|
|
186969
|
+
}
|
|
186970
|
+
if (!options.follow)
|
|
186971
|
+
return;
|
|
186972
|
+
}
|
|
186973
|
+
if (poll >= maxPolls) {
|
|
186974
|
+
return {
|
|
186975
|
+
error: interactiveRunnerMessages.followEventsTimedOut(parsed.value.stream, timeout.seconds),
|
|
186976
|
+
exitCode: exitCodes.timeout
|
|
186977
|
+
};
|
|
186978
|
+
}
|
|
186979
|
+
await deps.sleep(pollIntervalMs2);
|
|
186980
|
+
}
|
|
186981
|
+
}
|
|
186982
|
+
|
|
186983
|
+
// src/domains/interactiveRunner/collectFiles.ts
|
|
186984
|
+
async function collectRunFiles2(deps) {
|
|
186985
|
+
try {
|
|
186986
|
+
return { files: await deps.collectRunFiles(), ok: true };
|
|
186987
|
+
} catch (error51) {
|
|
186988
|
+
const path9 = readErrorPath(error51);
|
|
186989
|
+
return {
|
|
186990
|
+
error: path9 === undefined ? interactiveRunnerMessages.filesUnreadable(errorMessage(error51)) : interactiveRunnerMessages.fileUnreadable(path9, errorMessage(error51)),
|
|
186991
|
+
ok: false
|
|
186992
|
+
};
|
|
186993
|
+
}
|
|
186994
|
+
}
|
|
186995
|
+
function readErrorPath(error51) {
|
|
186996
|
+
if (typeof error51 !== "object" || error51 === null)
|
|
186997
|
+
return;
|
|
186998
|
+
const path9 = error51.path;
|
|
186999
|
+
return typeof path9 === "string" ? path9 : undefined;
|
|
187000
|
+
}
|
|
187001
|
+
|
|
187002
|
+
// src/domains/interactiveRunner/followRun.ts
|
|
187003
|
+
var pollIntervalMs3 = 1000;
|
|
187004
|
+
function findSettlement(entries) {
|
|
187005
|
+
for (const entry of entries) {
|
|
187006
|
+
const settlement = readRunSettlement(entry.payload);
|
|
187007
|
+
if (settlement.type !== "settled")
|
|
187008
|
+
continue;
|
|
187009
|
+
if (settlement.status === "passed")
|
|
187010
|
+
return { type: "passed" };
|
|
187011
|
+
if (settlement.status === "failed") {
|
|
187012
|
+
return { errorMessage: settlement.errorMessage, type: "failed" };
|
|
187013
|
+
}
|
|
187014
|
+
return { status: settlement.status, type: "unrecognized" };
|
|
187015
|
+
}
|
|
187016
|
+
return;
|
|
187017
|
+
}
|
|
187018
|
+
function reportSettlement(ctx, settlement) {
|
|
187019
|
+
if (settlement.type === "passed") {
|
|
187020
|
+
ctx.ui.success(interactiveRunnerMessages.runPassed);
|
|
187021
|
+
return;
|
|
187022
|
+
}
|
|
187023
|
+
return {
|
|
187024
|
+
error: settlement.type === "failed" ? interactiveRunnerMessages.runFailed(settlement.errorMessage) : interactiveRunnerMessages.runSettledUnknown(settlement.status),
|
|
187025
|
+
exitCode: exitCodes.testFailure
|
|
187026
|
+
};
|
|
187027
|
+
}
|
|
187028
|
+
async function followRun(ctx, options, deps) {
|
|
187029
|
+
const readLogs = createJournalCursor(ctx, options.runnerId, {
|
|
187030
|
+
runId: options.runId,
|
|
187031
|
+
stream: "run-logs"
|
|
187032
|
+
});
|
|
187033
|
+
const readStatus = createJournalCursor(ctx, options.runnerId, {
|
|
187034
|
+
runId: options.runId,
|
|
187035
|
+
stream: "run-status"
|
|
187036
|
+
});
|
|
187037
|
+
const unreachable = createUnreachableBudget(pollIntervalMs3);
|
|
187038
|
+
const printLogs = async () => {
|
|
187039
|
+
const logs = await readLogs();
|
|
187040
|
+
if (logs.type !== "entries")
|
|
187041
|
+
return logs;
|
|
187042
|
+
for (const entry of logs.entries) {
|
|
187043
|
+
ctx.ui.stream(entry, formatRunLogLine(entry.payload));
|
|
187044
|
+
}
|
|
187045
|
+
return logs;
|
|
187046
|
+
};
|
|
187047
|
+
const maxPolls = Math.max(1, Math.ceil(options.timeoutSeconds * 1000 / pollIntervalMs3));
|
|
187048
|
+
for (let poll = 1;; poll++) {
|
|
187049
|
+
const logs = await printLogs();
|
|
187050
|
+
if (logs.type === "failed")
|
|
187051
|
+
return journalReadFailure(logs);
|
|
187052
|
+
const status = logs.type === "unreachable" ? logs : await readStatus();
|
|
187053
|
+
if (status.type === "failed")
|
|
187054
|
+
return journalReadFailure(status);
|
|
187055
|
+
if (logs.type === "unreachable" || status.type === "unreachable") {
|
|
187056
|
+
if (unreachable.exhausted())
|
|
187057
|
+
return { ...unreachableFailure };
|
|
187058
|
+
} else {
|
|
187059
|
+
unreachable.reset();
|
|
187060
|
+
const settlement = findSettlement(status.entries);
|
|
187061
|
+
if (settlement !== undefined) {
|
|
187062
|
+
await printLogs();
|
|
187063
|
+
return reportSettlement(ctx, settlement);
|
|
187064
|
+
}
|
|
187065
|
+
}
|
|
187066
|
+
if (poll >= maxPolls) {
|
|
187067
|
+
return {
|
|
187068
|
+
error: interactiveRunnerMessages.followTimedOut(options.runId, options.runnerId, options.timeoutSeconds),
|
|
187069
|
+
exitCode: exitCodes.timeout
|
|
187070
|
+
};
|
|
187071
|
+
}
|
|
187072
|
+
await deps.sleep(pollIntervalMs3);
|
|
187073
|
+
}
|
|
187074
|
+
}
|
|
187075
|
+
|
|
187076
|
+
// src/domains/interactiveRunner/runFlow.ts
|
|
187077
|
+
async function handleRunnerRun(ctx, options, deps) {
|
|
187078
|
+
const timeout = parseFollowTimeout(options.timeout);
|
|
187079
|
+
if (!timeout.ok) {
|
|
187080
|
+
return { error: timeout.error, exitCode: exitCodes.invalidArgs };
|
|
187081
|
+
}
|
|
187082
|
+
const entryPointPath = toCollectedPath(deps.cwd, options.entryPoint);
|
|
187083
|
+
const collected = await collectRunFiles2(deps);
|
|
187084
|
+
if (!collected.ok) {
|
|
187085
|
+
return { error: collected.error, exitCode: exitCodes.config };
|
|
187086
|
+
}
|
|
187087
|
+
const files = collected.files;
|
|
187088
|
+
const check2 = checkRunFiles(files, entryPointPath);
|
|
187089
|
+
if (check2.type !== "ok") {
|
|
187090
|
+
return {
|
|
187091
|
+
error: describeRunFilesCheck(check2),
|
|
187092
|
+
exitCode: exitCodes.invalidArgs
|
|
187093
|
+
};
|
|
187094
|
+
}
|
|
187095
|
+
const resolved = await resolveRunner(ctx, { autoLaunch: true, runner: options.runner }, deps);
|
|
187096
|
+
if (resolved.type === "failed") {
|
|
187097
|
+
return { error: resolved.error, exitCode: resolved.exitCode };
|
|
187098
|
+
}
|
|
187099
|
+
announceRunner(ctx, resolved);
|
|
187100
|
+
const result = await ctx.platformClient.callPublicApi(publicContractsV1.runner.runFlow, { entryPointPath, files, id: resolved.runnerId });
|
|
187101
|
+
if (!result.ok) {
|
|
187102
|
+
return { ...failureFields(result), exitCode: exitCodes.network };
|
|
187103
|
+
}
|
|
187104
|
+
switch (result.value.outcome) {
|
|
187105
|
+
case "runner-target-mismatch":
|
|
187106
|
+
return {
|
|
187107
|
+
error: interactiveRunnerMessages.targetMismatch(result.value.runnerName, result.value.requiredRunnerName),
|
|
187108
|
+
exitCode: exitCodes.invalidArgs
|
|
187109
|
+
};
|
|
187110
|
+
case "runner-unreachable":
|
|
187111
|
+
return {
|
|
187112
|
+
error: interactiveRunnerMessages.submitMayHaveStarted,
|
|
187113
|
+
exitCode: exitCodes.network
|
|
187114
|
+
};
|
|
187115
|
+
case "submitted": {
|
|
187116
|
+
const runId = result.value.runId;
|
|
187117
|
+
if (!options.follow) {
|
|
187118
|
+
ctx.ui.output({ runId, runnerId: resolved.runnerId }, interactiveRunnerMessages.runSubmitted(runId));
|
|
187119
|
+
return;
|
|
187120
|
+
}
|
|
187121
|
+
ctx.ui.info(interactiveRunnerMessages.runSubmitted(runId));
|
|
187122
|
+
return followRun(ctx, {
|
|
187123
|
+
runId,
|
|
187124
|
+
runnerId: resolved.runnerId,
|
|
187125
|
+
timeoutSeconds: timeout.seconds
|
|
187126
|
+
}, deps);
|
|
187127
|
+
}
|
|
187128
|
+
default: {
|
|
187129
|
+
result.value;
|
|
187130
|
+
return {
|
|
187131
|
+
error: interactiveRunnerMessages.runSubmitAnsweredUnknown(String(result.value.outcome)),
|
|
187132
|
+
exitCode: exitCodes.network
|
|
187133
|
+
};
|
|
187134
|
+
}
|
|
187135
|
+
}
|
|
187136
|
+
}
|
|
187137
|
+
|
|
187138
|
+
// src/commands/runner/run.register.ts
|
|
187139
|
+
var runExamples2 = `
|
|
187140
|
+
Examples:
|
|
187141
|
+
$ qawolf runner run flows/checkout.flow.ts
|
|
187142
|
+
$ qawolf runner run flows/checkout.flow.ts --follow`;
|
|
187143
|
+
var eventsExamples = `
|
|
187144
|
+
Examples:
|
|
187145
|
+
$ qawolf runner events recorder --tail 5
|
|
187146
|
+
$ qawolf runner events run-logs --run <runId> --follow
|
|
187147
|
+
$ qawolf runner events console --since 120 --json`;
|
|
187148
|
+
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", "Stream the run's logs until it settles", 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
|
+
entryPoint: file2,
|
|
187151
|
+
follow: opts.follow,
|
|
187152
|
+
runner: opts.runner,
|
|
187153
|
+
timeout: opts.timeout
|
|
187154
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
187155
|
+
declareCommandKind(runner.command("events <stream>"), "read").description(`Print a runner's journal, one entry per line. QA Wolf writes ${knownJournalStreams.join(", ")}`).option("--follow", "Keep reading as new entries arrive. Reading counts as activity, so a follow left open keeps the runner alive and billing", false).option("--run <id>", "Restrict run-scoped streams to one run").option("--runner <id>", runnerFlagDescription).option("--since <sequence>", "Read entries after this sequence").option("--tail <count>", "Read only the newest <count> entries").option("--timeout <seconds>", "Give up following after this long. Reading keeps the runner alive, so a follow left open would otherwise bill until the terminal closed", String(defaultFollowTimeoutSeconds)).addHelpText("after", eventsExamples).action((stream, opts, command) => withAuthContext(signals, (ctx) => handleRunnerEvents(ctx, {
|
|
187156
|
+
envelope: Boolean(command.optsWithGlobals().json),
|
|
187157
|
+
follow: opts.follow,
|
|
187158
|
+
run: opts.run,
|
|
187159
|
+
runner: opts.runner,
|
|
187160
|
+
since: opts.since,
|
|
187161
|
+
stream,
|
|
187162
|
+
tail: opts.tail,
|
|
187163
|
+
timeout: opts.timeout
|
|
187164
|
+
}, runnerDeps(ctx)))(opts, command));
|
|
187165
|
+
}
|
|
187166
|
+
|
|
187167
|
+
// src/commands/runner/index.ts
|
|
187168
|
+
function registerRunnerCommand(program2, signals) {
|
|
187169
|
+
const runner = program2.command("runner").description("Drive an interactive runner on the QA Wolf platform");
|
|
187170
|
+
registerRunnerLifecycleCommands(runner, signals);
|
|
187171
|
+
registerRunnerRunCommands(runner, signals);
|
|
187172
|
+
registerRunnerInteractCommands(runner, signals);
|
|
187173
|
+
}
|
|
187174
|
+
|
|
186047
187175
|
// src/commands/program.ts
|
|
186048
187176
|
function createProgram({
|
|
186049
187177
|
signals
|
|
@@ -186056,6 +187184,7 @@ function createProgram({
|
|
|
186056
187184
|
registerFlowsCommand(program2, signals);
|
|
186057
187185
|
registerInitCommand(program2, signals);
|
|
186058
187186
|
registerInstallCommand(program2, signals);
|
|
187187
|
+
registerRunnerCommand(program2, signals);
|
|
186059
187188
|
registerPublicApiCommands(program2, signals);
|
|
186060
187189
|
return program2;
|
|
186061
187190
|
}
|
|
@@ -186079,4 +187208,4 @@ createProgram({ signals }).parseAsync().catch(() => {
|
|
|
186079
187208
|
process.exitCode = 1;
|
|
186080
187209
|
}).finally(() => flushAndExit(typeof process.exitCode === "number" ? process.exitCode : 0));
|
|
186081
187210
|
|
|
186082
|
-
//# debugId=
|
|
187211
|
+
//# debugId=B4115D163429543264756E2164756E21
|