@questpie/probe 0.1.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/agent-browser-Cxuu-Zz0.js +203 -0
- package/dist/assert-BLP5_JwC.js +212 -0
- package/dist/browser-DoCXU5Bs.js +736 -0
- package/dist/check-Cny-3lkZ.js +41 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +30 -0
- package/dist/codegen-BH3cUNuf.js +61 -0
- package/dist/compose-D5a8qHkg.js +233 -0
- package/dist/config-BUEMgFYN.js +89 -0
- package/dist/duration-D1ya1zLn.js +3 -0
- package/dist/duration-DUrbfMLK.js +30 -0
- package/dist/health-B36ufFzJ.js +62 -0
- package/dist/http-BZouO1Cj.js +187 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +4 -0
- package/dist/init-BjTfn_-A.js +92 -0
- package/dist/logs-BCgur07G.js +191 -0
- package/dist/output-CHUjdVDf.js +38 -0
- package/dist/process-manager-CzexpFO4.js +229 -0
- package/dist/process-manager-zzltWvZ0.js +4 -0
- package/dist/ps-DuHF7vmE.js +39 -0
- package/dist/record-C4SmoPsT.js +140 -0
- package/dist/recordings-Cb31alos.js +158 -0
- package/dist/replay-Dg9PHNrg.js +171 -0
- package/dist/reporter-CqWc26OP.js +25 -0
- package/dist/restart-By3Edj5X.js +44 -0
- package/dist/snapshot-diff-CqXEVTAZ.js +51 -0
- package/dist/start-BClY6oJq.js +79 -0
- package/dist/state-DRTSIt_r.js +62 -0
- package/dist/stop-QAP6gbDe.js +47 -0
- package/package.json +72 -0
- package/skills/qprobe/SKILL.md +103 -0
- package/skills/qprobe/references/browser.md +201 -0
- package/skills/qprobe/references/compose.md +128 -0
- package/skills/qprobe/references/http.md +151 -0
- package/skills/qprobe/references/process.md +114 -0
- package/skills/qprobe/references/recording.md +194 -0
- package/skills/qprobe-browser/SKILL.md +87 -0
- package/skills/qprobe-compose/SKILL.md +81 -0
- package/skills/qprobe-http/SKILL.md +67 -0
- package/skills/qprobe-process/SKILL.md +58 -0
- package/skills/qprobe-recording/SKILL.md +63 -0
- package/skills/qprobe-ux/SKILL.md +250 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { x } from "tinyexec";
|
|
4
|
+
|
|
5
|
+
//#region src/browser/agent-browser.ts
|
|
6
|
+
const SNAPSHOTS_DIR = "tmp/qprobe/snapshots";
|
|
7
|
+
const SHOTS_DIR = "tmp/qprobe/shots";
|
|
8
|
+
async function ensureDir(dir) {
|
|
9
|
+
await mkdir(dir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
let shotCounter = 0;
|
|
12
|
+
var AgentBrowserDriver = class {
|
|
13
|
+
session;
|
|
14
|
+
headed;
|
|
15
|
+
baseUrl;
|
|
16
|
+
constructor(opts = {}) {
|
|
17
|
+
this.session = opts.session ?? "qprobe";
|
|
18
|
+
this.headed = opts.headed ?? false;
|
|
19
|
+
this.baseUrl = opts.baseUrl;
|
|
20
|
+
}
|
|
21
|
+
async run(...args) {
|
|
22
|
+
const cmdArgs = [
|
|
23
|
+
"--session",
|
|
24
|
+
this.session,
|
|
25
|
+
"--json",
|
|
26
|
+
...args
|
|
27
|
+
];
|
|
28
|
+
if (this.headed) cmdArgs.unshift("--headed");
|
|
29
|
+
const result = await x("agent-browser", cmdArgs, {
|
|
30
|
+
timeout: 3e4,
|
|
31
|
+
throwOnError: false
|
|
32
|
+
});
|
|
33
|
+
const out = result.stdout.trim();
|
|
34
|
+
if (!out) {
|
|
35
|
+
if (result.exitCode !== 0) throw new Error(`agent-browser failed (exit ${result.exitCode}): ${result.stderr}`);
|
|
36
|
+
return { success: true };
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(out);
|
|
40
|
+
} catch {
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
data: { text: out }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async runOrThrow(...args) {
|
|
48
|
+
const res = await this.run(...args);
|
|
49
|
+
if (!res.success) throw new Error(res.error ?? "agent-browser command failed");
|
|
50
|
+
return res.data ?? {};
|
|
51
|
+
}
|
|
52
|
+
resolveUrl(url) {
|
|
53
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
|
54
|
+
if (this.baseUrl) return `${this.baseUrl}${url}`;
|
|
55
|
+
return url;
|
|
56
|
+
}
|
|
57
|
+
async open(url) {
|
|
58
|
+
await this.runOrThrow("open", this.resolveUrl(url));
|
|
59
|
+
}
|
|
60
|
+
async back() {
|
|
61
|
+
await this.runOrThrow("back");
|
|
62
|
+
}
|
|
63
|
+
async forward() {
|
|
64
|
+
await this.runOrThrow("forward");
|
|
65
|
+
}
|
|
66
|
+
async reload() {
|
|
67
|
+
await this.runOrThrow("reload");
|
|
68
|
+
}
|
|
69
|
+
async url() {
|
|
70
|
+
const data = await this.runOrThrow("get", "url");
|
|
71
|
+
return String(data["text"] ?? data["url"] ?? "");
|
|
72
|
+
}
|
|
73
|
+
async title() {
|
|
74
|
+
const data = await this.runOrThrow("get", "title");
|
|
75
|
+
return String(data["text"] ?? data["title"] ?? "");
|
|
76
|
+
}
|
|
77
|
+
async close() {
|
|
78
|
+
await this.runOrThrow("close");
|
|
79
|
+
}
|
|
80
|
+
async snapshot(opts) {
|
|
81
|
+
const args = ["snapshot"];
|
|
82
|
+
if (opts?.interactive) args.push("-i");
|
|
83
|
+
if (opts?.compact) args.push("-c");
|
|
84
|
+
if (opts?.depth !== void 0) args.push("-d", String(opts.depth));
|
|
85
|
+
if (opts?.selector) args.push("-s", opts.selector);
|
|
86
|
+
const data = await this.runOrThrow(...args);
|
|
87
|
+
const snapshotText = String(data["snapshot"] ?? data["text"] ?? "");
|
|
88
|
+
await ensureDir(SNAPSHOTS_DIR);
|
|
89
|
+
const currentPath = join(SNAPSHOTS_DIR, "current.yaml");
|
|
90
|
+
const previousPath = join(SNAPSHOTS_DIR, "previous.yaml");
|
|
91
|
+
try {
|
|
92
|
+
const existing = await readFile(currentPath, "utf-8");
|
|
93
|
+
await writeFile(previousPath, existing, "utf-8");
|
|
94
|
+
} catch {}
|
|
95
|
+
await writeFile(currentPath, snapshotText, "utf-8");
|
|
96
|
+
if (opts?.diff) {
|
|
97
|
+
const { diffSnapshots } = await import("./snapshot-diff-CqXEVTAZ.js");
|
|
98
|
+
try {
|
|
99
|
+
const previous = await readFile(previousPath, "utf-8");
|
|
100
|
+
return diffSnapshots(previous, snapshotText);
|
|
101
|
+
} catch {
|
|
102
|
+
return snapshotText;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return snapshotText;
|
|
106
|
+
}
|
|
107
|
+
async click(ref) {
|
|
108
|
+
await this.runOrThrow("click", ref);
|
|
109
|
+
}
|
|
110
|
+
async dblclick(ref) {
|
|
111
|
+
await this.runOrThrow("dblclick", ref);
|
|
112
|
+
}
|
|
113
|
+
async fill(ref, value) {
|
|
114
|
+
await this.runOrThrow("fill", ref, value);
|
|
115
|
+
}
|
|
116
|
+
async select(ref, value) {
|
|
117
|
+
await this.runOrThrow("select", ref, value);
|
|
118
|
+
}
|
|
119
|
+
async check(ref) {
|
|
120
|
+
await this.runOrThrow("check", ref);
|
|
121
|
+
}
|
|
122
|
+
async uncheck(ref) {
|
|
123
|
+
await this.runOrThrow("uncheck", ref);
|
|
124
|
+
}
|
|
125
|
+
async press(key) {
|
|
126
|
+
await this.runOrThrow("press", key);
|
|
127
|
+
}
|
|
128
|
+
async type(text) {
|
|
129
|
+
await this.runOrThrow("type", text);
|
|
130
|
+
}
|
|
131
|
+
async hover(ref) {
|
|
132
|
+
await this.runOrThrow("hover", ref);
|
|
133
|
+
}
|
|
134
|
+
async focus(ref) {
|
|
135
|
+
await this.runOrThrow("focus", ref);
|
|
136
|
+
}
|
|
137
|
+
async scroll(direction, px) {
|
|
138
|
+
const args = ["scroll", direction];
|
|
139
|
+
if (px !== void 0) args.push(String(px));
|
|
140
|
+
await this.runOrThrow(...args);
|
|
141
|
+
}
|
|
142
|
+
async upload(ref, file) {
|
|
143
|
+
await this.runOrThrow("upload", ref, file);
|
|
144
|
+
}
|
|
145
|
+
async screenshot(opts) {
|
|
146
|
+
await ensureDir(SHOTS_DIR);
|
|
147
|
+
shotCounter++;
|
|
148
|
+
const defaultPath = join(SHOTS_DIR, `shot-${String(shotCounter).padStart(3, "0")}.png`);
|
|
149
|
+
const targetPath = opts?.path ?? defaultPath;
|
|
150
|
+
const args = ["screenshot", targetPath];
|
|
151
|
+
if (opts?.annotate) args.push("--annotate");
|
|
152
|
+
if (opts?.full) args.push("--full");
|
|
153
|
+
if (opts?.selector) args.push("--selector", opts.selector);
|
|
154
|
+
const data = await this.runOrThrow(...args);
|
|
155
|
+
return String(data["path"] ?? targetPath);
|
|
156
|
+
}
|
|
157
|
+
async eval(js) {
|
|
158
|
+
const data = await this.runOrThrow("eval", js);
|
|
159
|
+
return String(data["text"] ?? data["result"] ?? JSON.stringify(data));
|
|
160
|
+
}
|
|
161
|
+
async text(selector) {
|
|
162
|
+
const args = ["get", "text"];
|
|
163
|
+
if (selector) args.push(selector);
|
|
164
|
+
const data = await this.runOrThrow(...args);
|
|
165
|
+
return String(data["text"] ?? "");
|
|
166
|
+
}
|
|
167
|
+
async console(opts) {
|
|
168
|
+
const args = ["console"];
|
|
169
|
+
if (opts?.clear) args.push("--clear");
|
|
170
|
+
const data = await this.runOrThrow(...args);
|
|
171
|
+
const messages = data["messages"] ?? [];
|
|
172
|
+
if (opts?.level) return messages.filter((m) => m.level === opts.level);
|
|
173
|
+
return messages;
|
|
174
|
+
}
|
|
175
|
+
async errors() {
|
|
176
|
+
const data = await this.runOrThrow("errors");
|
|
177
|
+
return data["errors"] ?? [];
|
|
178
|
+
}
|
|
179
|
+
async network(opts) {
|
|
180
|
+
const args = ["network", "requests"];
|
|
181
|
+
if (opts?.method) args.push("--method", opts.method);
|
|
182
|
+
if (opts?.grep) args.push("--filter", opts.grep);
|
|
183
|
+
const data = await this.runOrThrow(...args);
|
|
184
|
+
let entries = data["requests"] ?? [];
|
|
185
|
+
if (opts?.failed) entries = entries.filter((e) => e.status >= 400);
|
|
186
|
+
return entries;
|
|
187
|
+
}
|
|
188
|
+
async wait(opts) {
|
|
189
|
+
const args = ["wait"];
|
|
190
|
+
const timeout = opts.timeout ?? 3e4;
|
|
191
|
+
if (opts.ref) args.push(opts.ref);
|
|
192
|
+
else if (opts.selector) args.push(opts.selector);
|
|
193
|
+
else if (opts.url) args.push("--url", opts.url);
|
|
194
|
+
else if (opts.text) args.push("--text", opts.text);
|
|
195
|
+
else if (opts.network === "idle") args.push("--network", "idle");
|
|
196
|
+
if (opts.hidden) args.push("--hidden");
|
|
197
|
+
args.push("--timeout", String(timeout));
|
|
198
|
+
await this.runOrThrow(...args);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
export { AgentBrowserDriver };
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import "./duration-DUrbfMLK.js";
|
|
2
|
+
import { loadProbeConfig, resolveBaseUrl } from "./config-BUEMgFYN.js";
|
|
3
|
+
import { AgentBrowserDriver } from "./agent-browser-Cxuu-Zz0.js";
|
|
4
|
+
import { error, success } from "./output-CHUjdVDf.js";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import { ofetch } from "ofetch";
|
|
7
|
+
|
|
8
|
+
//#region src/commands/assert.ts
|
|
9
|
+
async function getDriver() {
|
|
10
|
+
const config = await loadProbeConfig();
|
|
11
|
+
return new AgentBrowserDriver({
|
|
12
|
+
session: config.browser?.session ?? "qprobe",
|
|
13
|
+
baseUrl: resolveBaseUrl(config)
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const textAssert = defineCommand({
|
|
17
|
+
meta: {
|
|
18
|
+
name: "text",
|
|
19
|
+
description: "Assert page contains text"
|
|
20
|
+
},
|
|
21
|
+
args: { text: {
|
|
22
|
+
type: "positional",
|
|
23
|
+
description: "Expected text",
|
|
24
|
+
required: true
|
|
25
|
+
} },
|
|
26
|
+
async run({ args }) {
|
|
27
|
+
const driver = await getDriver();
|
|
28
|
+
const content = await driver.text();
|
|
29
|
+
if (content.includes(args.text)) success(`Page contains "${args.text}"`);
|
|
30
|
+
else {
|
|
31
|
+
error(`Page does not contain "${args.text}"`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const noTextAssert = defineCommand({
|
|
37
|
+
meta: {
|
|
38
|
+
name: "no-text",
|
|
39
|
+
description: "Assert page does NOT contain text"
|
|
40
|
+
},
|
|
41
|
+
args: { text: {
|
|
42
|
+
type: "positional",
|
|
43
|
+
required: true
|
|
44
|
+
} },
|
|
45
|
+
async run({ args }) {
|
|
46
|
+
const driver = await getDriver();
|
|
47
|
+
const content = await driver.text();
|
|
48
|
+
if (!content.includes(args.text)) success(`Page does not contain "${args.text}"`);
|
|
49
|
+
else {
|
|
50
|
+
error(`Page contains "${args.text}" (unexpected)`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const elementAssert = defineCommand({
|
|
56
|
+
meta: {
|
|
57
|
+
name: "element",
|
|
58
|
+
description: "Assert element exists"
|
|
59
|
+
},
|
|
60
|
+
args: {
|
|
61
|
+
selector: {
|
|
62
|
+
type: "positional",
|
|
63
|
+
description: "Ref or CSS selector",
|
|
64
|
+
required: true
|
|
65
|
+
},
|
|
66
|
+
text: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Assert element has this text"
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async run({ args }) {
|
|
72
|
+
const driver = await getDriver();
|
|
73
|
+
const snapshot = await driver.snapshot({
|
|
74
|
+
interactive: true,
|
|
75
|
+
compact: true
|
|
76
|
+
});
|
|
77
|
+
if (snapshot.includes(args.selector)) success(`Element "${args.selector}" exists`);
|
|
78
|
+
else {
|
|
79
|
+
error(`Element "${args.selector}" not found`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
const urlAssert = defineCommand({
|
|
85
|
+
meta: {
|
|
86
|
+
name: "url",
|
|
87
|
+
description: "Assert URL matches pattern"
|
|
88
|
+
},
|
|
89
|
+
args: { pattern: {
|
|
90
|
+
type: "positional",
|
|
91
|
+
description: "URL pattern",
|
|
92
|
+
required: true
|
|
93
|
+
} },
|
|
94
|
+
async run({ args }) {
|
|
95
|
+
const driver = await getDriver();
|
|
96
|
+
const currentUrl = await driver.url();
|
|
97
|
+
const regex = new RegExp(args.pattern);
|
|
98
|
+
if (regex.test(currentUrl)) success(`URL matches "${args.pattern}" (${currentUrl})`);
|
|
99
|
+
else {
|
|
100
|
+
error(`URL "${currentUrl}" does not match "${args.pattern}"`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const titleAssert = defineCommand({
|
|
106
|
+
meta: {
|
|
107
|
+
name: "title",
|
|
108
|
+
description: "Assert page title contains text"
|
|
109
|
+
},
|
|
110
|
+
args: { text: {
|
|
111
|
+
type: "positional",
|
|
112
|
+
required: true
|
|
113
|
+
} },
|
|
114
|
+
async run({ args }) {
|
|
115
|
+
const driver = await getDriver();
|
|
116
|
+
const pageTitle = await driver.title();
|
|
117
|
+
if (pageTitle.includes(args.text)) success(`Title contains "${args.text}" (${pageTitle})`);
|
|
118
|
+
else {
|
|
119
|
+
error(`Title "${pageTitle}" does not contain "${args.text}"`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const statusAssert = defineCommand({
|
|
125
|
+
meta: {
|
|
126
|
+
name: "status",
|
|
127
|
+
description: "Assert HTTP endpoint returns status"
|
|
128
|
+
},
|
|
129
|
+
args: {
|
|
130
|
+
code: {
|
|
131
|
+
type: "positional",
|
|
132
|
+
description: "Expected status code",
|
|
133
|
+
required: true
|
|
134
|
+
},
|
|
135
|
+
path: {
|
|
136
|
+
type: "positional",
|
|
137
|
+
description: "URL path",
|
|
138
|
+
required: true
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async run({ args }) {
|
|
142
|
+
const config = await loadProbeConfig();
|
|
143
|
+
const base = resolveBaseUrl(config);
|
|
144
|
+
const url = args.path.startsWith("http") ? args.path : `${base}${args.path}`;
|
|
145
|
+
const expected = Number(args.code);
|
|
146
|
+
try {
|
|
147
|
+
const response = await ofetch.raw(url, { ignoreResponseError: true });
|
|
148
|
+
if (response.status === expected) success(`${url} returned ${expected}`);
|
|
149
|
+
else {
|
|
150
|
+
error(`${url} returned ${response.status}, expected ${expected}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
error(`Failed to reach ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const noErrorsAssert = defineCommand({
|
|
160
|
+
meta: {
|
|
161
|
+
name: "no-errors",
|
|
162
|
+
description: "Assert no JS console errors"
|
|
163
|
+
},
|
|
164
|
+
args: {},
|
|
165
|
+
async run() {
|
|
166
|
+
const driver = await getDriver();
|
|
167
|
+
const errs = await driver.errors();
|
|
168
|
+
if (errs.length === 0) success("No JS errors");
|
|
169
|
+
else {
|
|
170
|
+
error(`Found ${errs.length} JS error(s):`);
|
|
171
|
+
for (const e of errs) error(` ${e.message}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
const noNetworkErrorsAssert = defineCommand({
|
|
177
|
+
meta: {
|
|
178
|
+
name: "no-network-errors",
|
|
179
|
+
description: "Assert no 4xx/5xx network errors"
|
|
180
|
+
},
|
|
181
|
+
args: {},
|
|
182
|
+
async run() {
|
|
183
|
+
const driver = await getDriver();
|
|
184
|
+
const entries = await driver.network({ failed: true });
|
|
185
|
+
if (entries.length === 0) success("No network errors");
|
|
186
|
+
else {
|
|
187
|
+
error(`Found ${entries.length} network error(s):`);
|
|
188
|
+
for (const e of entries) error(` ${e.method} ${e.status} ${e.url}`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const command = defineCommand({
|
|
194
|
+
meta: {
|
|
195
|
+
name: "assert",
|
|
196
|
+
description: "Run assertions against browser/server state"
|
|
197
|
+
},
|
|
198
|
+
subCommands: {
|
|
199
|
+
text: textAssert,
|
|
200
|
+
"no-text": noTextAssert,
|
|
201
|
+
element: elementAssert,
|
|
202
|
+
url: urlAssert,
|
|
203
|
+
title: titleAssert,
|
|
204
|
+
status: statusAssert,
|
|
205
|
+
"no-errors": noErrorsAssert,
|
|
206
|
+
"no-network-errors": noNetworkErrorsAssert
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
var assert_default = command;
|
|
210
|
+
|
|
211
|
+
//#endregion
|
|
212
|
+
export { assert_default as default };
|