@ait-co/devtools 0.1.113 → 0.1.116

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.
Files changed (71) hide show
  1. package/dist/bundle-KFs4t-wc.d.ts.map +1 -1
  2. package/dist/cli-BLBo0lkP.js +2466 -0
  3. package/dist/cli-BLBo0lkP.js.map +1 -0
  4. package/dist/debug-server-DcAKrbnq.js +921 -0
  5. package/dist/debug-server-DcAKrbnq.js.map +1 -0
  6. package/dist/debug-server-DlXJARIC.js +8171 -0
  7. package/dist/debug-server-DlXJARIC.js.map +1 -0
  8. package/dist/mcp/cli.js +3 -7771
  9. package/dist/mcp/cli.js.map +1 -1
  10. package/dist/mcp/server.js +11 -2
  11. package/dist/mcp/server.js.map +1 -1
  12. package/dist/panel/index.js +7 -1
  13. package/dist/panel/index.js.map +1 -1
  14. package/dist/{pool-mZlgCmNQ.d.ts → pool-htlVnEFl.d.ts} +2 -2
  15. package/dist/{pool-mZlgCmNQ.d.ts.map → pool-htlVnEFl.d.ts.map} +1 -1
  16. package/dist/{qr-http-server-BVS-HZjU.cjs → qr-http-server-BpTvfeku.cjs} +92 -8
  17. package/dist/qr-http-server-BpTvfeku.cjs.map +1 -0
  18. package/dist/{qr-http-server-BJJt3ush.js → qr-http-server-BsWlOA85.js} +92 -8
  19. package/dist/qr-http-server-BsWlOA85.js.map +1 -0
  20. package/dist/{qr-http-server-C1T4RNbq.cjs → qr-http-server-CrVKno9V.cjs} +92 -8
  21. package/dist/qr-http-server-CrVKno9V.cjs.map +1 -0
  22. package/dist/{qr-http-server-Cs93vEPH.js → qr-http-server-UBV2qUEd.js} +92 -8
  23. package/dist/qr-http-server-UBV2qUEd.js.map +1 -0
  24. package/dist/relay-secret-store-BHcOmaNK.js +3 -0
  25. package/dist/relay-secret-store-CkA7KNUb.js +154 -0
  26. package/dist/{relay-secret-store-DhzAnnj-.js.map → relay-secret-store-CkA7KNUb.js.map} +1 -1
  27. package/dist/{relay-secret-store-DhzAnnj-.js → relay-secret-store-CmqchhR5.js} +3 -3
  28. package/dist/relay-secret-store-CmqchhR5.js.map +1 -0
  29. package/dist/{relay-url-store-CwKT7i04.js → relay-url-store-C0qukm3R.js} +2 -2
  30. package/dist/{relay-url-store-CwKT7i04.js.map → relay-url-store-C0qukm3R.js.map} +1 -1
  31. package/dist/relay-url-store-NDtEcOE-.js +123 -0
  32. package/dist/relay-url-store-NDtEcOE-.js.map +1 -0
  33. package/dist/{relay-worker-Dppp2yZj.d.ts → relay-worker-DWW4vZxU.d.ts} +2 -2
  34. package/dist/{relay-worker-Dppp2yZj.d.ts.map → relay-worker-DWW4vZxU.d.ts.map} +1 -1
  35. package/dist/runtime-BU-iMHz6.d.ts +192 -0
  36. package/dist/runtime-BU-iMHz6.d.ts.map +1 -0
  37. package/dist/test-runner/bundle.js +72 -2
  38. package/dist/test-runner/bundle.js.map +1 -1
  39. package/dist/test-runner/cli.d.ts +19 -7
  40. package/dist/test-runner/cli.d.ts.map +1 -1
  41. package/dist/test-runner/cli.js +1 -550
  42. package/dist/test-runner/config.d.ts +1 -1
  43. package/dist/test-runner/pool.d.ts +1 -1
  44. package/dist/test-runner/relay-worker.d.ts +1 -1
  45. package/dist/test-runner/rpc.d.ts +1 -1
  46. package/dist/test-runner/runtime.d.ts +2 -2
  47. package/dist/test-runner/runtime.js +407 -21
  48. package/dist/test-runner/runtime.js.map +1 -1
  49. package/dist/test-runner/task-graph.d.ts +1 -1
  50. package/dist/totp-D104cJQN.js +3 -0
  51. package/dist/{totp-WY6l0ysP.js → totp-D70zD5tJ.js} +1 -1
  52. package/dist/{totp-WY6l0ysP.js.map → totp-D70zD5tJ.js.map} +1 -1
  53. package/dist/totp-DvOYkYim.js +212 -0
  54. package/dist/totp-DvOYkYim.js.map +1 -0
  55. package/dist/{tunnel-Cpn3mA4u.js → tunnel-B7U5k1xa.js} +2 -2
  56. package/dist/{tunnel-Cpn3mA4u.js.map → tunnel-B7U5k1xa.js.map} +1 -1
  57. package/dist/{tunnel-Dj8Kf2QS.cjs → tunnel-C-cgMnyY.cjs} +2 -2
  58. package/dist/{tunnel-Dj8Kf2QS.cjs.map → tunnel-C-cgMnyY.cjs.map} +1 -1
  59. package/dist/unplugin/index.cjs +1 -1
  60. package/dist/unplugin/index.js +1 -1
  61. package/dist/unplugin/tunnel.cjs +1 -1
  62. package/dist/unplugin/tunnel.js +1 -1
  63. package/package.json +1 -1
  64. package/dist/qr-http-server-BJJt3ush.js.map +0 -1
  65. package/dist/qr-http-server-BVS-HZjU.cjs.map +0 -1
  66. package/dist/qr-http-server-C1T4RNbq.cjs.map +0 -1
  67. package/dist/qr-http-server-Cs93vEPH.js.map +0 -1
  68. package/dist/runtime-C7uxh3Mf.d.ts +0 -62
  69. package/dist/runtime-C7uxh3Mf.d.ts.map +0 -1
  70. package/dist/test-runner/cli.js.map +0 -1
  71. package/dist/totp-D1pulXLa.js +0 -3
