@qawolf/cli 1.29.0 → 1.30.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 +1996 -499
- package/dist/runner-sdk.js +60 -72
- package/dist/types/runnerSdk/types.d.ts +2 -0
- package/package.json +2 -2
- package/skills/qawolf-cli/SKILL.md +56 -2
- package/skills/qawolf-cli/references/runner.md +9 -1
package/dist/runner-sdk.js
CHANGED
|
@@ -9463,15 +9463,14 @@ var makeContractsV1 = (ids) => {
|
|
|
9463
9463
|
};
|
|
9464
9464
|
var publicContractsV1 = makeContractsV1(defaultIdSchemas);
|
|
9465
9465
|
|
|
9466
|
-
// src/core/pluralize.ts
|
|
9467
|
-
function pluralize(count, singular, plural = `${singular}s`) {
|
|
9468
|
-
return `${String(count)} ${count === 1 ? singular : plural}`;
|
|
9469
|
-
}
|
|
9470
|
-
|
|
9471
9466
|
// src/core/formatSeconds.ts
|
|
9472
9467
|
function formatSeconds(ms) {
|
|
9473
9468
|
return `${Math.round(ms / 1000)}s`;
|
|
9474
9469
|
}
|
|
9470
|
+
// src/core/pluralize.ts
|
|
9471
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
9472
|
+
return `${String(count)} ${count === 1 ? singular : plural}`;
|
|
9473
|
+
}
|
|
9475
9474
|
|
|
9476
9475
|
// src/core/messages/authErrors.ts
|
|
9477
9476
|
var authErrorMessages = {
|
|
@@ -9665,7 +9664,7 @@ var flowsMessages = {
|
|
|
9665
9664
|
extractingBundle: "Extracting bundle",
|
|
9666
9665
|
downloadingTeamStorageAssets: "Downloading team-storage assets",
|
|
9667
9666
|
downloadingTeamStorageAssetsProgress: (current, total) => `Downloading team-storage assets (${String(current)}/${String(total)})`,
|
|
9668
|
-
|
|
9667
|
+
teamStorageRequiresTeam: "Team storage needs a team. Pull an environment to name its team, choose a workspace with 'qawolf auth switch', or use a team API key.",
|
|
9669
9668
|
summary: (result, assetsAbs) => {
|
|
9670
9669
|
const flows = pluralize(result.flowCount, "flow");
|
|
9671
9670
|
const envVars = result.envVarCount === 0 ? "" : ` and ${pluralize(result.envVarCount, "environment variable")}`;
|
|
@@ -9815,17 +9814,18 @@ var interactMessages = {
|
|
|
9815
9814
|
};
|
|
9816
9815
|
|
|
9817
9816
|
// src/core/messages/interactiveRunner/lifecycle.ts
|
|
9817
|
+
var pageAt = (url) => `Its runner page is at ${url}`;
|
|
9818
9818
|
var noRunnerId = "No runner id. Pass --runner, set QAWOLF_RUNNER_ID, or run qawolf runner launch first.";
|
|
9819
9819
|
var lifecycleMessages = {
|
|
9820
|
-
alreadyRunning: (id) => `Runner ${id} was already running
|
|
9820
|
+
alreadyRunning: (id, url) => `Runner ${id} was already running. ${pageAt(url)}`,
|
|
9821
9821
|
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}.`,
|
|
9822
9822
|
defaultNotForgotten: (id) => `Runner ${id} was terminated, 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.`,
|
|
9823
9823
|
envRunnerIdShadowsLaunch: (launchedId, envId) => `QAWOLF_RUNNER_ID=${envId} is still set, so commands that omit --runner will keep targeting it instead of the runner just launched. Pass --runner ${launchedId} to address this one.`,
|
|
9824
9824
|
keptAlive: (id) => `Runner ${id} is alive, and its inactivity clock has been reset.`,
|
|
9825
9825
|
launchFailed: (id, error) => `Launching runner ${id} failed. ${error}`,
|
|
9826
9826
|
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 terminate it with qawolf runner terminate --runner ${id}.`,
|
|
9827
|
-
launched: (id) => `Launched runner ${id}
|
|
9828
|
-
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 terminated or idles out, so terminate it with qawolf runner terminate --runner ${id} when you are done
|
|
9827
|
+
launched: (id, url) => `Launched runner ${id}. ${pageAt(url)}`,
|
|
9828
|
+
launchedForCommand: (id, url) => `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 terminated or idles out, so terminate it with qawolf runner terminate --runner ${id} when you are done. ${pageAt(url)}`,
|
|
9829
9829
|
noRunnerIdForImport: `${noRunnerId} An install also needs a live run to go into, which a runner gets from qawolf runner run.`,
|
|
9830
9830
|
noRunnerIdForHighlight: `${noRunnerId} Highlighting also needs a live page to draw on, which a runner gets from its first run: run a flow on it with qawolf runner run.`,
|
|
9831
9831
|
noRunnerIdForInspect: `${noRunnerId} Inspecting also needs a page, which a runner gets from its first run: run a flow on it with qawolf runner run.`,
|
|
@@ -10017,7 +10017,8 @@ async function listRunners(ctx, deps) {
|
|
|
10017
10017
|
id: runner.id,
|
|
10018
10018
|
isDefault: runner.id === defaultRunnerId,
|
|
10019
10019
|
launchedHere: heldIds.has(runner.id),
|
|
10020
|
-
runnerName: runner.runnerName
|
|
10020
|
+
runnerName: runner.runnerName,
|
|
10021
|
+
url: runner.url
|
|
10021
10022
|
}));
|
|
10022
10023
|
return {
|
|
10023
10024
|
items: items.sort((left, right) => rank(left) - rank(right) || left.id.localeCompare(right.id)),
|
|
@@ -11057,6 +11058,37 @@ import { join as join3 } from "node:path";
|
|
|
11057
11058
|
var qawolfDir = ".qawolf";
|
|
11058
11059
|
var defaultOutputDir = `${qawolfDir}/output`;
|
|
11059
11060
|
|
|
11061
|
+
// src/core/parseJson.ts
|
|
11062
|
+
function parseJson(text) {
|
|
11063
|
+
try {
|
|
11064
|
+
return JSON.parse(text);
|
|
11065
|
+
} catch {
|
|
11066
|
+
return;
|
|
11067
|
+
}
|
|
11068
|
+
}
|
|
11069
|
+
|
|
11070
|
+
// src/shell/jsonFile.ts
|
|
11071
|
+
var pendingWrites = 0;
|
|
11072
|
+
async function readJsonFile(fs, path, schema) {
|
|
11073
|
+
const contents = await fs.readFile(path).catch(() => {
|
|
11074
|
+
return;
|
|
11075
|
+
});
|
|
11076
|
+
if (contents === undefined)
|
|
11077
|
+
return;
|
|
11078
|
+
const parsed = schema.safeParse(parseJson(contents));
|
|
11079
|
+
return parsed.success ? parsed.data : undefined;
|
|
11080
|
+
}
|
|
11081
|
+
async function writeJsonFileAtomically(fs, path, value) {
|
|
11082
|
+
const pendingPath = `${path}.${String(process.pid)}.${String(++pendingWrites)}.tmp`;
|
|
11083
|
+
try {
|
|
11084
|
+
await fs.writeFile(pendingPath, `${JSON.stringify(value, undefined, 2)}
|
|
11085
|
+
`);
|
|
11086
|
+
await fs.rename(pendingPath, path);
|
|
11087
|
+
} finally {
|
|
11088
|
+
await fs.rm(pendingPath, { force: true }).catch(() => {});
|
|
11089
|
+
}
|
|
11090
|
+
}
|
|
11091
|
+
|
|
11060
11092
|
// src/shell/interactiveRunner/runFilesManifest.ts
|
|
11061
11093
|
var manifestFileName = "runner-files.json";
|
|
11062
11094
|
var manifestSchema = object({
|
|
@@ -11067,33 +11099,14 @@ var manifestSchema = object({
|
|
|
11067
11099
|
function makeRunFilesManifestStore(options) {
|
|
11068
11100
|
const directory = join3(options.cwd, qawolfDir);
|
|
11069
11101
|
const path = join3(directory, manifestFileName);
|
|
11070
|
-
let pendingWrites = 0;
|
|
11071
11102
|
return {
|
|
11072
|
-
|
|
11073
|
-
const contents = await options.fs.readFile(path).catch(() => {
|
|
11074
|
-
return;
|
|
11075
|
-
});
|
|
11076
|
-
if (contents === undefined)
|
|
11077
|
-
return;
|
|
11078
|
-
const parsed = manifestSchema.safeParse(parseJson(contents));
|
|
11079
|
-
return parsed.success ? parsed.data : undefined;
|
|
11080
|
-
},
|
|
11103
|
+
read: () => readJsonFile(options.fs, path, manifestSchema),
|
|
11081
11104
|
async write(manifest) {
|
|
11082
|
-
const pendingPath = `${path}.${String(process.pid)}.${String(++pendingWrites)}.tmp`;
|
|
11083
11105
|
await options.fs.mkdir(directory, { recursive: true });
|
|
11084
|
-
await options.fs
|
|
11085
|
-
`);
|
|
11086
|
-
await options.fs.rename(pendingPath, path);
|
|
11106
|
+
await writeJsonFileAtomically(options.fs, path, manifest);
|
|
11087
11107
|
}
|
|
11088
11108
|
};
|
|
11089
11109
|
}
|
|
11090
|
-
function parseJson(text) {
|
|
11091
|
-
try {
|
|
11092
|
-
return JSON.parse(text);
|
|
11093
|
-
} catch {
|
|
11094
|
-
return;
|
|
11095
|
-
}
|
|
11096
|
-
}
|
|
11097
11110
|
|
|
11098
11111
|
// src/shell/interactiveRunner/runnerStore.ts
|
|
11099
11112
|
import { join as join4 } from "node:path";
|
|
@@ -11106,34 +11119,15 @@ var storedRunnerSchema = object({
|
|
|
11106
11119
|
var storeSchema = object({
|
|
11107
11120
|
defaultRunnerId: string2().optional()
|
|
11108
11121
|
});
|
|
11109
|
-
function parseJson2(text) {
|
|
11110
|
-
try {
|
|
11111
|
-
return JSON.parse(text);
|
|
11112
|
-
} catch {
|
|
11113
|
-
return;
|
|
11114
|
-
}
|
|
11115
|
-
}
|
|
11116
|
-
var pendingWrites = 0;
|
|
11117
11122
|
function makeRunnerStore(options) {
|
|
11118
11123
|
const directory = join4(options.cwd, qawolfDir);
|
|
11119
11124
|
const path = join4(directory, storeFileName);
|
|
11120
11125
|
const runnersDir = join4(directory, runnersDirName);
|
|
11121
|
-
const nextPendingPath = (target) => `${target}.${process.pid}.${++pendingWrites}.tmp`;
|
|
11122
11126
|
const runnerPath = (runnerId) => join4(runnersDir, `${encodeURIComponent(runnerId)}.json`);
|
|
11123
|
-
const writeAtomically =
|
|
11124
|
-
const pendingPath = nextPendingPath(target);
|
|
11125
|
-
await options.fs.writeFile(pendingPath, `${JSON.stringify(contents, undefined, 2)}
|
|
11126
|
-
`);
|
|
11127
|
-
await options.fs.rename(pendingPath, target);
|
|
11128
|
-
};
|
|
11127
|
+
const writeAtomically = (target, contents) => writeJsonFileAtomically(options.fs, target, contents);
|
|
11129
11128
|
const readDefaultRunnerId = async () => {
|
|
11130
|
-
const
|
|
11131
|
-
|
|
11132
|
-
});
|
|
11133
|
-
if (contents === undefined)
|
|
11134
|
-
return;
|
|
11135
|
-
const parsed = storeSchema.safeParse(parseJson2(contents));
|
|
11136
|
-
return parsed.success ? parsed.data.defaultRunnerId : undefined;
|
|
11129
|
+
const stored = await readJsonFile(options.fs, path, storeSchema);
|
|
11130
|
+
return stored?.defaultRunnerId;
|
|
11137
11131
|
};
|
|
11138
11132
|
const writeDefaultRunnerId = async (runnerId) => {
|
|
11139
11133
|
await options.fs.mkdir(directory, { recursive: true });
|
|
@@ -11155,15 +11149,7 @@ function makeRunnerStore(options) {
|
|
|
11155
11149
|
},
|
|
11156
11150
|
readDefaultRunnerId,
|
|
11157
11151
|
async readRunners() {
|
|
11158
|
-
const runners = await Promise.all((await readRunnerFileNames()).map(
|
|
11159
|
-
const contents = await options.fs.readFile(join4(runnersDir, name)).catch(() => {
|
|
11160
|
-
return;
|
|
11161
|
-
});
|
|
11162
|
-
if (contents === undefined)
|
|
11163
|
-
return;
|
|
11164
|
-
const parsed = storedRunnerSchema.safeParse(parseJson2(contents));
|
|
11165
|
-
return parsed.success ? parsed.data : undefined;
|
|
11166
|
-
}));
|
|
11152
|
+
const runners = await Promise.all((await readRunnerFileNames()).map((name) => readJsonFile(options.fs, join4(runnersDir, name), storedRunnerSchema)));
|
|
11167
11153
|
return runners.filter((runner) => !!runner).sort((left, right) => left.id.localeCompare(right.id));
|
|
11168
11154
|
},
|
|
11169
11155
|
async rememberLaunch(runner) {
|
|
@@ -11181,7 +11167,7 @@ function makeRunnerStore(options) {
|
|
|
11181
11167
|
});
|
|
11182
11168
|
if (contents === undefined)
|
|
11183
11169
|
return;
|
|
11184
|
-
if (storedRunnerSchema.safeParse(
|
|
11170
|
+
if (storedRunnerSchema.safeParse(parseJson(contents)).success)
|
|
11185
11171
|
return;
|
|
11186
11172
|
await options.fs.rm(runnerFile, { force: true });
|
|
11187
11173
|
}));
|
|
@@ -12800,7 +12786,8 @@ var environmentListItemSchema = object({
|
|
|
12800
12786
|
slug: string2()
|
|
12801
12787
|
});
|
|
12802
12788
|
var environmentWithVariablesResponseSchema = object({
|
|
12803
|
-
environmentVariables: record(string2(), string2())
|
|
12789
|
+
environmentVariables: record(string2(), string2()),
|
|
12790
|
+
teamId: string2()
|
|
12804
12791
|
});
|
|
12805
12792
|
var flowListItemSchema = object({
|
|
12806
12793
|
flowId: string2(),
|
|
@@ -13077,7 +13064,10 @@ async function writeTeamStorageAssets(args, deps) {
|
|
|
13077
13064
|
|
|
13078
13065
|
// src/shell/platform/teamStorageMethods.ts
|
|
13079
13066
|
function createTeamStorageMethods(trpc, deps, fs, getIdentity) {
|
|
13080
|
-
async function list() {
|
|
13067
|
+
async function list(opts) {
|
|
13068
|
+
if (opts?.teamId !== undefined) {
|
|
13069
|
+
return listTeamStorageFiles(trpc, { teamId: opts.teamId }, deps);
|
|
13070
|
+
}
|
|
13081
13071
|
if (deps.workspaceId !== undefined) {
|
|
13082
13072
|
return listTeamStorageFiles(trpc, { teamId: deps.workspaceId }, deps);
|
|
13083
13073
|
}
|
|
@@ -13087,7 +13077,7 @@ function createTeamStorageMethods(trpc, deps, fs, getIdentity) {
|
|
|
13087
13077
|
if (!("team" in identity.value)) {
|
|
13088
13078
|
return {
|
|
13089
13079
|
ok: false,
|
|
13090
|
-
error: flowsMessages.pull.
|
|
13080
|
+
error: flowsMessages.pull.teamStorageRequiresTeam
|
|
13091
13081
|
};
|
|
13092
13082
|
}
|
|
13093
13083
|
return listTeamStorageFiles(trpc, { teamId: identity.value.team.id }, deps);
|
|
@@ -13095,7 +13085,7 @@ function createTeamStorageMethods(trpc, deps, fs, getIdentity) {
|
|
|
13095
13085
|
return {
|
|
13096
13086
|
listTeamStorageFiles: list,
|
|
13097
13087
|
async syncTeamStorageAssets(assetsAbs, opts) {
|
|
13098
|
-
const files = await list();
|
|
13088
|
+
const files = await list({ teamId: opts?.teamId });
|
|
13099
13089
|
if (!files.ok)
|
|
13100
13090
|
return files;
|
|
13101
13091
|
return downloadTeamStorageAssets({ assetsAbs, files: files.value }, { fetch: deps.fetch, fs, onProgress: opts?.onProgress });
|
|
@@ -13125,16 +13115,14 @@ function createPlatformClient(apiKey, deps) {
|
|
|
13125
13115
|
...createTeamStorageMethods(trpc, deps, fs, identityMethods.getIdentity),
|
|
13126
13116
|
getFlowsBundleUrl: getFlowsBundleUrlImpl,
|
|
13127
13117
|
callPublicApi: makeCallPublicApiMethod(trpc, deps, requestBackoffMs2),
|
|
13128
|
-
async
|
|
13118
|
+
async getEnvironmentWithVariables(envId) {
|
|
13129
13119
|
const result = await requestWithRetry({
|
|
13130
13120
|
call: () => trpc.query("environment.getEnvironmentWithVariables", { id: envId }, environmentWithVariablesResponseSchema),
|
|
13131
13121
|
backoffMs: requestBackoffMs2,
|
|
13132
13122
|
describe: (err) => describeRequestError(err, deps.baseUrl, "env-vars"),
|
|
13133
13123
|
sleep: deps.sleep
|
|
13134
13124
|
});
|
|
13135
|
-
|
|
13136
|
-
return result;
|
|
13137
|
-
return { ok: true, value: result.value.environmentVariables };
|
|
13125
|
+
return result;
|
|
13138
13126
|
},
|
|
13139
13127
|
async downloadBundle(envId) {
|
|
13140
13128
|
const urlResult = await getFlowsBundleUrlImpl(envId);
|
|
@@ -13863,4 +13851,4 @@ export {
|
|
|
13863
13851
|
createRunnerSdk
|
|
13864
13852
|
};
|
|
13865
13853
|
|
|
13866
|
-
//# debugId=
|
|
13854
|
+
//# debugId=395E9F8ECC554DB664756E2164756E21
|
|
@@ -124,6 +124,8 @@ export type ListedRunner = {
|
|
|
124
124
|
isDefault: boolean;
|
|
125
125
|
launchedHere: boolean;
|
|
126
126
|
runnerName: string;
|
|
127
|
+
/** The QA Wolf page for this runner, showing its screen once it has one. */
|
|
128
|
+
url: string;
|
|
127
129
|
};
|
|
128
130
|
export type KeptAlive = {
|
|
129
131
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qawolf/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Run and manage QA Wolf flows from the terminal, CI, or an AI agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@qawolf/emails": "1.1.1",
|
|
76
76
|
"@qawolf/flow-targets": "1.0.0",
|
|
77
77
|
"@qawolf/flows": "0.1.4",
|
|
78
|
-
"@qawolf/testkit": "1.
|
|
78
|
+
"@qawolf/testkit": "1.2.1",
|
|
79
79
|
"commander": "14.0.3",
|
|
80
80
|
"env-paths": "4.0.0",
|
|
81
81
|
"picomatch": "4.0.4",
|
|
@@ -125,8 +125,8 @@ that `url`; never guess a route and never send a repository link in its place.
|
|
|
125
125
|
<!-- prettier-ignore -->
|
|
126
126
|
| Command | Kind | What it does |
|
|
127
127
|
| --- | --- | --- |
|
|
128
|
-
| `qawolf agent get` | read |
|
|
129
|
-
| `qawolf agent send` | write |
|
|
128
|
+
| `qawolf agent get` | read | Read what the QA Wolf AI has said and whether it is still working |
|
|
129
|
+
| `qawolf agent send` | write | Ask the QA Wolf AI to do a piece of work, such as covering a journey or fixing a broken flow |
|
|
130
130
|
| `qawolf auth login` | local | Authenticate with QA Wolf in a browser or with an API key |
|
|
131
131
|
| `qawolf auth logout` | local | Remove stored credentials |
|
|
132
132
|
| `qawolf auth switch` | local | Choose which workspace to work in |
|
|
@@ -209,6 +209,60 @@ changes team state; `local` only affects this machine. A parenthesized
|
|
|
209
209
|
note like `local (read with --remote)` means that flag makes the command
|
|
210
210
|
call the QA Wolf API and require auth.
|
|
211
211
|
|
|
212
|
+
## Asking QA Wolf to do the work: the `agent` group
|
|
213
|
+
|
|
214
|
+
`qawolf agent send "<what you want>"` is the one verb that starts work from
|
|
215
|
+
nothing. Every other write acts on a flow, run or issue that already exists.
|
|
216
|
+
Name the journey, the part of the app it covers, and anything the AI cannot
|
|
217
|
+
discover for itself, such as a test account, a feature flag, or how to reach a
|
|
218
|
+
staging environment. For a long message, put it in a file and pass
|
|
219
|
+
`"$(cat prompt.md)"`, so shell quoting cannot split it into arguments.
|
|
220
|
+
|
|
221
|
+
`--environment-id <id-or-alias>` picks the environment and reads
|
|
222
|
+
`QAWOLF_ENVIRONMENT`. A credential that is not bound to one workspace, such as
|
|
223
|
+
an organization or user API key, needs `--workspace-id`.
|
|
224
|
+
|
|
225
|
+
To give the AI a file, such as a spreadsheet of journeys, upload it first with
|
|
226
|
+
`qawolf file requestUpload --file-name <name>`, PUT the bytes to the returned
|
|
227
|
+
URL with the returned content type, then pass the returned path as
|
|
228
|
+
`--file-paths <path>` on the send. The AI reads it from storage, so a plan of
|
|
229
|
+
hundreds of journeys costs nothing to send. Up to 20 paths per send.
|
|
230
|
+
|
|
231
|
+
The work runs for minutes to tens of minutes. **Pass `--follow` and do not poll.**
|
|
232
|
+
The CLI reads the session for you, prints each reply once as it arrives, and
|
|
233
|
+
exits when the session settles: 0 when the work is complete, non-zero when it
|
|
234
|
+
failed or was cancelled. Looping `qawolf agent get` yourself costs a round trip
|
|
235
|
+
per tick and shows you replies you have already seen.
|
|
236
|
+
|
|
237
|
+
A session can stop and ask a question. With `--follow` in `--agent` or `--json`
|
|
238
|
+
mode the CLI prints the question and **exits 0** — a session that asked something
|
|
239
|
+
has handed the work back, it has not failed. Answer it, then pick the session
|
|
240
|
+
back up:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
qawolf agent send "<your answer>" --session <sessionId>
|
|
244
|
+
qawolf agent get --follow
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
In `--agent` or `--json` mode a follow ends with one JSON line holding the whole
|
|
248
|
+
session: `sessionId`, `status`, `url` and `replies`. It is the same object a
|
|
249
|
+
plain `qawolf agent get` answers with. Read the final `status` from that line;
|
|
250
|
+
the exit code alone does not tell a blocked session from a completed one.
|
|
251
|
+
|
|
252
|
+
`agent send` remembers the session it started, so a later `agent get --follow`
|
|
253
|
+
in the same directory needs no id. `--session <id>` or `QAWOLF_SESSION_ID`
|
|
254
|
+
override that.
|
|
255
|
+
|
|
256
|
+
Every session has a `url`, which the CLI prints when it starts or attaches.
|
|
257
|
+
A session opens in whichever workspace the credential is pointed at, which is not
|
|
258
|
+
always the one the user has open in the app, so send that `url` when you report
|
|
259
|
+
on a session rather than describing where it went.
|
|
260
|
+
|
|
261
|
+
Expect quiet stretches. QA Wolf reports a session as working and says nothing
|
|
262
|
+
more until the AI speaks, so several minutes with no output is the session
|
|
263
|
+
working, not the command hanging. `--timeout` bounds the wait; it is 30 minutes
|
|
264
|
+
by default.
|
|
265
|
+
|
|
212
266
|
## Driving a browser: the `runner` group
|
|
213
267
|
|
|
214
268
|
The `runner` commands drive a live cloud browser: `launch` one, `screenshot` to
|
|
@@ -13,6 +13,8 @@ of starting and billing a second one, and the answer says which happened: read
|
|
|
13
13
|
cheap and safe pattern, and the same id with a different `--name` is refused
|
|
14
14
|
rather than silently ignored.
|
|
15
15
|
|
|
16
|
+
Either answer carries a `url`, which `qawolf runner launch` prints, as does a command that launched its own runner. It is a QA Wolf page showing what the runner is doing, where a person can also take over with their own mouse and keyboard. Hand it to a person who asks what your runner is up to. The page opens for anyone on the runner's team, however the runner was launched. There is nothing to see until the runner's first run starts its screen, so the page waits until then. You read the screen with `screenshot`, not with the page.
|
|
17
|
+
|
|
16
18
|
Commands that target a runner find one in this order: `--runner`, then
|
|
17
19
|
`QAWOLF_RUNNER_ID`, then the runner stored for the current directory (which
|
|
18
20
|
`qawolf runner launch` sets). Setting the environment variable once is the most
|
|
@@ -83,6 +85,12 @@ directory did not launch it, so a harness handed a runner sees it alongside the
|
|
|
83
85
|
ones it started itself. Use the `id` column with `--runner` to address any of
|
|
84
86
|
them; addressing one does not make it the default.
|
|
85
87
|
|
|
88
|
+
The table leaves out the page address, which beside a 63-character id outgrows a terminal. `--json` carries it as `url` on every runner:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
qawolf runner list --json | jq -r '.[] | [.id, .url] | @tsv'
|
|
92
|
+
```
|
|
93
|
+
|
|
86
94
|
## The order that matters
|
|
87
95
|
|
|
88
96
|
A freshly launched runner has no screen. The virtual desktop starts with the
|
|
@@ -586,7 +594,7 @@ the screen, so it is not optional even though the goal here is to drive by hand.
|
|
|
586
594
|
export QAWOLF_API_KEY=... # the only credential
|
|
587
595
|
export QAWOLF_RUNNER_ID=agent-1 # so no command below needs --runner
|
|
588
596
|
|
|
589
|
-
qawolf runner launch --id agent-1 --json # --id, not the variable; read .alreadyRunning
|
|
597
|
+
qawolf runner launch --id agent-1 --json # --id, not the variable; read .alreadyRunning and .url
|
|
590
598
|
qawolf runner run flows/smoke.flow.ts --follow # starts the screen; exit 1 if it failed
|
|
591
599
|
|
|
592
600
|
qawolf runner act navigate --url https://example.com/login --screenshot step-1.jpg # then read step-1.jpg yourself
|