@questpie/probe 0.1.2 → 0.1.3
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/{browser-DoCXU5Bs.js → browser-C0KoBYfo.js} +17 -0
- package/dist/{check-1Y6cPMkN.js → check-DzHesF9c.js} +2 -2
- package/dist/cli.js +34 -15
- package/dist/{compose-1CYahcRa.js → compose-BuBOYqop.js} +3 -3
- package/dist/{logs-BIt5sCky.js → logs-LWtXR5o6.js} +1 -1
- package/dist/{process-manager-CITbaM9X.js → process-manager-CUmXxN1Y.js} +1 -1
- package/dist/process-manager-D_k19e8-.js +4 -0
- package/dist/{ps-UFZx0jy0.js → ps-E1hgvd2u.js} +2 -2
- package/dist/{record-C4SmoPsT.js → record-DE4BA6jl.js} +3 -56
- package/dist/recorder-FojdM3gr.js +81 -0
- package/dist/{recordings-Cb31alos.js → recordings-CmVHyt3A.js} +2 -2
- package/dist/{replay-Dg9PHNrg.js → replay-DTB0C9DC.js} +1 -1
- package/dist/{restart-CEzd2Mgn.js → restart-CCnfVg7M.js} +2 -2
- package/dist/{start-BJnzhZp9.js → start-DAj1ldz_.js} +2 -2
- package/dist/{stop-C0xDWnnB.js → stop-Y3DscZCW.js} +2 -2
- package/package.json +1 -1
- package/skills/qprobe/SKILL.md +227 -93
- package/skills/qprobe/references/assertions.md +168 -0
- package/skills/qprobe/references/check.md +61 -0
- package/skills/qprobe/references/logs.md +116 -0
- package/skills/qprobe/references/recording.md +42 -0
- package/dist/process-manager-BVSQm3gK.js +0 -4
- /package/dist/{codegen-BH3cUNuf.js → codegen-XzGmnbb5.js} +0 -0
- /package/dist/{health-B36ufFzJ.js → health-Derc4CP4.js} +0 -0
- /package/dist/{http-BZouO1Cj.js → http-7baasp3-.js} +0 -0
- /package/dist/{init-BjTfn_-A.js → init-B91b6oc2.js} +0 -0
- /package/dist/{reporter-CqWc26OP.js → reporter-B-dIKeqZ.js} +0 -0
- /package/dist/{state-DRTSIt_r.js → state-B0f4kwdz.js} +0 -0
|
@@ -2,9 +2,13 @@ import { parseDuration } from "./duration-DUrbfMLK.js";
|
|
|
2
2
|
import { loadProbeConfig, resolveBaseUrl } from "./config-BUEMgFYN.js";
|
|
3
3
|
import { AgentBrowserDriver } from "./agent-browser-Cxuu-Zz0.js";
|
|
4
4
|
import { error, info, json, log, success } from "./output-CHUjdVDf.js";
|
|
5
|
+
import { recordAction } from "./recorder-FojdM3gr.js";
|
|
5
6
|
import { defineCommand } from "citty";
|
|
6
7
|
|
|
7
8
|
//#region src/commands/browser.ts
|
|
9
|
+
function record(command$1, ...args) {
|
|
10
|
+
recordAction(`browser ${command$1}`, args);
|
|
11
|
+
}
|
|
8
12
|
async function getDriver(args) {
|
|
9
13
|
const config = await loadProbeConfig();
|
|
10
14
|
const driverName = args.driver ?? config.browser?.driver ?? "agent-browser";
|
|
@@ -45,6 +49,7 @@ const open = defineCommand({
|
|
|
45
49
|
async run({ args }) {
|
|
46
50
|
const driver = await getDriver(args);
|
|
47
51
|
await driver.open(args.url);
|
|
52
|
+
record("open", args.url);
|
|
48
53
|
success(`Opened ${args.url}`);
|
|
49
54
|
}
|
|
50
55
|
});
|
|
@@ -61,6 +66,7 @@ const back = defineCommand({
|
|
|
61
66
|
async run({ args }) {
|
|
62
67
|
const driver = await getDriver(args);
|
|
63
68
|
await driver.back();
|
|
69
|
+
record("back");
|
|
64
70
|
success("Navigated back");
|
|
65
71
|
}
|
|
66
72
|
});
|
|
@@ -77,6 +83,7 @@ const forward = defineCommand({
|
|
|
77
83
|
async run({ args }) {
|
|
78
84
|
const driver = await getDriver(args);
|
|
79
85
|
await driver.forward();
|
|
86
|
+
record("forward");
|
|
80
87
|
success("Navigated forward");
|
|
81
88
|
}
|
|
82
89
|
});
|
|
@@ -93,6 +100,7 @@ const reload = defineCommand({
|
|
|
93
100
|
async run({ args }) {
|
|
94
101
|
const driver = await getDriver(args);
|
|
95
102
|
await driver.reload();
|
|
103
|
+
record("reload");
|
|
96
104
|
success("Reloaded");
|
|
97
105
|
}
|
|
98
106
|
});
|
|
@@ -206,6 +214,7 @@ const click = defineCommand({
|
|
|
206
214
|
async run({ args }) {
|
|
207
215
|
const driver = await getDriver(args);
|
|
208
216
|
await driver.click(args.ref);
|
|
217
|
+
record("click", args.ref);
|
|
209
218
|
success(`Clicked ${args.ref}`);
|
|
210
219
|
}
|
|
211
220
|
});
|
|
@@ -226,6 +235,7 @@ const dblclick = defineCommand({
|
|
|
226
235
|
async run({ args }) {
|
|
227
236
|
const driver = await getDriver(args);
|
|
228
237
|
await driver.dblclick(args.ref);
|
|
238
|
+
record("dblclick", args.ref);
|
|
229
239
|
success(`Double-clicked ${args.ref}`);
|
|
230
240
|
}
|
|
231
241
|
});
|
|
@@ -252,6 +262,7 @@ const fill = defineCommand({
|
|
|
252
262
|
async run({ args }) {
|
|
253
263
|
const driver = await getDriver(args);
|
|
254
264
|
await driver.fill(args.ref, args.value);
|
|
265
|
+
record("fill", args.ref, args.value);
|
|
255
266
|
success(`Filled ${args.ref}`);
|
|
256
267
|
}
|
|
257
268
|
});
|
|
@@ -276,6 +287,7 @@ const selectCmd = defineCommand({
|
|
|
276
287
|
async run({ args }) {
|
|
277
288
|
const driver = await getDriver(args);
|
|
278
289
|
await driver.select(args.ref, args.value);
|
|
290
|
+
record("select", args.ref, args.value);
|
|
279
291
|
success(`Selected "${args.value}" in ${args.ref}`);
|
|
280
292
|
}
|
|
281
293
|
});
|
|
@@ -296,6 +308,7 @@ const checkCmd = defineCommand({
|
|
|
296
308
|
async run({ args }) {
|
|
297
309
|
const driver = await getDriver(args);
|
|
298
310
|
await driver.check(args.ref);
|
|
311
|
+
record("check", args.ref);
|
|
299
312
|
success(`Checked ${args.ref}`);
|
|
300
313
|
}
|
|
301
314
|
});
|
|
@@ -316,6 +329,7 @@ const uncheckCmd = defineCommand({
|
|
|
316
329
|
async run({ args }) {
|
|
317
330
|
const driver = await getDriver(args);
|
|
318
331
|
await driver.uncheck(args.ref);
|
|
332
|
+
record("uncheck", args.ref);
|
|
319
333
|
success(`Unchecked ${args.ref}`);
|
|
320
334
|
}
|
|
321
335
|
});
|
|
@@ -337,6 +351,7 @@ const press = defineCommand({
|
|
|
337
351
|
async run({ args }) {
|
|
338
352
|
const driver = await getDriver(args);
|
|
339
353
|
await driver.press(args.key);
|
|
354
|
+
record("press", args.key);
|
|
340
355
|
success(`Pressed ${args.key}`);
|
|
341
356
|
}
|
|
342
357
|
});
|
|
@@ -358,6 +373,7 @@ const typeCmd = defineCommand({
|
|
|
358
373
|
async run({ args }) {
|
|
359
374
|
const driver = await getDriver(args);
|
|
360
375
|
await driver.type(args.text);
|
|
376
|
+
record("type", args.text);
|
|
361
377
|
success("Typed text");
|
|
362
378
|
}
|
|
363
379
|
});
|
|
@@ -487,6 +503,7 @@ const screenshot = defineCommand({
|
|
|
487
503
|
full: args.full,
|
|
488
504
|
selector: args.selector
|
|
489
505
|
});
|
|
506
|
+
record("screenshot", path);
|
|
490
507
|
success(`Screenshot saved: ${path}`);
|
|
491
508
|
}
|
|
492
509
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "./duration-DUrbfMLK.js";
|
|
2
2
|
import { loadProbeConfig, resolveBaseUrl } from "./config-BUEMgFYN.js";
|
|
3
3
|
import { error, info, success, warn } from "./output-CHUjdVDf.js";
|
|
4
|
-
import "./state-
|
|
5
|
-
import { listProcesses } from "./process-manager-
|
|
4
|
+
import "./state-B0f4kwdz.js";
|
|
5
|
+
import { listProcesses } from "./process-manager-CUmXxN1Y.js";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
7
|
import { ofetch } from "ofetch";
|
|
8
8
|
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,23 @@ import { defineCommand, runMain, showUsage } from "citty";
|
|
|
4
4
|
import { consola } from "consola";
|
|
5
5
|
|
|
6
6
|
//#region src/cli.ts
|
|
7
|
+
const subCommandNames = new Set([
|
|
8
|
+
"start",
|
|
9
|
+
"stop",
|
|
10
|
+
"restart",
|
|
11
|
+
"ps",
|
|
12
|
+
"health",
|
|
13
|
+
"compose",
|
|
14
|
+
"logs",
|
|
15
|
+
"http",
|
|
16
|
+
"check",
|
|
17
|
+
"browser",
|
|
18
|
+
"record",
|
|
19
|
+
"replay",
|
|
20
|
+
"recordings",
|
|
21
|
+
"assert",
|
|
22
|
+
"init"
|
|
23
|
+
]);
|
|
7
24
|
const main = defineCommand({
|
|
8
25
|
meta: {
|
|
9
26
|
name: "qprobe",
|
|
@@ -11,23 +28,25 @@ const main = defineCommand({
|
|
|
11
28
|
description: "Dev testing CLI for AI coding agents"
|
|
12
29
|
},
|
|
13
30
|
subCommands: {
|
|
14
|
-
start: () => import("./start-
|
|
15
|
-
stop: () => import("./stop-
|
|
16
|
-
restart: () => import("./restart-
|
|
17
|
-
ps: () => import("./ps-
|
|
18
|
-
health: () => import("./health-
|
|
19
|
-
compose: () => import("./compose-
|
|
20
|
-
logs: () => import("./logs-
|
|
21
|
-
http: () => import("./http-
|
|
22
|
-
check: () => import("./check-
|
|
23
|
-
browser: () => import("./browser-
|
|
24
|
-
record: () => import("./record-
|
|
25
|
-
replay: () => import("./replay-
|
|
26
|
-
recordings: () => import("./recordings-
|
|
31
|
+
start: () => import("./start-DAj1ldz_.js").then((m) => m.default),
|
|
32
|
+
stop: () => import("./stop-Y3DscZCW.js").then((m) => m.default),
|
|
33
|
+
restart: () => import("./restart-CCnfVg7M.js").then((m) => m.default),
|
|
34
|
+
ps: () => import("./ps-E1hgvd2u.js").then((m) => m.default),
|
|
35
|
+
health: () => import("./health-Derc4CP4.js").then((m) => m.default),
|
|
36
|
+
compose: () => import("./compose-BuBOYqop.js").then((m) => m.default),
|
|
37
|
+
logs: () => import("./logs-LWtXR5o6.js").then((m) => m.default),
|
|
38
|
+
http: () => import("./http-7baasp3-.js").then((m) => m.default),
|
|
39
|
+
check: () => import("./check-DzHesF9c.js").then((m) => m.default),
|
|
40
|
+
browser: () => import("./browser-C0KoBYfo.js").then((m) => m.default),
|
|
41
|
+
record: () => import("./record-DE4BA6jl.js").then((m) => m.default),
|
|
42
|
+
replay: () => import("./replay-DTB0C9DC.js").then((m) => m.default),
|
|
43
|
+
recordings: () => import("./recordings-CmVHyt3A.js").then((m) => m.default),
|
|
27
44
|
assert: () => import("./assert-BLP5_JwC.js").then((m) => m.default),
|
|
28
|
-
init: () => import("./init-
|
|
45
|
+
init: () => import("./init-B91b6oc2.js").then((m) => m.default)
|
|
29
46
|
},
|
|
30
|
-
async run({ cmd }) {
|
|
47
|
+
async run({ cmd, rawArgs }) {
|
|
48
|
+
const firstArg = rawArgs[0];
|
|
49
|
+
if (firstArg && subCommandNames.has(firstArg)) return;
|
|
31
50
|
const hasConfig = existsSync("qprobe.config.ts") || existsSync("qprobe.config.js") || existsSync("qprobe.config.mjs");
|
|
32
51
|
if (hasConfig) {
|
|
33
52
|
await showUsage(cmd);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "./duration-DUrbfMLK.js";
|
|
2
2
|
import { loadProbeConfig } from "./config-BUEMgFYN.js";
|
|
3
3
|
import { error, info, json, success, table } from "./output-CHUjdVDf.js";
|
|
4
|
-
import "./state-
|
|
5
|
-
import { listProcesses, startProcess, stopProcess } from "./process-manager-
|
|
4
|
+
import "./state-B0f4kwdz.js";
|
|
5
|
+
import { listProcesses, startProcess, stopProcess } from "./process-manager-CUmXxN1Y.js";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
7
|
import { ofetch } from "ofetch";
|
|
8
8
|
|
|
@@ -163,7 +163,7 @@ const restart = defineCommand({
|
|
|
163
163
|
error(`Unknown service: "${args.name}"`);
|
|
164
164
|
process.exit(1);
|
|
165
165
|
}
|
|
166
|
-
const { stopProcess: stopProcess$1, startProcess: startProcess$1 } = await import("./process-manager-
|
|
166
|
+
const { stopProcess: stopProcess$1, startProcess: startProcess$1 } = await import("./process-manager-D_k19e8-.js");
|
|
167
167
|
try {
|
|
168
168
|
await stopProcess$1(args.name);
|
|
169
169
|
} catch {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseDuration } from "./duration-DUrbfMLK.js";
|
|
2
2
|
import { error, info, json, log } from "./output-CHUjdVDf.js";
|
|
3
|
-
import { getLogPath, listProcessNames } from "./state-
|
|
3
|
+
import { getLogPath, listProcessNames } from "./state-B0f4kwdz.js";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { consola } from "consola";
|
|
6
6
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ensureLogsDir, getLogPath, listProcessNames, readPid, readState, removePid, removeState, savePid, saveState } from "./state-
|
|
1
|
+
import { ensureLogsDir, getLogPath, listProcessNames, readPid, readState, removePid, removeState, savePid, saveState } from "./state-B0f4kwdz.js";
|
|
2
2
|
import { openSync } from "node:fs";
|
|
3
3
|
import { appendFile } from "node:fs/promises";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { info, json, table } from "./output-CHUjdVDf.js";
|
|
2
|
-
import "./state-
|
|
3
|
-
import { listProcesses } from "./process-manager-
|
|
2
|
+
import "./state-B0f4kwdz.js";
|
|
3
|
+
import { listProcesses } from "./process-manager-CUmXxN1Y.js";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/ps.ts
|
|
@@ -1,65 +1,12 @@
|
|
|
1
1
|
import "./duration-DUrbfMLK.js";
|
|
2
2
|
import { loadProbeConfig } from "./config-BUEMgFYN.js";
|
|
3
3
|
import { error, info, success } from "./output-CHUjdVDf.js";
|
|
4
|
-
import {
|
|
4
|
+
import { cancelRecording, getActiveRecording, startRecording, stopRecording } from "./recorder-FojdM3gr.js";
|
|
5
|
+
import { generatePlaywrightTest } from "./codegen-XzGmnbb5.js";
|
|
5
6
|
import { defineCommand } from "citty";
|
|
6
|
-
import { mkdir,
|
|
7
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
|
|
9
|
-
//#region src/testing/recorder.ts
|
|
10
|
-
const STATE_FILE = "tmp/qprobe/state/recording.json";
|
|
11
|
-
let activeRecording = null;
|
|
12
|
-
async function startRecording(name) {
|
|
13
|
-
if (activeRecording) throw new Error(`Already recording "${activeRecording.name}". Stop it first.`);
|
|
14
|
-
const config = await loadProbeConfig();
|
|
15
|
-
activeRecording = {
|
|
16
|
-
name,
|
|
17
|
-
startedAt: new Date().toISOString(),
|
|
18
|
-
baseUrl: config.http?.baseUrl ?? config.browser?.baseUrl,
|
|
19
|
-
actions: []
|
|
20
|
-
};
|
|
21
|
-
await mkdir("tmp/qprobe/state", { recursive: true });
|
|
22
|
-
await writeFile(STATE_FILE, JSON.stringify(activeRecording, null, 2), "utf-8");
|
|
23
|
-
}
|
|
24
|
-
async function stopRecording() {
|
|
25
|
-
const recording = activeRecording ?? await loadActiveRecording();
|
|
26
|
-
if (!recording) throw new Error("No active recording");
|
|
27
|
-
recording.finishedAt = new Date().toISOString();
|
|
28
|
-
const config = await loadProbeConfig();
|
|
29
|
-
const testsDir = config.tests?.dir ?? "tests/qprobe";
|
|
30
|
-
const recordingsDir = join(testsDir, "recordings");
|
|
31
|
-
await mkdir(recordingsDir, { recursive: true });
|
|
32
|
-
const jsonPath = join(recordingsDir, `${recording.name}.json`);
|
|
33
|
-
await writeFile(jsonPath, JSON.stringify(recording, null, 2), "utf-8");
|
|
34
|
-
activeRecording = null;
|
|
35
|
-
try {
|
|
36
|
-
const { rm: rm$1 } = await import("node:fs/promises");
|
|
37
|
-
await rm$1(STATE_FILE);
|
|
38
|
-
} catch {}
|
|
39
|
-
return recording;
|
|
40
|
-
}
|
|
41
|
-
async function cancelRecording() {
|
|
42
|
-
activeRecording = null;
|
|
43
|
-
try {
|
|
44
|
-
const { rm: rm$1 } = await import("node:fs/promises");
|
|
45
|
-
await rm$1(STATE_FILE);
|
|
46
|
-
} catch {}
|
|
47
|
-
}
|
|
48
|
-
async function loadActiveRecording() {
|
|
49
|
-
try {
|
|
50
|
-
const content = await readFile(STATE_FILE, "utf-8");
|
|
51
|
-
activeRecording = JSON.parse(content);
|
|
52
|
-
return activeRecording;
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
async function getActiveRecording() {
|
|
58
|
-
if (activeRecording) return activeRecording;
|
|
59
|
-
return loadActiveRecording();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
//#endregion
|
|
63
10
|
//#region src/commands/record.ts
|
|
64
11
|
const start = defineCommand({
|
|
65
12
|
meta: {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { loadProbeConfig } from "./config-BUEMgFYN.js";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/testing/recorder.ts
|
|
7
|
+
const STATE_FILE = "tmp/qprobe/state/recording.json";
|
|
8
|
+
let activeRecording = null;
|
|
9
|
+
async function startRecording(name) {
|
|
10
|
+
const existing = activeRecording ?? await loadActiveRecording();
|
|
11
|
+
if (existing) {
|
|
12
|
+
const startedAt = new Date(existing.startedAt);
|
|
13
|
+
const elapsed = Date.now() - startedAt.getTime();
|
|
14
|
+
const mins = Math.floor(elapsed / 6e4);
|
|
15
|
+
const age = mins > 0 ? ` (started ${mins}m ago)` : "";
|
|
16
|
+
throw new Error(`Already recording "${existing.name}"${age}. Run "qprobe record stop" or "qprobe record cancel" first.`);
|
|
17
|
+
}
|
|
18
|
+
const config = await loadProbeConfig();
|
|
19
|
+
activeRecording = {
|
|
20
|
+
name,
|
|
21
|
+
startedAt: new Date().toISOString(),
|
|
22
|
+
baseUrl: config.http?.baseUrl ?? config.browser?.baseUrl,
|
|
23
|
+
actions: []
|
|
24
|
+
};
|
|
25
|
+
await mkdir("tmp/qprobe/state", { recursive: true });
|
|
26
|
+
await writeFile(STATE_FILE, JSON.stringify(activeRecording, null, 2), "utf-8");
|
|
27
|
+
}
|
|
28
|
+
function recordAction(command, args) {
|
|
29
|
+
if (!activeRecording) try {
|
|
30
|
+
const content = readFileSync(STATE_FILE, "utf-8");
|
|
31
|
+
activeRecording = JSON.parse(content);
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
activeRecording.actions.push({
|
|
36
|
+
command,
|
|
37
|
+
args,
|
|
38
|
+
timestamp: new Date().toISOString()
|
|
39
|
+
});
|
|
40
|
+
writeFile(STATE_FILE, JSON.stringify(activeRecording, null, 2), "utf-8");
|
|
41
|
+
}
|
|
42
|
+
async function stopRecording() {
|
|
43
|
+
const recording = activeRecording ?? await loadActiveRecording();
|
|
44
|
+
if (!recording) throw new Error("No active recording");
|
|
45
|
+
recording.finishedAt = new Date().toISOString();
|
|
46
|
+
const config = await loadProbeConfig();
|
|
47
|
+
const testsDir = config.tests?.dir ?? "tests/qprobe";
|
|
48
|
+
const recordingsDir = join(testsDir, "recordings");
|
|
49
|
+
await mkdir(recordingsDir, { recursive: true });
|
|
50
|
+
const jsonPath = join(recordingsDir, `${recording.name}.json`);
|
|
51
|
+
await writeFile(jsonPath, JSON.stringify(recording, null, 2), "utf-8");
|
|
52
|
+
activeRecording = null;
|
|
53
|
+
try {
|
|
54
|
+
const { rm: rm$1 } = await import("node:fs/promises");
|
|
55
|
+
await rm$1(STATE_FILE);
|
|
56
|
+
} catch {}
|
|
57
|
+
return recording;
|
|
58
|
+
}
|
|
59
|
+
async function cancelRecording() {
|
|
60
|
+
activeRecording = null;
|
|
61
|
+
try {
|
|
62
|
+
const { rm: rm$1 } = await import("node:fs/promises");
|
|
63
|
+
await rm$1(STATE_FILE);
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
async function loadActiveRecording() {
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(STATE_FILE, "utf-8");
|
|
69
|
+
activeRecording = JSON.parse(content);
|
|
70
|
+
return activeRecording;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function getActiveRecording() {
|
|
76
|
+
if (activeRecording) return activeRecording;
|
|
77
|
+
return loadActiveRecording();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
export { cancelRecording, getActiveRecording, recordAction, startRecording, stopRecording };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "./duration-DUrbfMLK.js";
|
|
2
2
|
import { loadProbeConfig } from "./config-BUEMgFYN.js";
|
|
3
3
|
import { error, info, json, log, success } from "./output-CHUjdVDf.js";
|
|
4
|
-
import { generatePlaywrightTest } from "./codegen-
|
|
5
|
-
import { formatRecordingsList } from "./reporter-
|
|
4
|
+
import { generatePlaywrightTest } from "./codegen-XzGmnbb5.js";
|
|
5
|
+
import { formatRecordingsList } from "./reporter-B-dIKeqZ.js";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
7
|
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "./duration-DUrbfMLK.js";
|
|
2
2
|
import { loadProbeConfig } from "./config-BUEMgFYN.js";
|
|
3
3
|
import { error, log } from "./output-CHUjdVDf.js";
|
|
4
|
-
import { formatReplayResult } from "./reporter-
|
|
4
|
+
import { formatReplayResult } from "./reporter-B-dIKeqZ.js";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { error, success } from "./output-CHUjdVDf.js";
|
|
2
|
-
import "./state-
|
|
3
|
-
import { getProcessState, startProcess, stopProcess } from "./process-manager-
|
|
2
|
+
import "./state-B0f4kwdz.js";
|
|
3
|
+
import { getProcessState, startProcess, stopProcess } from "./process-manager-CUmXxN1Y.js";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/restart.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseDuration } from "./duration-DUrbfMLK.js";
|
|
2
2
|
import { error, success } from "./output-CHUjdVDf.js";
|
|
3
|
-
import "./state-
|
|
4
|
-
import { startProcess } from "./process-manager-
|
|
3
|
+
import "./state-B0f4kwdz.js";
|
|
4
|
+
import { startProcess } from "./process-manager-CUmXxN1Y.js";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
|
|
7
7
|
//#region src/commands/start.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { error, success } from "./output-CHUjdVDf.js";
|
|
2
|
-
import "./state-
|
|
3
|
-
import { stopAll, stopProcess } from "./process-manager-
|
|
2
|
+
import "./state-B0f4kwdz.js";
|
|
3
|
+
import { stopAll, stopProcess } from "./process-manager-CUmXxN1Y.js";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/stop.ts
|
package/package.json
CHANGED
package/skills/qprobe/SKILL.md
CHANGED
|
@@ -5,100 +5,234 @@ description: "QUESTPIE Probe — dev testing CLI for AI coding agents. Orchestra
|
|
|
5
5
|
|
|
6
6
|
# QUESTPIE Probe
|
|
7
7
|
|
|
8
|
-
Dev testing CLI.
|
|
8
|
+
Dev testing CLI for AI coding agents. Start servers, read logs, test APIs, control browsers, record and replay tests.
|
|
9
9
|
|
|
10
10
|
**Install CLI:** `bun add -g @questpie/probe`
|
|
11
11
|
**Install skill:** `bunx skills add questpie/probe`
|
|
12
|
+
**Docs:** https://probe.questpie.com/docs
|
|
12
13
|
|
|
13
|
-
## Core
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
14
|
+
## Core Principles
|
|
15
|
+
|
|
16
|
+
1. **Logs before browser** — 90% of bugs are visible in logs and HTTP responses. Open a browser only when you need visual verification.
|
|
17
|
+
2. **Record → replay free** — record a flow once, replay it forever with pure Playwright. Zero AI tokens on regression.
|
|
18
|
+
3. **Token-efficient** — use `snapshot -i` (interactive only), `--diff` (changes only), `--grep` (filtered logs). Read only what you need.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Testing Workflow
|
|
23
|
+
|
|
24
|
+
Follow this order every time you test an app. Do NOT skip to browser.
|
|
25
|
+
|
|
26
|
+
### Step 1 — Start the app
|
|
27
|
+
```bash
|
|
28
|
+
# Option A: from config
|
|
29
|
+
qprobe compose up
|
|
30
|
+
|
|
31
|
+
# Option B: manually
|
|
32
|
+
qprobe start server "bun dev" --ready "ready on" --port 3000
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Step 2 — Check health (no browser yet)
|
|
36
|
+
```bash
|
|
37
|
+
qprobe ps # is it running?
|
|
38
|
+
qprobe health http://localhost:3000/api/health # does it respond?
|
|
39
|
+
qprobe check http://localhost:3000 # all-in-one summary
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Step 3 — Read logs (no browser yet)
|
|
43
|
+
```bash
|
|
44
|
+
qprobe logs server # recent logs
|
|
45
|
+
qprobe logs server --grep "ERROR" # find errors
|
|
46
|
+
qprobe logs server --level error # only ERROR level
|
|
47
|
+
qprobe logs --all --grep "ERROR" # errors from all services
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Step 4 — Test API (no browser yet)
|
|
51
|
+
```bash
|
|
52
|
+
qprobe http GET /api/health --status 200
|
|
53
|
+
qprobe http GET /api/users --status 200
|
|
54
|
+
qprobe http POST /api/users -d '{"name":"Test"}' --status 201
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Step 5 — Open browser (only if needed)
|
|
58
|
+
```bash
|
|
59
|
+
qprobe browser open http://localhost:3000
|
|
60
|
+
qprobe browser snapshot -i -c # see interactive elements
|
|
61
|
+
qprobe browser errors # JS errors?
|
|
62
|
+
qprobe browser network --failed # failed requests?
|
|
63
|
+
qprobe browser console --level error # console errors?
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Step 6 — Interact and verify
|
|
67
|
+
```bash
|
|
68
|
+
qprobe browser click @e3 # click element from snapshot
|
|
69
|
+
qprobe browser fill @e2 "hello" # fill input
|
|
70
|
+
qprobe browser press Enter # submit
|
|
71
|
+
qprobe browser snapshot --diff # what changed?
|
|
72
|
+
qprobe browser screenshot # visual proof
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Step 7 — Assert
|
|
76
|
+
```bash
|
|
77
|
+
qprobe assert status 200 /api/health # API is healthy
|
|
78
|
+
qprobe assert no-errors # no JS errors in browser
|
|
79
|
+
qprobe assert no-network-errors # no 4xx/5xx in network
|
|
80
|
+
qprobe assert text "Dashboard" # page shows expected text
|
|
81
|
+
qprobe assert title "My App" # page title matches
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Step 8 — Record for regression
|
|
85
|
+
```bash
|
|
86
|
+
qprobe record start "my-flow"
|
|
87
|
+
# ... repeat steps 5-7 ...
|
|
88
|
+
qprobe record stop # saves JSON + generates .spec.ts
|
|
89
|
+
qprobe replay "my-flow" # run Playwright test (zero tokens)
|
|
90
|
+
qprobe replay --all # run all recorded tests
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Step 9 — Cleanup
|
|
94
|
+
```bash
|
|
95
|
+
qprobe stop --all # or: qprobe compose down
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Debugging Workflow
|
|
101
|
+
|
|
102
|
+
When something doesn't work — don't jump to browser. Triage in this order:
|
|
103
|
+
|
|
104
|
+
### 1. Is it running?
|
|
105
|
+
```bash
|
|
106
|
+
qprobe ps
|
|
107
|
+
qprobe check
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 2. Check logs for errors
|
|
111
|
+
```bash
|
|
112
|
+
qprobe logs server --level error
|
|
113
|
+
qprobe logs --all --grep "Error|fail|crash" --since 10m
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 3. Test API directly
|
|
117
|
+
```bash
|
|
118
|
+
qprobe http GET /api/health --status 200
|
|
119
|
+
qprobe http GET /api/<endpoint> -v # verbose: see full request/response
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 4. Browser only if UI bug
|
|
123
|
+
```bash
|
|
124
|
+
qprobe browser open <url>
|
|
125
|
+
qprobe browser console --level error # JS errors?
|
|
126
|
+
qprobe browser network --failed # failed requests?
|
|
127
|
+
qprobe browser snapshot -i # what does the page show?
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 5. Fix → restart → verify → record
|
|
131
|
+
```bash
|
|
132
|
+
qprobe restart server
|
|
133
|
+
qprobe logs server --level error --since 1m # should be empty now
|
|
134
|
+
qprobe record start "fix-<issue>"
|
|
135
|
+
# (re-run the verification steps that failed before)
|
|
136
|
+
qprobe assert no-errors
|
|
137
|
+
qprobe record stop
|
|
138
|
+
qprobe replay "fix-<issue>" # MUST pass
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Exit Codes
|
|
142
|
+
|
|
143
|
+
| Code | Meaning | What to do |
|
|
144
|
+
|------|---------|------------|
|
|
145
|
+
| 0 | Success | Continue |
|
|
146
|
+
| 1 | Assertion/command error | Read the output message |
|
|
147
|
+
| 2 | Process timeout | Ready pattern not matched — check logs, increase `--timeout` |
|
|
148
|
+
| 3 | Health check failed | Server not responding — check if process is running |
|
|
149
|
+
| 4 | Config error | Check `qprobe.config.ts` syntax |
|
|
150
|
+
| 5 | Browser driver missing | Install: `bun add -g agent-browser` |
|
|
151
|
+
| 6 | Replay test failure | Regression — check what changed in code |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Shipping a Feature
|
|
156
|
+
|
|
157
|
+
A feature is **NOT done** until its replay passes.
|
|
158
|
+
|
|
159
|
+
### 1. Plan what to test
|
|
160
|
+
|
|
161
|
+
Before writing any test, identify:
|
|
162
|
+
- What are the **critical user flows** for this feature?
|
|
163
|
+
- What are the **edge cases**? (empty state, validation errors, permissions)
|
|
164
|
+
- What **API endpoints** does it touch?
|
|
165
|
+
|
|
166
|
+
If unclear — **ask the user** for their expected flows and scenarios.
|
|
167
|
+
|
|
168
|
+
### 2. Verify with logs + API first
|
|
169
|
+
```bash
|
|
170
|
+
qprobe logs server --level error # no errors after change
|
|
171
|
+
qprobe http GET /api/<endpoint> --status 200 # API works
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 3. Record each user flow
|
|
175
|
+
```bash
|
|
176
|
+
# For each critical flow:
|
|
177
|
+
qprobe record start "<feature>-<flow-name>"
|
|
178
|
+
qprobe browser open <start-url>
|
|
179
|
+
# (interact: fill, click, navigate)
|
|
180
|
+
qprobe assert no-errors
|
|
181
|
+
qprobe assert no-network-errors
|
|
182
|
+
qprobe record stop
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 4. Replay ALL — must pass
|
|
186
|
+
```bash
|
|
187
|
+
qprobe replay --all
|
|
188
|
+
# If any replay fails → fix and re-record that flow
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 5. Feature is shippable when:
|
|
192
|
+
- All **existing replays** still pass (no regressions)
|
|
193
|
+
- All **new feature flows** are recorded and pass
|
|
194
|
+
- **No JS errors**, no network errors
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Command Reference
|
|
199
|
+
|
|
200
|
+
| Command | Purpose | When to use |
|
|
201
|
+
|---------|---------|-------------|
|
|
202
|
+
| `qprobe start` | Start process with ready detection | Dev server, DB, worker |
|
|
203
|
+
| `qprobe stop` | Graceful stop (SIGTERM → SIGKILL) | Cleanup |
|
|
204
|
+
| `qprobe restart` | Stop + start with saved config | After code changes |
|
|
205
|
+
| `qprobe ps` | List running processes | Check what's up |
|
|
206
|
+
| `qprobe health` | Poll URL until healthy | Wait for server |
|
|
207
|
+
| `qprobe compose` | Multi-service orchestration | Start full stack |
|
|
208
|
+
| `qprobe logs` | Read/filter/follow logs | Debug errors |
|
|
209
|
+
| `qprobe http` | HTTP requests with assertions | Test API |
|
|
210
|
+
| `qprobe check` | All-in-one health summary | Quick overview |
|
|
211
|
+
| `qprobe browser` | Browser automation (27 subcommands) | Visual testing |
|
|
212
|
+
| `qprobe record` | Record browser actions | Capture test flow |
|
|
213
|
+
| `qprobe replay` | Run recorded Playwright tests | Regression testing |
|
|
214
|
+
| `qprobe assert` | Assert conditions (exit 1 on fail) | Verify state |
|
|
215
|
+
| `qprobe init` | Scaffold qprobe.config.ts | Project setup |
|
|
216
|
+
|
|
217
|
+
## Detailed References
|
|
218
|
+
|
|
219
|
+
Read the reference file for the command group you need:
|
|
220
|
+
|
|
221
|
+
- **Process management** (start, stop, ps, health, restart) → `references/process.md`
|
|
222
|
+
- **Logs** (read, filter, grep, follow, merge) → `references/logs.md`
|
|
223
|
+
- **HTTP requests & API testing** → `references/http.md`
|
|
224
|
+
- **Quick health check** (one-shot status + services) → `references/check.md`
|
|
225
|
+
- **Browser control** (snapshot, click, fill, console, network) → `references/browser.md`
|
|
226
|
+
- **Service orchestration** (compose up/down, dependencies) → `references/compose.md`
|
|
227
|
+
- **Recording & replay** (test capture, Playwright codegen) → `references/recording.md`
|
|
228
|
+
- **Assertions** (text, element, URL, status, errors) → `references/assertions.md`
|
|
229
|
+
- **UX testing methods** (heuristics, task completion, a11y) → `references/ux.md`
|
|
230
|
+
|
|
231
|
+
## Tips
|
|
232
|
+
|
|
233
|
+
- `qprobe logs server --grep "ERROR"` is faster than reading all logs
|
|
234
|
+
- `qprobe http` is cheaper than `qprobe browser` for API testing
|
|
235
|
+
- `snapshot -i` saves tokens — shows only interactive elements
|
|
236
|
+
- `snapshot --diff` after actions shows only what changed
|
|
237
|
+
- `qprobe check` gives a one-command status overview
|
|
238
|
+
- Record flows early — replay is free forever
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Assertions Reference
|
|
2
|
+
|
|
3
|
+
Assert conditions against browser state or HTTP endpoints. Exit code 0 on pass, exit code 1 on fail.
|
|
4
|
+
|
|
5
|
+
## Assertion Types
|
|
6
|
+
|
|
7
|
+
### text / no-text
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
qprobe assert text "Dashboard" # page body contains text
|
|
11
|
+
qprobe assert no-text "Error" # page body does NOT contain text
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **Case-sensitive** — `"Dashboard"` will NOT match `"dashboard"`
|
|
15
|
+
- Uses `includes()` on full page text content
|
|
16
|
+
- Requires open browser session
|
|
17
|
+
|
|
18
|
+
Output:
|
|
19
|
+
```
|
|
20
|
+
✅ Page contains "Dashboard"
|
|
21
|
+
❌ Page does not contain "Dashboard" # exit 1
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### element
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
qprobe assert element @e3 # ref from snapshot exists
|
|
28
|
+
qprobe assert element "#user-avatar" # CSS selector exists
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Checks the interactive snapshot (`-i -c`) for the ref or selector string
|
|
32
|
+
- Accepts `@e` refs (from most recent snapshot) and CSS selectors
|
|
33
|
+
|
|
34
|
+
Output:
|
|
35
|
+
```
|
|
36
|
+
✅ Element "@e3" exists
|
|
37
|
+
❌ Element "@e3" not found # exit 1
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### url
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
qprobe assert url "/dashboard" # URL contains /dashboard
|
|
44
|
+
qprobe assert url "^https://" # URL starts with https
|
|
45
|
+
qprobe assert url "/users/\\d+" # regex match
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- Pattern is a **JavaScript RegExp** (not glob, not exact match)
|
|
49
|
+
- Matches against full current URL
|
|
50
|
+
|
|
51
|
+
Output:
|
|
52
|
+
```
|
|
53
|
+
✅ URL matches "/dashboard" (http://localhost:3000/dashboard)
|
|
54
|
+
❌ URL "http://localhost:3000/login" does not match "/dashboard" # exit 1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### title
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
qprobe assert title "Dashboard"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **Case-sensitive** `includes()` on page title
|
|
64
|
+
|
|
65
|
+
Output:
|
|
66
|
+
```
|
|
67
|
+
✅ Title contains "Dashboard" (Dashboard - My App)
|
|
68
|
+
❌ Title "Login - My App" does not contain "Dashboard" # exit 1
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### status
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
qprobe assert status 200 /api/health # TWO arguments: code + path
|
|
75
|
+
qprobe assert status 404 /api/nonexistent
|
|
76
|
+
qprobe assert status 201 /api/users
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **Two positional arguments**: expected status code, then URL path
|
|
80
|
+
- Path resolves against baseUrl (same resolution as `qprobe http`)
|
|
81
|
+
- Makes an HTTP request and checks the response status
|
|
82
|
+
|
|
83
|
+
Output:
|
|
84
|
+
```
|
|
85
|
+
✅ http://localhost:3000/api/health returned 200
|
|
86
|
+
❌ http://localhost:3000/api/health returned 500, expected 200 # exit 1
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### no-errors
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
qprobe assert no-errors
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- Checks browser console for uncaught JS exceptions
|
|
96
|
+
- Requires open browser session
|
|
97
|
+
|
|
98
|
+
Output:
|
|
99
|
+
```
|
|
100
|
+
✅ No JS errors
|
|
101
|
+
|
|
102
|
+
❌ Found 2 JS error(s): # exit 1
|
|
103
|
+
TypeError: Cannot read properties of undefined (reading 'map')
|
|
104
|
+
ReferenceError: user is not defined
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### no-network-errors
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
qprobe assert no-network-errors
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- Checks network tab for 4xx/5xx HTTP responses
|
|
114
|
+
- Requires open browser session
|
|
115
|
+
|
|
116
|
+
Output:
|
|
117
|
+
```
|
|
118
|
+
✅ No network errors
|
|
119
|
+
|
|
120
|
+
❌ Found 1 network error(s): # exit 1
|
|
121
|
+
POST 500 /api/users
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Common Patterns
|
|
125
|
+
|
|
126
|
+
### After form submission
|
|
127
|
+
```bash
|
|
128
|
+
qprobe browser click @e5 # submit
|
|
129
|
+
qprobe browser wait --network idle
|
|
130
|
+
qprobe assert no-errors
|
|
131
|
+
qprobe assert no-network-errors
|
|
132
|
+
qprobe assert text "Saved successfully"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### After page navigation
|
|
136
|
+
```bash
|
|
137
|
+
qprobe browser click @e3
|
|
138
|
+
qprobe assert url "/dashboard"
|
|
139
|
+
qprobe assert title "Dashboard"
|
|
140
|
+
qprobe assert no-errors
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Full health check (API + browser)
|
|
144
|
+
```bash
|
|
145
|
+
qprobe assert status 200 /api/health
|
|
146
|
+
qprobe assert status 200 /api/users
|
|
147
|
+
qprobe assert no-errors
|
|
148
|
+
qprobe assert no-network-errors
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### CI/script chaining
|
|
152
|
+
```bash
|
|
153
|
+
# All assertions exit 1 on failure — chain with &&
|
|
154
|
+
qprobe assert status 200 /api/health && \
|
|
155
|
+
qprobe assert no-errors && \
|
|
156
|
+
qprobe assert text "Welcome" && \
|
|
157
|
+
echo "All checks passed"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Tips
|
|
161
|
+
|
|
162
|
+
- `text` / `no-text` are **case-sensitive** — use exact case from the page
|
|
163
|
+
- `url` uses **regex** — escape dots in literal URLs: `"localhost\\.com"`
|
|
164
|
+
- `element` checks the interactive snapshot string, not raw DOM
|
|
165
|
+
- `status` needs **TWO args** — the expected code AND the URL path
|
|
166
|
+
- `no-errors` and `no-network-errors` **require an open browser session**
|
|
167
|
+
- Chain assertions with `&&` for scripts and CI
|
|
168
|
+
- All assertions exit code 1 on failure — useful for conditional logic
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Check Reference
|
|
2
|
+
|
|
3
|
+
Quick one-shot health check. Single HTTP request + timing + running services summary.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
qprobe check [url]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
No flags. URL defaults to baseUrl from config (same resolution as `qprobe http`).
|
|
12
|
+
|
|
13
|
+
## What It Does
|
|
14
|
+
|
|
15
|
+
1. Makes a single HTTP request to the URL
|
|
16
|
+
2. Reports HTTP status code + response time in milliseconds
|
|
17
|
+
3. Lists all running managed processes
|
|
18
|
+
|
|
19
|
+
## Output
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
qprobe check http://localhost:3000
|
|
23
|
+
# ✅ HTTP 200 OK (45ms)
|
|
24
|
+
# ℹ 3 service(s) running (db, server, worker)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
qprobe check http://localhost:3000
|
|
29
|
+
# ❌ HTTP 500 Internal Server Error (120ms)
|
|
30
|
+
# ℹ 2 service(s) running (db, server)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
qprobe check http://localhost:3000
|
|
35
|
+
# ❌ Connection failed: ECONNREFUSED
|
|
36
|
+
# ⚠ No managed services running
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Without URL — uses config baseUrl
|
|
41
|
+
qprobe check
|
|
42
|
+
# ✅ HTTP 200 OK (32ms)
|
|
43
|
+
# ℹ 1 service(s) running (server)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## check vs health
|
|
47
|
+
|
|
48
|
+
| | `qprobe check` | `qprobe health` |
|
|
49
|
+
|--|----------------|-----------------|
|
|
50
|
+
| Requests | Single | Polling until success |
|
|
51
|
+
| Timeout | None | `--timeout` (default 30s) |
|
|
52
|
+
| Retries | No | Yes (`--interval`) |
|
|
53
|
+
| Service list | Yes | No |
|
|
54
|
+
| Use case | "Is it up right now?" | "Wait until it's up" |
|
|
55
|
+
|
|
56
|
+
## When to Use
|
|
57
|
+
|
|
58
|
+
- **First command in debugging** — quick sanity check before digging deeper
|
|
59
|
+
- **After `qprobe compose up`** — verify the stack came up
|
|
60
|
+
- **After `qprobe restart`** — verify server is responding
|
|
61
|
+
- Simpler than `qprobe health` when you don't need polling/retries
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Logs Reference
|
|
2
|
+
|
|
3
|
+
Timestamped, level-parsed logs for every managed process.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
qprobe logs <name> [flags] # single process
|
|
9
|
+
qprobe logs --all [flags] # all processes, merged by timestamp
|
|
10
|
+
qprobe logs --unified [flags] # all processes + browser log
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Flags
|
|
14
|
+
|
|
15
|
+
| Flag | Short | Default | Description |
|
|
16
|
+
|------|-------|---------|-------------|
|
|
17
|
+
| `--follow` | `-f` | `false` | Stream new entries in real time (tail -f). Blocks until Ctrl+C |
|
|
18
|
+
| `--lines` | `-n` | `50` | Number of recent lines to show |
|
|
19
|
+
| `--grep` | | — | Case-insensitive regex filter on message text |
|
|
20
|
+
| `--level` | | — | Exact level filter: `error`, `warn`, `info`, `debug` |
|
|
21
|
+
| `--since` | | — | Time range: `30s`, `5m`, `1h` — only entries newer than this |
|
|
22
|
+
| `--all` | | `false` | Merge logs from all processes, sorted by timestamp |
|
|
23
|
+
| `--unified` | | `false` | All processes + browser console, interleaved |
|
|
24
|
+
| `--json` | | `false` | Output as JSON array of LogEntry objects |
|
|
25
|
+
|
|
26
|
+
## Output Format
|
|
27
|
+
|
|
28
|
+
Single process:
|
|
29
|
+
```
|
|
30
|
+
2026-03-25T20:30:15.247Z INFO Server ready on http://localhost:3000
|
|
31
|
+
2026-03-25T20:30:16.103Z ERROR Connection refused to database
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
With `--all` or `--unified` (includes source name):
|
|
35
|
+
```
|
|
36
|
+
[server] 2026-03-25T20:30:15.247Z INFO Server ready on http://localhost:3000
|
|
37
|
+
[db] 2026-03-25T20:30:14.892Z INFO PostgreSQL ready to accept connections
|
|
38
|
+
[server] 2026-03-25T20:30:16.103Z ERROR Connection refused to database
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Colors in follow mode: ERROR = red, WARN = yellow, others = default.
|
|
42
|
+
|
|
43
|
+
JSON output (`--json`):
|
|
44
|
+
```json
|
|
45
|
+
[
|
|
46
|
+
{
|
|
47
|
+
"timestamp": "2026-03-25T20:30:16.103Z",
|
|
48
|
+
"level": "ERROR",
|
|
49
|
+
"message": "Connection refused to database",
|
|
50
|
+
"source": "server"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Examples
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Last 50 lines from server (default)
|
|
59
|
+
qprobe logs server
|
|
60
|
+
|
|
61
|
+
# Only errors
|
|
62
|
+
qprobe logs server --level error
|
|
63
|
+
|
|
64
|
+
# Regex search across message text (case-insensitive)
|
|
65
|
+
qprobe logs server --grep "connection|timeout|refused"
|
|
66
|
+
|
|
67
|
+
# Errors from the last 5 minutes
|
|
68
|
+
qprobe logs server --level error --since 5m
|
|
69
|
+
|
|
70
|
+
# Stream new errors in real time
|
|
71
|
+
qprobe logs server -f --level error
|
|
72
|
+
# (blocks — Ctrl+C to stop)
|
|
73
|
+
|
|
74
|
+
# Check all services for a pattern
|
|
75
|
+
qprobe logs --all --grep "ECONNREFUSED"
|
|
76
|
+
|
|
77
|
+
# Last 200 lines as JSON (pipe to jq)
|
|
78
|
+
qprobe logs server --json --lines 200
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Common Patterns
|
|
82
|
+
|
|
83
|
+
### Find why the server crashed
|
|
84
|
+
```bash
|
|
85
|
+
qprobe ps # is it still running?
|
|
86
|
+
qprobe logs server --level error # recent errors
|
|
87
|
+
qprobe logs server --grep "stack|trace|fatal" # stack traces
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Watch for errors during testing
|
|
91
|
+
```bash
|
|
92
|
+
# In one terminal / before testing:
|
|
93
|
+
qprobe logs server -f --level error
|
|
94
|
+
# Run browser commands in another terminal — errors appear in real time
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Check all services after compose up
|
|
98
|
+
```bash
|
|
99
|
+
qprobe compose up
|
|
100
|
+
qprobe logs --all --level error --since 1m # any errors during startup?
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Machine-readable logs for analysis
|
|
104
|
+
```bash
|
|
105
|
+
qprobe logs server --json --since 1h | jq '.[] | select(.level == "ERROR")'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Tips
|
|
109
|
+
|
|
110
|
+
- **Always check logs BEFORE opening a browser** — most bugs are visible here
|
|
111
|
+
- `--grep` is a regex — use `"fail|error|crash"` for broad search
|
|
112
|
+
- `--level error` is faster than `--grep "error"` (exact field match vs string search)
|
|
113
|
+
- `--since 5m` narrows results to recent issues — useful after code changes
|
|
114
|
+
- Follow mode (`-f`) blocks the terminal — use it for real-time monitoring during testing
|
|
115
|
+
- Log files are stored in `tmp/qprobe/logs/<name>.log`
|
|
116
|
+
- Level detection is automatic: lines containing "error" → ERROR, "warn" → WARN, etc.
|
|
@@ -184,6 +184,47 @@ tests/qprobe/
|
|
|
184
184
|
└── playwright.config.ts # auto-generated
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
## Edge Cases
|
|
188
|
+
|
|
189
|
+
### Stale recording from a previous session
|
|
190
|
+
If a recording was started but the CLI process was interrupted (Ctrl+C, crash, terminal closed), the state file persists on disk.
|
|
191
|
+
|
|
192
|
+
Next `qprobe record start` will detect it:
|
|
193
|
+
```
|
|
194
|
+
Already recording "old-name" (started 45m ago).
|
|
195
|
+
Run "qprobe record stop" or "qprobe record cancel" first.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Fix: `qprobe record cancel`
|
|
199
|
+
|
|
200
|
+
### Starting when one is already active
|
|
201
|
+
```
|
|
202
|
+
Already recording "login-flow".
|
|
203
|
+
Run "qprobe record stop" or "qprobe record cancel" first.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Always cancel or stop before starting a new recording.
|
|
207
|
+
|
|
208
|
+
### Error during recording
|
|
209
|
+
If a browser command fails during recording, the recording **continues**. The failed action is still captured. Stop or cancel when ready.
|
|
210
|
+
|
|
211
|
+
### Empty recording
|
|
212
|
+
If you stop a recording with zero actions, the JSON and `.spec.ts` files are still generated — but the test will be empty and pass trivially.
|
|
213
|
+
|
|
214
|
+
### Recording is transparent
|
|
215
|
+
When no recording is active, all browser and HTTP commands work identically. Recording state is completely transparent — commands behave the same whether recording is on or off.
|
|
216
|
+
|
|
217
|
+
### Corrupted state file
|
|
218
|
+
If the state file is corrupted and `record cancel` doesn't work:
|
|
219
|
+
```bash
|
|
220
|
+
rm tmp/qprobe/state/recording.json
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### File locations
|
|
224
|
+
- Active recording state: `tmp/qprobe/state/recording.json`
|
|
225
|
+
- Saved recordings: `tests/qprobe/recordings/<name>.json`
|
|
226
|
+
- Generated tests: `tests/qprobe/recordings/<name>.spec.ts`
|
|
227
|
+
|
|
187
228
|
## Tips
|
|
188
229
|
|
|
189
230
|
- **Record important flows early** — login, CRUD, critical paths
|
|
@@ -192,3 +233,4 @@ tests/qprobe/
|
|
|
192
233
|
- **Export recordings** to share with team or run in CI
|
|
193
234
|
- **Edit generated .spec.ts** files to add custom assertions
|
|
194
235
|
- **Use `--retries 2`** for network-dependent tests that may be flaky
|
|
236
|
+
- **If `record start` fails** — check for stale recording with `qprobe record cancel`
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|