@@ -1,552 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs } from "node:util";
3
- import * as fs from "node:fs/promises";
4
- import { glob } from "node:fs/promises";
5
- import * as path from "node:path";
6
- import { isAbsolute, resolve } from "node:path";
7
- import { accessSync } from "node:fs";
8
- import { fileURLToPath } from "node:url";
9
- //#region src/test-runner/discover.ts
10
- /**
11
- * Test-file discovery shared by the `devtools-test` CLI and the `run_tests`
12
- * MCP tool, so both expand glob patterns with identical semantics.
13
- *
14
- * Uses Node's built-in `fs/promises` `glob` (Node 22+) — no extra dependency,
15
- * which keeps the MCP daemon install graph lean (a plain glob lib would land in
16
- * the `npx … devtools-mcp` path for no benefit).
17
- *
18
- * Pure Node IO only (`node:fs/promises` + `node:path`) — react-free, so it is
19
- * safe to import from the MCP daemon graph.
20
- */
21
- /**
22
- * Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of
23
- * ABSOLUTE test file paths, resolved relative to `cwd`.
24
- *
25
- * A plain (non-glob) path passes through when it matches a real file; a glob
26
- * expands against `cwd`. Absolute matches are kept as-is; relative matches are
27
- * resolved against `cwd`. `bundleTestFile` requires an absolute path, so the
28
- * absolute output feeds it directly.
29
- *
30
- * @param patterns Glob patterns or file paths (e.g. `['src/**\/*.ait.test.ts']`).
31
- * @param cwd Base directory for relative patterns/results.
32
- * @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.
33
- */
34
- async function discoverTestFiles(patterns, cwd) {
35
- const out = /* @__PURE__ */ new Set();
36
- for await (const match of glob(patterns, { cwd })) out.add(isAbsolute(match) ? match : resolve(cwd, match));
37
- return [...out].sort();
38
- }
39
- //#endregion
40
- //#region src/test-runner/bundle.ts
41
- /**
42
- * esbuild-based bundler for user test files.
43
- *
44
- * Bundles a single test file into a self-contained IIFE string that can be
45
- * injected into a WebView via `Runtime.evaluate`. The bundle includes the
46
- * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and
47
- * the `runTestModule(factory)` entry point.
48
- *
49
- * ## How the wiring works
50
- *
51
- * The bundle exposes two exports on `globalThis.__testBundle`:
52
- * - `runTestModule` — the runtime's entry function.
53
- * - `__userFactory` — an async function whose body is the user's top-level
54
- * test registration code (describe/it/test calls).
55
- *
56
- * The Node-side RPC (`rpc.ts`) calls:
57
- * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
58
- *
59
- * `runTestModule` then installs `describe/it/test/expect` as globals, invokes
60
- * the factory (which registers all tests), runs them, and returns a `RunReport`.
61
- *
62
- * ## Why a factory wrapper is needed
63
- *
64
- * Naively adding the runtime to `entryPoints` and bundling the user file would
65
- * fail for two reasons:
66
- * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE
67
- * scope. The user's top-level `describe(...)` calls expect them as globals —
68
- * they are not globals until `runTestModule` installs them.
69
- * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation
70
- * time, before the RPC layer calls `runTestModule` to reset state and start
71
- * the test clock.
72
- *
73
- * The factory approach solves both: the user's registration code is deferred
74
- * into a function that `runTestModule` calls AFTER installing the globals.
75
- *
76
- * ## Factory extraction algorithm
77
- *
78
- * The `userFactoryPlugin` reads the user file and splits lines into:
79
- * - **top-level**: `import …` and re-export lines — kept at module scope
80
- * (the only valid position for static `import` in ESM).
81
- * - **body**: all other statements — moved into the body of the exported
82
- * `__userFactory` async function.
83
- *
84
- * esbuild processes the re-generated module, following each static import
85
- * through the normal dependency graph (including the SDK-redirect plugin).
86
- *
87
- * ## SDK redirect
88
- *
89
- * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via
90
- * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that
91
- * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.
92
- *
93
- * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
94
- */
95
- /**
96
- * Matches the bare SDK package and any sub-path import
97
- * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).
98
- * Built from {@link SDK_PACKAGE} so the package name has a single source.
99
- */
100
- const SDK_IMPORT_FILTER = new RegExp(`^${"@apps-in-toss/web-framework".replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
101
- /**
102
- * esbuild plugin that intercepts SDK imports and redirects them to the
103
- * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.
104
- *
105
- * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),
106
- * esbuild resolves it to a virtual module that re-exports all named exports
107
- * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not
108
- * be available in the test environment) while still making named imports work.
109
- *
110
- * If `window.__sdk` is absent (non-dog-food build), every access throws a
111
- * descriptive error rather than returning `undefined` silently.
112
- */
113
- function sdkRedirectPlugin() {
114
- return {
115
- name: "sdk-redirect",
116
- setup(build) {
117
- build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({
118
- path: args.path,
119
- namespace: "sdk-redirect"
120
- }));
121
- build.onLoad({
122
- filter: /.*/,
123
- namespace: "sdk-redirect"
124
- }, () => ({
125
- contents: `
126
- var __proxy = (typeof window !== 'undefined' && window.__sdk)
127
- ? window.__sdk
128
- : new Proxy({}, {
129
- get: function(_t, p) {
130
- throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
131
- }
132
- });
133
- module.exports = __proxy;
134
- `,
135
- loader: "js"
136
- }));
137
- }
138
- };
139
- }
140
- /**
141
- * esbuild plugin that transforms the user test file into a module that exports
142
- * an async `__userFactory` function. The factory defers the user's top-level
143
- * test registration code (describe/it/test calls) so it only runs when
144
- * `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has
145
- * installed describe/it/test/expect as globals.
146
- *
147
- * Algorithm:
148
- * - Import declarations and re-export statements are kept at module top-level
149
- * (the only valid ESM position for static `import`). A statement that spans
150
- * multiple lines — e.g. a named import with one member per line:
151
- * import {
152
- * appLogin,
153
- * getAnonymousKey,
154
- * } from '@apps-in-toss/web-framework';
155
- * is tracked as a single block: every line from the opening `import {` /
156
- * `export {` through the closing `from '…'` (or side-effect `'…'`) line is
157
- * kept together at top-level. This prevents the member lines and the
158
- * closing `} from '…'` line from leaking into the factory body, which would
159
- * leave an unterminated `import {` at module scope (the #678 env3 failure:
160
- * esbuild threw `Expected "as" but found "{"` on multi-line SDK imports).
161
- * - All other lines (describe/it/test calls, local declarations, etc.) are
162
- * moved into the body of the exported async factory function.
163
- *
164
- * This preserves SDK import resolution (the sdk-redirect plugin processes
165
- * top-level imports normally) while deferring test registration to the factory.
166
- */
167
- function userFactoryPlugin(absPath) {
168
- const NAMESPACE = "user-test-factory";
169
- return {
170
- name: "user-test-factory",
171
- setup(build) {
172
- build.onResolve({ filter: /^user-test-factory$/ }, () => ({
173
- path: absPath,
174
- namespace: NAMESPACE
175
- }));
176
- build.onLoad({
177
- filter: /.*/,
178
- namespace: NAMESPACE
179
- }, async (args) => {
180
- const lines = (await fs.readFile(args.path, "utf8")).split("\n");
181
- const topLevelLines = [];
182
- const bodyLines = [];
183
- const EXPORT_DECLARATION_RE = /^(export\s+)(default\s+|async\s+function\s+|function\s+|class\s+|const\s+|let\s+|var\s+)/;
184
- const isImportStart = (trimmed) => trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import'") || trimmed.startsWith("import\"");
185
- const endsStatement = (trimmed) => /['"]\s*;?\s*$/.test(trimmed.replace(/\/\/.*$/, "").trimEnd());
186
- let inImportBlock = false;
187
- for (const line of lines) {
188
- const trimmed = line.trimStart();
189
- const indent = line.slice(0, line.length - trimmed.length);
190
- if (inImportBlock) {
191
- topLevelLines.push(line);
192
- if (endsStatement(trimmed)) inImportBlock = false;
193
- continue;
194
- }
195
- if (isImportStart(trimmed)) {
196
- topLevelLines.push(line);
197
- if (!endsStatement(trimmed)) inImportBlock = true;
198
- } else if (trimmed.startsWith("export ")) if (trimmed.match(EXPORT_DECLARATION_RE)) bodyLines.push(indent + trimmed.slice(7));
199
- else {
200
- topLevelLines.push(line);
201
- if (/\bfrom\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith("{")) inImportBlock = true;
202
- }
203
- else bodyLines.push(line);
204
- }
205
- return {
206
- contents: [
207
- ...topLevelLines,
208
- "",
209
- "// biome-ignore lint: generated factory wrapper",
210
- "export default async function __userFactory(): Promise<void> {",
211
- ...bodyLines.map((l) => ` ${l}`),
212
- "}"
213
- ].join("\n"),
214
- loader: "ts",
215
- resolveDir: path.dirname(absPath)
216
- };
217
- });
218
- }
219
- };
220
- }
221
- /**
222
- * Returns the absolute path to the test-runner runtime module.
223
- *
224
- * Searches candidates in priority order:
225
- * 1. Co-located `runtime.ts` / `runtime.js` — covers the source tree
226
- * (tsx / ts-node) and the `dist/test-runner/` entry.
227
- * 2. `../test-runner/runtime.js` — covers the `dist/mcp/cli.js` entry,
228
- * where `import.meta.url` resolves to `dist/mcp/` (a sibling directory
229
- * of `dist/test-runner/`). Without this second candidate the MCP entry
230
- * point would look for `dist/mcp/runtime.js`, which does not exist, and
231
- * every `run_tests` call would fail with an esbuild "Could not resolve"
232
- * error (#678).
233
- *
234
- * Returns the first candidate that exists on disk. Falls back to the
235
- * co-located `runtime.js` path so esbuild produces a clear "file not found"
236
- * error rather than a cryptic failure.
237
- */
238
- function getRuntimePath() {
239
- const dir = path.dirname(fileURLToPath(import.meta.url));
240
- const candidates = [
241
- path.join(dir, "runtime.ts"),
242
- path.join(dir, "runtime.js"),
243
- path.join(dir, "..", "test-runner", "runtime.js")
244
- ];
245
- for (const candidate of candidates) try {
246
- accessSync(candidate);
247
- return candidate;
248
- } catch {}
249
- return path.join(dir, "runtime.js");
250
- }
251
- /**
252
- * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
253
- *
254
- * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
255
- * - `runTestModule` — the runtime entry (from `runtime.ts`).
256
- * - `__userFactory` — an async function wrapping the user's test registration
257
- * code so it runs AFTER `runTestModule` installs the globals.
258
- *
259
- * Callers (rpc.ts) invoke:
260
- * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
261
- *
262
- * @param absPath - Absolute path to the user test file.
263
- * @param opts - Optional bundling overrides.
264
- */
265
- async function bundleTestFile(absPath, opts) {
266
- const globalName = opts?.globalName ?? "__testBundle";
267
- const extraExternals = opts?.extraExternals ?? [];
268
- const esbuild = await import("esbuild");
269
- const runtimePath = getRuntimePath();
270
- const wrapperContent = [
271
- `import { runTestModule } from ${JSON.stringify(runtimePath)};`,
272
- `import __userFactory from "user-test-factory";`,
273
- `export { runTestModule, __userFactory };`
274
- ].join("\n");
275
- const result = await esbuild.build({
276
- stdin: {
277
- contents: wrapperContent,
278
- loader: "ts",
279
- resolveDir: path.dirname(absPath)
280
- },
281
- bundle: true,
282
- format: "iife",
283
- globalName,
284
- platform: "browser",
285
- target: "es2022",
286
- write: false,
287
- plugins: [userFactoryPlugin(absPath), sdkRedirectPlugin()],
288
- external: extraExternals,
289
- treeShaking: true,
290
- footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
291
- });
292
- const warnings = result.warnings.map((w) => `${path.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
293
- const outputFile = result.outputFiles?.[0];
294
- if (!outputFile) throw new Error("bundleTestFile: esbuild produced no output — check entryPoints");
295
- return {
296
- code: outputFile.text,
297
- warnings
298
- };
299
- }
300
- //#endregion
301
- //#region src/test-runner/rpc.ts
302
- /** Maximum milliseconds to wait for a single evaluate round-trip. */
303
- const DEFAULT_TIMEOUT_MS = 3e4;
304
- /**
305
- * Wraps bundle code in a self-executing IIFE that:
306
- * 1. Evaluates the bundle (registering describe/it/test).
307
- * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.
308
- * 3. Returns a JSON-serialised `RunReport` string.
309
- *
310
- * The double-serialisation (RunReport → JSON string → returnByValue string)
311
- * is intentional: CDP `returnByValue` reliably transports strings; deeply
312
- * nested objects can lose fidelity across the Chii relay.
313
- *
314
- * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
315
- */
316
- function buildRunTestsExpression(bundleCode) {
317
- return `(async () => { try { ${bundleCode} } catch(e) { return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)}); } if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') { return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'}); } try { const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory); return JSON.stringify({ok:true,value:report}); } catch(e) { return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)}); }})()`;
318
- }
319
- /**
320
- * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
321
- * evaluate call into a typed `RpcRunResult`.
322
- *
323
- * Throws only on parse failure — an `ok:false` envelope is a normal result.
324
- *
325
- * SECRET-HANDLING: `rawValue` is not included in error messages.
326
- */
327
- function parseRunTestsResult(rawValue) {
328
- if (typeof rawValue !== "string") throw new Error(`rpc.parseRunTestsResult: unexpected return type "${typeof rawValue}" — expected JSON string`);
329
- let parsed;
330
- try {
331
- parsed = JSON.parse(rawValue);
332
- } catch {
333
- throw new Error("rpc.parseRunTestsResult: bridge returned non-JSON string");
334
- }
335
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("rpc.parseRunTestsResult: parsed result is not an object");
336
- const obj = parsed;
337
- if (obj.ok === true) return {
338
- ok: true,
339
- report: obj.value
340
- };
341
- if (obj.ok === false) return {
342
- ok: false,
343
- error: typeof obj.error === "string" ? obj.error : String(obj.error)
344
- };
345
- throw new Error("rpc.parseRunTestsResult: result missing \"ok\" field");
346
- }
347
- /**
348
- * Injects `bundleCode` into the attached page and awaits test execution.
349
- *
350
- * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the
351
- * async IIFE to settle. The 30-second CDP command timeout covers even
352
- * long-running test suites; split into smaller files if you hit it.
353
- *
354
- * @param connection - Active CDP connection (relay or local).
355
- * @param bundleCode - IIFE bundle string from `bundleTestFile`.
356
- * @param timeoutMs - Override the default 30 s timeout.
357
- *
358
- * SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.
359
- */
360
- async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TIMEOUT_MS) {
361
- const expression = buildRunTestsExpression(bundleCode);
362
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs));
363
- const evalPromise = connection.send("Runtime.evaluate", {
364
- expression,
365
- returnByValue: true,
366
- awaitPromise: true
367
- });
368
- const cdpResult = await Promise.race([evalPromise, timeoutPromise]);
369
- if (cdpResult.exceptionDetails) {
370
- const msg = cdpResult.exceptionDetails.exception?.description ?? cdpResult.exceptionDetails.text ?? "Runtime.evaluate threw an exception";
371
- throw new Error(`rpc.injectAndRunBundle: ${msg}`);
372
- }
373
- return parseRunTestsResult(cdpResult.result.value);
374
- }
375
- //#endregion
376
- //#region src/test-runner/relay-worker.ts
377
- /**
378
- * Runs all `files` sequentially over the given CDP `connection`.
379
- *
380
- * For each file:
381
- * 1. Bundle with esbuild (includes SDK shim + runtime).
382
- * 2. Inject into the attached page via `Runtime.evaluate`.
383
- * 3. Await the `RunReport` JSON response.
384
- * 4. Accumulate results.
385
- *
386
- * Returns a `RelayRunReport` with per-file results and flattened totals.
387
- *
388
- * This function does NOT open or manage the relay connection — the caller
389
- * is responsible for attaching and closing it.
390
- *
391
- * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
392
- * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
393
- *
394
- * @param connection - Active CDP connection (relay or local kind).
395
- * @param files - Absolute paths to test files, run in order.
396
- * @param opts - Optional per-run overrides.
397
- */
398
- async function runTestFilesOverRelay(connection, files, opts) {
399
- const wallStart = Date.now();
400
- const startedAt = new Date(wallStart).toISOString();
401
- const fileResults = [];
402
- for (const file of files) {
403
- let fileEntry;
404
- try {
405
- const { code } = await bundleTestFile(file, opts?.bundleOptions);
406
- const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
407
- if (rpcResult.ok) fileEntry = {
408
- file,
409
- result: rpcResult.report
410
- };
411
- else fileEntry = {
412
- file,
413
- result: { error: rpcResult.error }
414
- };
415
- } catch (e) {
416
- fileEntry = {
417
- file,
418
- result: { error: e instanceof Error ? e.message : String(e) }
419
- };
420
- }
421
- fileResults.push(fileEntry);
422
- }
423
- const totals = fileResults.reduce((acc, { result }) => {
424
- if ("error" in result) {
425
- acc.failed += 1;
426
- acc.total += 1;
427
- } else {
428
- acc.passed += result.passed;
429
- acc.failed += result.failed;
430
- acc.skipped += result.skipped;
431
- acc.total += result.passed + result.failed + result.skipped;
432
- }
433
- return acc;
434
- }, {
435
- passed: 0,
436
- failed: 0,
437
- skipped: 0,
438
- total: 0
439
- });
440
- return {
441
- startedAt,
442
- duration: Date.now() - wallStart,
443
- files: fileResults,
444
- totals
445
- };
446
- }
447
- //#endregion
448
- //#region src/test-runner/cli.ts
449
- /**
450
- * `devtools-test` CLI.
451
- *
452
- * Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)
453
- * and exposes `runWithConnection` — the pure run core that bundles, injects, and
454
- * collects each file over a CDP connection. Today the run path that has a live
455
- * connection is the `run_tests` MCP tool (it runs these files against the
456
- * daemon's attached page); the CLI's own standalone relay attach (resolve CDP
457
- * URL → attach → run → close) is not wired yet, so `main()` resolves the matched
458
- * files and points the operator at the MCP tool.
459
- *
460
- * NOTE: no shebang in this source file — the tsdown entry's `banner` option
461
- * injects `#!/usr/bin/env node` into the compiled output (same pattern as
462
- * `src/mcp/cli.ts`).
463
- */
464
- const USAGE = `
465
- devtools-test — run mini-app tests on a real device WebView over the CDP relay
466
-
467
- USAGE
468
- devtools-test <glob> [<glob> ...] [options]
469
-
470
- OPTIONS
471
- --timeout <ms> Per-file evaluate timeout in ms (default: 30000)
472
- --help, -h Show this help message
473
-
474
- DESCRIPTION
475
- Bundles each matched test file with esbuild (SDK imports redirected to
476
- window.__sdk), injects the bundle into the attached WebView via
477
- Runtime.evaluate, and returns a RunReport.
478
-
479
- A live CDP relay connection must be active before running tests. Use the
480
- \`run_tests\` MCP tool (via \`devtools-mcp\` / \`/ait debug\`) to run these files
481
- against an attached page — the CLI's own standalone relay attach is not wired
482
- yet (it currently resolves the matched files and defers to that tool).
483
-
484
- EXAMPLE
485
- devtools-test 'src/**/*.ait.test.ts' --timeout 60000
486
-
487
- `.trimStart();
488
- /**
489
- * Runs `files` over `connection` and returns the aggregate report.
490
- * This pure function is the testable core of the CLI (and is what the
491
- * `run_tests` MCP tool calls against the daemon's attached connection); it is
492
- * separate from `main()` so tests can call it without spawning a subprocess.
493
- *
494
- * A standalone CLI relay attach/detach lifecycle (connect via Chii relay URL,
495
- * `enableDomains`, run, then close) is not wired into `main()` yet.
496
- */
497
- async function runWithConnection(connection, files, opts) {
498
- const report = await runTestFilesOverRelay(connection, files, opts);
499
- if (opts?.printSummary) {
500
- const { totals } = report;
501
- process.stdout.write(`\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\n`);
502
- }
503
- return report;
504
- }
505
- /**
506
- * CLI entry point.
507
- *
508
- * Resolves the matched test files and prints a "relay attach required" notice:
509
- * the CLI's own standalone relay attach (resolve CDP URL, attach, run, close) is
510
- * not wired yet, so today these files run via the `run_tests` MCP tool against
511
- * the daemon's attached page.
512
- */
513
- async function main(argv = process.argv.slice(2)) {
514
- let parsed;
515
- try {
516
- parsed = parseArgs({
517
- args: argv,
518
- options: {
519
- help: {
520
- type: "boolean",
521
- short: "h"
522
- },
523
- timeout: { type: "string" }
524
- },
525
- allowPositionals: true
526
- });
527
- } catch (e) {
528
- process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\n`);
529
- process.exitCode = 1;
530
- return;
531
- }
532
- if (parsed.values.help || argv.length === 0) {
533
- process.stdout.write(USAGE);
534
- return;
535
- }
536
- const files = await discoverTestFiles(parsed.positionals, process.cwd());
537
- if (files.length === 0) {
538
- process.stderr.write(`devtools-test: no test files matched ${parsed.positionals.join(", ")}\n`);
539
- process.exitCode = 1;
540
- return;
541
- }
542
- process.stderr.write(`devtools-test: matched ${files.length} test file(s), but direct CLI relay attach is not yet wired.\n Use the devtools-mcp server (\`devtools-mcp\`) to start a debug session,\n then the \`run_tests\` MCP tool to run these files against the attached page.\n`);
543
- process.exitCode = 1;
544
- }
545
- if (import.meta.url === new URL(process.argv[1], "file://").href) main().catch((e) => {
546
- process.stderr.write(`devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\n`);
547
- process.exitCode = 1;
548
- });
549
- //#endregion
2
+ import { n as runWithConnection, t as main } from "../cli-BLBo0lkP.js";
550
3
  export { main, runWithConnection };
551
-
552
- //# sourceMappingURL=cli.js.map
@@ -1,4 +1,4 @@
1
- import { i as createRelayPool, n as RelayConnectionFactory, t as RELAY_POOL_NAME } from "../pool-mZlgCmNQ.js";
1
+ import { i as createRelayPool, n as RelayConnectionFactory, t as RELAY_POOL_NAME } from "../pool-htlVnEFl.js";
2
2
 
3
3
  //#region src/test-runner/config.d.ts
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { i as createRelayPool, n as RelayConnectionFactory, r as RelayPoolOptions, t as RELAY_POOL_NAME } from "../pool-mZlgCmNQ.js";
1
+ import { i as createRelayPool, n as RelayConnectionFactory, r as RelayPoolOptions, t as RELAY_POOL_NAME } from "../pool-htlVnEFl.js";
2
2
  export { RELAY_POOL_NAME, RelayConnectionFactory, RelayPoolOptions, createRelayPool };
@@ -1,2 +1,2 @@
1
- import { a as runTestFilesOverRelay, i as flattenResults, n as RelayRunOptions, r as RelayRunReport, t as FileResult } from "../relay-worker-Dppp2yZj.js";
1
+ import { a as runTestFilesOverRelay, i as flattenResults, n as RelayRunOptions, r as RelayRunReport, t as FileResult } from "../relay-worker-DWW4vZxU.js";
2
2
  export { FileResult, RelayRunOptions, RelayRunReport, flattenResults, runTestFilesOverRelay };
@@ -1,5 +1,5 @@
1
1
  import { t as CdpConnection } from "../cdp-connection-C0AP0tH2.js";
2
- import { t as RunReport } from "../runtime-C7uxh3Mf.js";
2
+ import { t as RunReport } from "../runtime-BU-iMHz6.js";
3
3
 
4
4
  //#region src/test-runner/rpc.d.ts
5
5
  /**
@@ -1,2 +1,2 @@
1
- import { n as TestResult, r as runTestModule, t as RunReport } from "../runtime-C7uxh3Mf.js";
2
- export { RunReport, TestResult, runTestModule };
1
+ import { i as runtimeGlobals, n as TestResult, r as runTestModule, t as RunReport } from "../runtime-BU-iMHz6.js";
2
+ export { RunReport, TestResult, runTestModule, runtimeGlobals };