@ait-co/devtools 0.1.108 → 0.1.109

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 (33) hide show
  1. package/dist/bundle-KFs4t-wc.d.ts +96 -0
  2. package/dist/bundle-KFs4t-wc.d.ts.map +1 -0
  3. package/dist/mcp/cli.js +186 -40
  4. package/dist/mcp/cli.js.map +1 -1
  5. package/dist/mcp/server.js +3 -3
  6. package/dist/mcp/server.js.map +1 -1
  7. package/dist/panel/index.js +1 -1
  8. package/dist/{pool-Dkp7I9Bf.d.ts → pool-CuVMzWGB.d.ts} +5 -5
  9. package/dist/{pool-Dkp7I9Bf.d.ts.map → pool-CuVMzWGB.d.ts.map} +1 -1
  10. package/dist/{relay-worker-BzFQ3fv9.d.ts → relay-worker-xxanNQGs.d.ts} +3 -3
  11. package/dist/relay-worker-xxanNQGs.d.ts.map +1 -0
  12. package/dist/{runtime-ORdrpizY.d.ts → runtime-Wi5d6Ywz.d.ts} +3 -3
  13. package/dist/{runtime-ORdrpizY.d.ts.map → runtime-Wi5d6Ywz.d.ts.map} +1 -1
  14. package/dist/test-runner/bundle.d.ts +1 -1
  15. package/dist/test-runner/bundle.js +148 -11
  16. package/dist/test-runner/bundle.js.map +1 -1
  17. package/dist/test-runner/cli.d.ts +59 -14
  18. package/dist/test-runner/cli.d.ts.map +1 -1
  19. package/dist/test-runner/cli.js +171 -32
  20. package/dist/test-runner/cli.js.map +1 -1
  21. package/dist/test-runner/config.d.ts +1 -1
  22. package/dist/test-runner/pool.d.ts +1 -1
  23. package/dist/test-runner/relay-worker.d.ts +1 -1
  24. package/dist/test-runner/relay-worker.js.map +1 -1
  25. package/dist/test-runner/rpc.d.ts +1 -1
  26. package/dist/test-runner/rpc.d.ts.map +1 -1
  27. package/dist/test-runner/rpc.js +1 -1
  28. package/dist/test-runner/rpc.js.map +1 -1
  29. package/dist/test-runner/task-graph.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/dist/bundle-BJm5jk56.d.ts +0 -49
  32. package/dist/bundle-BJm5jk56.d.ts.map +0 -1
  33. package/dist/relay-worker-BzFQ3fv9.d.ts.map +0 -1
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from "node:util";
3
+ import * as fs from "node:fs/promises";
3
4
  import { glob } from "node:fs/promises";
4
5
  import * as path from "node:path";
5
6
  import { isAbsolute, resolve } from "node:path";
7
+ import { accessSync } from "node:fs";
8
+ import { fileURLToPath } from "node:url";
6
9
  //#region src/test-runner/discover.ts
7
10
  /**
8
11
  * Test-file discovery shared by the `devtools-test` CLI and the `run_tests`
@@ -39,11 +42,53 @@ async function discoverTestFiles(patterns, cwd) {
39
42
  * esbuild-based bundler for user test files.
40
43
  *
41
44
  * Bundles a single test file into a self-contained IIFE string that can be
42
- * injected into a WebView via `Runtime.evaluate`. The user's SDK imports
43
- * (`@apps-in-toss/web-framework` and sub-paths) are intercepted via an
44
- * esbuild plugin that redirects them to `window.__sdk`, which the in-app
45
- * debug gate (`src/in-app/auto.ts`) installs as a namespace mirror of the
46
- * SDK exports (works for both 2.x and 3.x SDK).
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.
47
92
  *
48
93
  * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
49
94
  */
@@ -93,10 +138,90 @@ module.exports = __proxy;
93
138
  };
94
139
  }
95
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
+ * - Lines matching import declarations or re-export statements are kept at
149
+ * module top-level (the only valid ESM position for static `import`).
150
+ * - All other lines (describe/it/test calls, local declarations, etc.) are
151
+ * moved into the body of the exported async factory function.
152
+ *
153
+ * This preserves SDK import resolution (the sdk-redirect plugin processes
154
+ * top-level imports normally) while deferring test registration to the factory.
155
+ */
156
+ function userFactoryPlugin(absPath) {
157
+ const NAMESPACE = "user-test-factory";
158
+ return {
159
+ name: "user-test-factory",
160
+ setup(build) {
161
+ build.onResolve({ filter: /^user-test-factory$/ }, () => ({
162
+ path: absPath,
163
+ namespace: NAMESPACE
164
+ }));
165
+ build.onLoad({
166
+ filter: /.*/,
167
+ namespace: NAMESPACE
168
+ }, async (args) => {
169
+ const lines = (await fs.readFile(args.path, "utf8")).split("\n");
170
+ const topLevelLines = [];
171
+ const bodyLines = [];
172
+ const EXPORT_DECLARATION_RE = /^(export\s+)(default\s+|async\s+function\s+|function\s+|class\s+|const\s+|let\s+|var\s+)/;
173
+ for (const line of lines) {
174
+ const trimmed = line.trimStart();
175
+ const indent = line.slice(0, line.length - trimmed.length);
176
+ if (trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import'") || trimmed.startsWith("import\"")) topLevelLines.push(line);
177
+ else if (trimmed.startsWith("export ")) if (trimmed.match(EXPORT_DECLARATION_RE)) bodyLines.push(indent + trimmed.slice(7));
178
+ else topLevelLines.push(line);
179
+ else bodyLines.push(line);
180
+ }
181
+ return {
182
+ contents: [
183
+ ...topLevelLines,
184
+ "",
185
+ "// biome-ignore lint: generated factory wrapper",
186
+ "export default async function __userFactory(): Promise<void> {",
187
+ ...bodyLines.map((l) => ` ${l}`),
188
+ "}"
189
+ ].join("\n"),
190
+ loader: "ts",
191
+ resolveDir: path.dirname(absPath)
192
+ };
193
+ });
194
+ }
195
+ };
196
+ }
197
+ /**
198
+ * Returns the absolute path to the co-located runtime module.
199
+ *
200
+ * In the source tree (running via tsx / ts-node) the file is `runtime.ts`.
201
+ * After `tsdown` compiles to `dist/test-runner/`, it becomes `runtime.js`.
202
+ * We try both extensions to support both environments.
203
+ */
204
+ function getRuntimePath() {
205
+ const dir = path.dirname(fileURLToPath(import.meta.url));
206
+ for (const ext of [".ts", ".js"]) {
207
+ const candidate = path.join(dir, `runtime${ext}`);
208
+ try {
209
+ accessSync(candidate);
210
+ return candidate;
211
+ } catch {}
212
+ }
213
+ return path.join(dir, "runtime.js");
214
+ }
215
+ /**
96
216
  * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
97
217
  *
98
- * The IIFE installs `window.__testBundle` (or the custom `globalName`) with
99
- * `runTestModule` as the callable entry point.
218
+ * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
219
+ * - `runTestModule` the runtime entry (from `runtime.ts`).
220
+ * - `__userFactory` — an async function wrapping the user's test registration
221
+ * code so it runs AFTER `runTestModule` installs the globals.
222
+ *
223
+ * Callers (rpc.ts) invoke:
224
+ * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
100
225
  *
101
226
  * @param absPath - Absolute path to the user test file.
102
227
  * @param opts - Optional bundling overrides.
@@ -104,17 +229,29 @@ module.exports = __proxy;
104
229
  async function bundleTestFile(absPath, opts) {
105
230
  const globalName = opts?.globalName ?? "__testBundle";
106
231
  const extraExternals = opts?.extraExternals ?? [];
107
- const result = await (await import("esbuild")).build({
108
- entryPoints: [absPath],
232
+ const esbuild = await import("esbuild");
233
+ const runtimePath = getRuntimePath();
234
+ const wrapperContent = [
235
+ `import { runTestModule } from ${JSON.stringify(runtimePath)};`,
236
+ `import __userFactory from "user-test-factory";`,
237
+ `export { runTestModule, __userFactory };`
238
+ ].join("\n");
239
+ const result = await esbuild.build({
240
+ stdin: {
241
+ contents: wrapperContent,
242
+ loader: "ts",
243
+ resolveDir: path.dirname(absPath)
244
+ },
109
245
  bundle: true,
110
246
  format: "iife",
111
247
  globalName,
112
248
  platform: "browser",
113
249
  target: "es2022",
114
250
  write: false,
115
- plugins: [sdkRedirectPlugin()],
251
+ plugins: [userFactoryPlugin(absPath), sdkRedirectPlugin()],
116
252
  external: extraExternals,
117
- treeShaking: true
253
+ treeShaking: true,
254
+ footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
118
255
  });
119
256
  const warnings = result.warnings.map((w) => `${path.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
120
257
  const outputFile = result.outputFiles?.[0];
@@ -141,7 +278,7 @@ const DEFAULT_TIMEOUT_MS = 3e4;
141
278
  * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
142
279
  */
143
280
  function buildRunTestsExpression(bundleCode) {
144
- 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') { return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule is not a function'}); } try { const report = await globalThis.__testBundle.runTestModule(); return JSON.stringify({ok:true,value:report}); } catch(e) { return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)}); }})()`;
281
+ 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)}); }})()`;
145
282
  }
146
283
  /**
147
284
  * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
@@ -274,14 +411,15 @@ async function runTestFilesOverRelay(connection, files, opts) {
274
411
  //#endregion
275
412
  //#region src/test-runner/cli.ts
276
413
  /**
277
- * `devtools-test` CLI — MVP skeleton.
414
+ * `devtools-test` CLI.
278
415
  *
279
- * Parses argv, prints usage, and delegates to `runTestFilesOverRelay` when
280
- * a live CDP connection is provided. The relay connection wiring
281
- * (attach run detach) is tracked in issue #645 / #646.
282
- *
283
- * MVP contract: `--help` works, `runWithConnection` is a testable pure
284
- * function, and the binary entry exists in package.json.
416
+ * Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)
417
+ * and exposes `runWithConnection` the pure run core that bundles, injects, and
418
+ * collects each file over a CDP connection. Today the run path that has a live
419
+ * connection is the `run_tests` MCP tool (it runs these files against the
420
+ * daemon's attached page); the CLI's own standalone relay attach (resolve CDP
421
+ * URL attach run close) is not wired yet, so `main()` resolves the matched
422
+ * files and points the operator at the MCP tool.
285
423
  *
286
424
  * NOTE: no shebang in this source file — the tsdown entry's `banner` option
287
425
  * injects `#!/usr/bin/env node` into the compiled output (same pattern as
@@ -302,12 +440,10 @@ DESCRIPTION
302
440
  window.__sdk), injects the bundle into the attached WebView via
303
441
  Runtime.evaluate, and returns a RunReport.
304
442
 
305
- A live CDP relay connection must be active before running tests.
306
- Use \`/ait debug\` (devtools-mcp) to attach and then call this CLI from
307
- the same process context.
308
-
309
- Full Vitest pool integration and the \`run_tests\` MCP tool are tracked in
310
- issues #645 and #646 respectively. This MVP provides the transport layer.
443
+ A live CDP relay connection must be active before running tests. Use the
444
+ \`run_tests\` MCP tool (via \`devtools-mcp\` / \`/ait debug\`) to run these files
445
+ against an attached page — the CLI's own standalone relay attach is not wired
446
+ yet (it currently resolves the matched files and defers to that tool).
311
447
 
312
448
  EXAMPLE
313
449
  devtools-test 'src/**/*.phone.test.ts' --timeout 60000
@@ -315,11 +451,12 @@ EXAMPLE
315
451
  `.trimStart();
316
452
  /**
317
453
  * Runs `files` over `connection` and returns the aggregate report.
318
- * This pure function is the testable core of the CLI; it is separate from
319
- * `main()` so tests can call it without spawning a subprocess.
454
+ * This pure function is the testable core of the CLI (and is what the
455
+ * `run_tests` MCP tool calls against the daemon's attached connection); it is
456
+ * separate from `main()` so tests can call it without spawning a subprocess.
320
457
  *
321
- * TODO (#645): add real relay attach/detach lifecycle here (connect via
322
- * Chii relay URL, call enableDomains, run, then close).
458
+ * A standalone CLI relay attach/detach lifecycle (connect via Chii relay URL,
459
+ * `enableDomains`, run, then close) is not wired into `main()` yet.
323
460
  */
324
461
  async function runWithConnection(connection, files, opts) {
325
462
  const report = await runTestFilesOverRelay(connection, files, opts);
@@ -332,8 +469,10 @@ async function runWithConnection(connection, files, opts) {
332
469
  /**
333
470
  * CLI entry point.
334
471
  *
335
- * MVP: prints usage and a "relay attach required" notice. Real relay wiring
336
- * (resolve CDP URL, attach, run, close) is tracked in issues #645 / #646.
472
+ * Resolves the matched test files and prints a "relay attach required" notice:
473
+ * the CLI's own standalone relay attach (resolve CDP URL, attach, run, close) is
474
+ * not wired yet, so today these files run via the `run_tests` MCP tool against
475
+ * the daemon's attached page.
337
476
  */
338
477
  async function main(argv = process.argv.slice(2)) {
339
478
  let parsed;
@@ -364,7 +503,7 @@ async function main(argv = process.argv.slice(2)) {
364
503
  process.exitCode = 1;
365
504
  return;
366
505
  }
367
- 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 Direct CLI relay wiring is tracked in issue #645.\n`);
506
+ 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`);
368
507
  process.exitCode = 1;
369
508
  }
370
509
  if (import.meta.url === new URL(process.argv[1], "file://").href) main().catch((e) => {
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../../src/test-runner/discover.ts","../../src/test-runner/bundle.ts","../../src/test-runner/rpc.ts","../../src/test-runner/relay-worker.ts","../../src/test-runner/cli.ts"],"sourcesContent":["/**\n * Test-file discovery shared by the `devtools-test` CLI and the `run_tests`\n * MCP tool, so both expand glob patterns with identical semantics.\n *\n * Uses Node's built-in `fs/promises` `glob` (Node 22+) — no extra dependency,\n * which keeps the MCP daemon install graph lean (a plain glob lib would land in\n * the `npx … devtools-mcp` path for no benefit).\n *\n * Pure Node IO only (`node:fs/promises` + `node:path`) — react-free, so it is\n * safe to import from the MCP daemon graph.\n */\n\nimport { glob } from 'node:fs/promises';\nimport { isAbsolute, resolve } from 'node:path';\n\n/**\n * Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of\n * ABSOLUTE test file paths, resolved relative to `cwd`.\n *\n * A plain (non-glob) path passes through when it matches a real file; a glob\n * expands against `cwd`. Absolute matches are kept as-is; relative matches are\n * resolved against `cwd`. `bundleTestFile` requires an absolute path, so the\n * absolute output feeds it directly.\n *\n * @param patterns Glob patterns or file paths (e.g. `['src/**\\/*.phone.test.ts']`).\n * @param cwd Base directory for relative patterns/results.\n * @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.\n */\nexport async function discoverTestFiles(patterns: string[], cwd: string): Promise<string[]> {\n const out = new Set<string>();\n for await (const match of glob(patterns, { cwd })) {\n out.add(isAbsolute(match) ? match : resolve(cwd, match));\n }\n return [...out].sort();\n}\n","/**\n * esbuild-based bundler for user test files.\n *\n * Bundles a single test file into a self-contained IIFE string that can be\n * injected into a WebView via `Runtime.evaluate`. The user's SDK imports\n * (`@apps-in-toss/web-framework` and sub-paths) are intercepted via an\n * esbuild plugin that redirects them to `window.__sdk`, which the in-app\n * debug gate (`src/in-app/auto.ts`) installs as a namespace mirror of the\n * SDK exports (works for both 2.x and 3.x SDK).\n *\n * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.\n */\n\nimport * as path from 'node:path';\n// esbuild is imported for TYPES only at module scope; the runtime module is\n// loaded lazily inside `bundleTestFile` via dynamic import. esbuild runs a\n// startup invariant check (`TextEncoder().encode('') instanceof Uint8Array`)\n// that fails in a jsdom realm — a static import would break every MCP/test\n// module that merely *imports* this file's transitive graph (e.g. debug-server →\n// run_tests). Lazy load keeps esbuild off the import graph until a bundle is\n// actually built, and mirrors the cloudflared/chii dynamic-import precedent.\nimport type * as esbuild from 'esbuild';\n\n/** Options accepted by `bundleTestFile`. */\nexport interface BundleOptions {\n /**\n * Additional esbuild `external` patterns. The SDK package\n * (`@apps-in-toss/web-framework` and `@apps-in-toss/web-framework/*`) is\n * always handled by the SDK redirect plugin — callers may add more patterns\n * to be left as globals.\n */\n extraExternals?: string[];\n /**\n * Global name for the IIFE output object. Defaults to `__testBundle`.\n * The runtime entry uses this to call `__testBundle.runTestModule()`.\n */\n globalName?: string;\n}\n\n/**\n * The result of bundling a test file.\n * `code` is a self-contained IIFE string ready for `Runtime.evaluate`.\n */\nexport interface BundleResult {\n code: string;\n warnings: string[];\n}\n\n/** The SDK package name that mini-app test code imports from. */\nconst SDK_PACKAGE = '@apps-in-toss/web-framework';\n\n/**\n * Matches the bare SDK package and any sub-path import\n * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).\n * Built from {@link SDK_PACKAGE} so the package name has a single source.\n */\nconst SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`);\n\n/**\n * esbuild plugin that intercepts SDK imports and redirects them to the\n * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.\n *\n * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),\n * esbuild resolves it to a virtual module that re-exports all named exports\n * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not\n * be available in the test environment) while still making named imports work.\n *\n * If `window.__sdk` is absent (non-dog-food build), every access throws a\n * descriptive error rather than returning `undefined` silently.\n */\nfunction sdkRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'sdk-redirect',\n setup(build) {\n // Match the bare package and any sub-path imports\n build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({\n path: args.path,\n namespace: 'sdk-redirect',\n }));\n\n build.onLoad({ filter: /.*/, namespace: 'sdk-redirect' }, () => ({\n // Generate a virtual CommonJS-style module so that esbuild does NOT perform\n // strict named-export matching. When `format:'iife'` bundles a CJS module,\n // it wraps it with its own __toCommonJS helper and satisfies named imports\n // via property access on the module.exports object — which is our Proxy.\n // This means `import { getPlatformOS } from '...'` becomes\n // `__proxy.getPlatformOS` at runtime, which correctly reads from window.__sdk.\n contents: `\nvar __proxy = (typeof window !== 'undefined' && window.__sdk)\n ? window.__sdk\n : new Proxy({}, {\n get: function(_t, p) {\n throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));\n }\n });\nmodule.exports = __proxy;\n`,\n loader: 'js',\n }));\n },\n };\n}\n\n/**\n * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.\n *\n * The IIFE installs `window.__testBundle` (or the custom `globalName`) with\n * `runTestModule` as the callable entry point.\n *\n * @param absPath - Absolute path to the user test file.\n * @param opts - Optional bundling overrides.\n */\nexport async function bundleTestFile(absPath: string, opts?: BundleOptions): Promise<BundleResult> {\n const globalName = opts?.globalName ?? '__testBundle';\n const extraExternals = opts?.extraExternals ?? [];\n\n // Lazy load esbuild at call time (see the module-scope import note).\n const esbuild = await import('esbuild');\n\n const result = await esbuild.build({\n entryPoints: [absPath],\n bundle: true,\n format: 'iife',\n globalName,\n platform: 'browser',\n target: 'es2022',\n write: false,\n plugins: [sdkRedirectPlugin()],\n // Extra externals are left as global references (caller's responsibility\n // to ensure they exist in the WebView context).\n external: extraExternals,\n // Keep bundle self-contained; no dynamic require/import at runtime.\n treeShaking: true,\n });\n\n const warnings = result.warnings.map(\n (w) =>\n `${path.relative(process.cwd(), w.location?.file ?? '')}:${w.location?.line ?? '?'}: ${w.text}`,\n );\n\n const outputFile = result.outputFiles?.[0];\n if (!outputFile) {\n throw new Error('bundleTestFile: esbuild produced no output — check entryPoints');\n }\n\n return { code: outputFile.text, warnings };\n}\n","/**\n * Node-side RPC helpers for injecting and collecting test execution over CDP.\n *\n * Uses the same IIFE + JSON.stringify envelope pattern as `buildCallSdkExpression`\n * in `src/mcp/tools.ts` to reliably shuttle structured results through\n * `Runtime.evaluate`'s `returnByValue: true` boundary.\n *\n * SECRET-HANDLING: bundle code, relay URLs, and result values are NOT logged.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { RunReport } from './runtime.js';\n\n/** Maximum milliseconds to wait for a single evaluate round-trip. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Wraps bundle code in a self-executing IIFE that:\n * 1. Evaluates the bundle (registering describe/it/test).\n * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.\n * 3. Returns a JSON-serialised `RunReport` string.\n *\n * The double-serialisation (RunReport → JSON string → returnByValue string)\n * is intentional: CDP `returnByValue` reliably transports strings; deeply\n * nested objects can lose fidelity across the Chii relay.\n *\n * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.\n */\nexport function buildRunTestsExpression(bundleCode: string): string {\n // We trust bundleCode is already a self-contained IIFE that installs\n // `window.__testBundle` (or `globalThis.__testBundle`).\n // We then call `__testBundle.runTestModule()` and return a JSON string.\n return (\n `(async () => {` +\n // Step 1: evaluate the bundle to register tests\n ` try { ${bundleCode} } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)});` +\n ` }` +\n // Step 2: check that the expected export is present\n ` if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule is not a function'});` +\n ` }` +\n // Step 3: run tests\n ` try {` +\n ` const report = await globalThis.__testBundle.runTestModule();` +\n ` return JSON.stringify({ok:true,value:report});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Result of `injectAndRunBundle`.\n */\nexport type RpcRunResult = { ok: true; report: RunReport } | { ok: false; error: string };\n\n/**\n * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`\n * evaluate call into a typed `RpcRunResult`.\n *\n * Throws only on parse failure — an `ok:false` envelope is a normal result.\n *\n * SECRET-HANDLING: `rawValue` is not included in error messages.\n */\nexport function parseRunTestsResult(rawValue: unknown): RpcRunResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `rpc.parseRunTestsResult: unexpected return type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue — could contain secrets.\n throw new Error('rpc.parseRunTestsResult: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('rpc.parseRunTestsResult: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, report: obj.value as RunReport };\n }\n if (obj.ok === false) {\n return {\n ok: false,\n error: typeof obj.error === 'string' ? obj.error : String(obj.error),\n };\n }\n throw new Error('rpc.parseRunTestsResult: result missing \"ok\" field');\n}\n\n/**\n * Injects `bundleCode` into the attached page and awaits test execution.\n *\n * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the\n * async IIFE to settle. The 30-second CDP command timeout covers even\n * long-running test suites; split into smaller files if you hit it.\n *\n * @param connection - Active CDP connection (relay or local).\n * @param bundleCode - IIFE bundle string from `bundleTestFile`.\n * @param timeoutMs - Override the default 30 s timeout.\n *\n * SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.\n */\nexport async function injectAndRunBundle(\n connection: CdpConnection,\n bundleCode: string,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n): Promise<RpcRunResult> {\n const expression = buildRunTestsExpression(bundleCode);\n\n // Use AbortSignal-style timeout via Promise.race so we surface a clear\n // message rather than hanging indefinitely.\n const timeoutPromise = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs),\n );\n\n const evalPromise = connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\n });\n\n const cdpResult = await Promise.race([evalPromise, timeoutPromise]);\n\n if (cdpResult.exceptionDetails) {\n // Surface only the engine error string — not the expression or value.\n const msg =\n cdpResult.exceptionDetails.exception?.description ??\n cdpResult.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`rpc.injectAndRunBundle: ${msg}`);\n }\n\n return parseRunTestsResult(cdpResult.result.value);\n}\n","/**\n * Orchestrator: runs a list of test files sequentially over a CDP relay.\n *\n * Each file goes through: bundle → inject → run → collect.\n * This is the MVP transport layer. Full Vitest pool integration (issue #645)\n * and `run_tests` MCP tool (issue #646) are NOT implemented here.\n *\n * Single-attach constraint: only one page is active at a time. Files run\n * sequentially; parallel execution across targets is a post-MVP concern.\n *\n * The 30-second per-file timeout is inherited from `injectAndRunBundle`.\n * For suites that exceed it, split the file into smaller pieces.\n *\n * SECRET-HANDLING: file paths are surfaced in reports; relay URLs are not.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport { type BundleOptions, bundleTestFile } from './bundle.js';\nimport { injectAndRunBundle } from './rpc.js';\nimport type { RunReport, TestResult } from './runtime.js';\n\n/** Per-file result in the aggregate `RunReport`. */\nexport interface FileResult {\n /** Absolute or relative path to the test file. */\n file: string;\n /** Full run report for this file, or an error if bundling/injection failed. */\n result: RunReport | { error: string };\n}\n\n/** Aggregate report returned by `runTestFilesOverRelay`. */\nexport interface RelayRunReport {\n /** ISO timestamp of when the run started. */\n startedAt: string;\n /** Total elapsed wall-clock milliseconds. */\n duration: number;\n /** Per-file results in execution order. */\n files: FileResult[];\n /** Flattened totals across all files. */\n totals: {\n passed: number;\n failed: number;\n skipped: number;\n total: number;\n };\n}\n\n/** Options for `runTestFilesOverRelay`. */\nexport interface RelayRunOptions {\n /**\n * Options forwarded to `bundleTestFile` for each file.\n */\n bundleOptions?: BundleOptions;\n /**\n * Per-file evaluate timeout in milliseconds. Defaults to 30 000.\n * Increase for long-running suites or split the file.\n */\n timeoutMs?: number;\n}\n\n/**\n * Runs all `files` sequentially over the given CDP `connection`.\n *\n * For each file:\n * 1. Bundle with esbuild (includes SDK shim + runtime).\n * 2. Inject into the attached page via `Runtime.evaluate`.\n * 3. Await the `RunReport` JSON response.\n * 4. Accumulate results.\n *\n * Returns a `RelayRunReport` with per-file results and flattened totals.\n *\n * This function does NOT open or manage the relay connection — the caller\n * is responsible for attaching and closing it.\n *\n * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here\n * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.\n *\n * @param connection - Active CDP connection (relay or local kind).\n * @param files - Absolute paths to test files, run in order.\n * @param opts - Optional per-run overrides.\n */\nexport async function runTestFilesOverRelay(\n connection: CdpConnection,\n files: string[],\n opts?: RelayRunOptions,\n): Promise<RelayRunReport> {\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const fileResults: FileResult[] = [];\n\n for (const file of files) {\n let fileEntry: FileResult;\n try {\n const { code } = await bundleTestFile(file, opts?.bundleOptions);\n const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);\n if (rpcResult.ok) {\n fileEntry = { file, result: rpcResult.report };\n } else {\n fileEntry = { file, result: { error: rpcResult.error } };\n }\n } catch (e) {\n // Capture bundle/inject errors per-file so subsequent files still run.\n fileEntry = {\n file,\n result: {\n error: e instanceof Error ? e.message : String(e),\n },\n };\n }\n fileResults.push(fileEntry);\n }\n\n const totals = fileResults.reduce(\n (acc, { result }) => {\n if ('error' in result) {\n // Treat whole-file errors as a single failure.\n acc.failed += 1;\n acc.total += 1;\n } else {\n acc.passed += result.passed;\n acc.failed += result.failed;\n acc.skipped += result.skipped;\n acc.total += result.passed + result.failed + result.skipped;\n }\n return acc;\n },\n { passed: 0, failed: 0, skipped: 0, total: 0 },\n );\n\n return {\n startedAt,\n duration: Date.now() - wallStart,\n files: fileResults,\n totals,\n };\n}\n\n/**\n * Flattens all test results from a `RelayRunReport` into a single array.\n * Files that errored during bundle/inject produce a synthetic failed entry.\n */\nexport function flattenResults(report: RelayRunReport): Array<TestResult & { file: string }> {\n const out: Array<TestResult & { file: string }> = [];\n for (const { file, result } of report.files) {\n if ('error' in result) {\n out.push({\n file,\n name: `<bundle/inject error>`,\n status: 'fail',\n duration: 0,\n error: result.error,\n });\n } else {\n for (const t of result.tests) {\n out.push({ ...t, file });\n }\n }\n }\n return out;\n}\n","/**\n * `devtools-test` CLI — MVP skeleton.\n *\n * Parses argv, prints usage, and delegates to `runTestFilesOverRelay` when\n * a live CDP connection is provided. The relay connection wiring\n * (attach → run → detach) is tracked in issue #645 / #646.\n *\n * MVP contract: `--help` works, `runWithConnection` is a testable pure\n * function, and the binary entry exists in package.json.\n *\n * NOTE: no shebang in this source file — the tsdown entry's `banner` option\n * injects `#!/usr/bin/env node` into the compiled output (same pattern as\n * `src/mcp/cli.ts`).\n */\n\nimport { parseArgs } from 'node:util';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport { discoverTestFiles } from './discover.js';\nimport type { RelayRunOptions, RelayRunReport } from './relay-worker.js';\nimport { runTestFilesOverRelay } from './relay-worker.js';\n\n/* -------------------------------------------------------------------------- */\n/* CLI help */\n/* -------------------------------------------------------------------------- */\n\nconst USAGE = `\ndevtools-test — run mini-app tests on a real device WebView over the CDP relay\n\nUSAGE\n devtools-test <glob> [<glob> ...] [options]\n\nOPTIONS\n --timeout <ms> Per-file evaluate timeout in ms (default: 30000)\n --help, -h Show this help message\n\nDESCRIPTION\n Bundles each matched test file with esbuild (SDK imports redirected to\n window.__sdk), injects the bundle into the attached WebView via\n Runtime.evaluate, and returns a RunReport.\n\n A live CDP relay connection must be active before running tests.\n Use \\`/ait debug\\` (devtools-mcp) to attach and then call this CLI from\n the same process context.\n\n Full Vitest pool integration and the \\`run_tests\\` MCP tool are tracked in\n issues #645 and #646 respectively. This MVP provides the transport layer.\n\nEXAMPLE\n devtools-test 'src/**/*.phone.test.ts' --timeout 60000\n\n`.trimStart();\n\n/* -------------------------------------------------------------------------- */\n/* Pure run function (testable without a real relay) */\n/* -------------------------------------------------------------------------- */\n\n/** Options for `runWithConnection`. */\nexport interface RunWithConnectionOptions extends RelayRunOptions {\n /** If true, print a summary to stdout. Defaults to false in tests. */\n printSummary?: boolean;\n}\n\n/**\n * Runs `files` over `connection` and returns the aggregate report.\n * This pure function is the testable core of the CLI; it is separate from\n * `main()` so tests can call it without spawning a subprocess.\n *\n * TODO (#645): add real relay attach/detach lifecycle here (connect via\n * Chii relay URL, call enableDomains, run, then close).\n */\nexport async function runWithConnection(\n connection: CdpConnection,\n files: string[],\n opts?: RunWithConnectionOptions,\n): Promise<RelayRunReport> {\n const report = await runTestFilesOverRelay(connection, files, opts);\n\n if (opts?.printSummary) {\n const { totals } = report;\n process.stdout.write(\n `\\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\\n`,\n );\n }\n\n return report;\n}\n\n/* -------------------------------------------------------------------------- */\n/* main() — CLI entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * CLI entry point.\n *\n * MVP: prints usage and a \"relay attach required\" notice. Real relay wiring\n * (resolve CDP URL, attach, run, close) is tracked in issues #645 / #646.\n */\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<void> {\n let parsed: ReturnType<typeof parseArgs>;\n try {\n parsed = parseArgs({\n args: argv,\n options: {\n help: { type: 'boolean', short: 'h' },\n timeout: { type: 'string' },\n },\n allowPositionals: true,\n });\n } catch (e) {\n process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\\n`);\n process.exitCode = 1;\n return;\n }\n\n if (parsed.values.help || argv.length === 0) {\n process.stdout.write(USAGE);\n return;\n }\n\n // Discovery is shared with the `run_tests` MCP tool (#646) via\n // `discoverTestFiles`, so both expand patterns identically. We resolve the\n // matched files here to give the operator concrete feedback before the\n // (still-pending) relay attach wiring.\n const files = await discoverTestFiles(parsed.positionals, process.cwd());\n if (files.length === 0) {\n process.stderr.write(`devtools-test: no test files matched ${parsed.positionals.join(', ')}\\n`);\n process.exitCode = 1;\n return;\n }\n\n // Relay attach lifecycle (resolve CDP URL, attach, close) is tracked in #645;\n // until then the CLI cannot run on its own. The `run_tests` MCP tool (#646)\n // already runs these files against the daemon's attached connection.\n process.stderr.write(\n `devtools-test: matched ${files.length} test file(s), but direct CLI relay attach is not yet wired.\\n` +\n ` Use the devtools-mcp server (\\`devtools-mcp\\`) to start a debug session,\\n` +\n ` then the \\`run_tests\\` MCP tool to run these files against the attached page.\\n` +\n ` Direct CLI relay wiring is tracked in issue #645.\\n`,\n );\n process.exitCode = 1;\n}\n\n// Run main() when executed as a binary (not imported as a module).\n// Node ESM: `import.meta.url === pathToFileURL(process.argv[1]).href` is the\n// canonical \"am I the main module?\" check.\nif (import.meta.url === new URL(process.argv[1], 'file://').href) {\n main().catch((e: unknown) => {\n process.stderr.write(\n `devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\\n`,\n );\n process.exitCode = 1;\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../../src/test-runner/discover.ts","../../src/test-runner/bundle.ts","../../src/test-runner/rpc.ts","../../src/test-runner/relay-worker.ts","../../src/test-runner/cli.ts"],"sourcesContent":["/**\n * Test-file discovery shared by the `devtools-test` CLI and the `run_tests`\n * MCP tool, so both expand glob patterns with identical semantics.\n *\n * Uses Node's built-in `fs/promises` `glob` (Node 22+) — no extra dependency,\n * which keeps the MCP daemon install graph lean (a plain glob lib would land in\n * the `npx … devtools-mcp` path for no benefit).\n *\n * Pure Node IO only (`node:fs/promises` + `node:path`) — react-free, so it is\n * safe to import from the MCP daemon graph.\n */\n\nimport { glob } from 'node:fs/promises';\nimport { isAbsolute, resolve } from 'node:path';\n\n/**\n * Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of\n * ABSOLUTE test file paths, resolved relative to `cwd`.\n *\n * A plain (non-glob) path passes through when it matches a real file; a glob\n * expands against `cwd`. Absolute matches are kept as-is; relative matches are\n * resolved against `cwd`. `bundleTestFile` requires an absolute path, so the\n * absolute output feeds it directly.\n *\n * @param patterns Glob patterns or file paths (e.g. `['src/**\\/*.phone.test.ts']`).\n * @param cwd Base directory for relative patterns/results.\n * @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.\n */\nexport async function discoverTestFiles(patterns: string[], cwd: string): Promise<string[]> {\n const out = new Set<string>();\n for await (const match of glob(patterns, { cwd })) {\n out.add(isAbsolute(match) ? match : resolve(cwd, match));\n }\n return [...out].sort();\n}\n","/**\n * esbuild-based bundler for user test files.\n *\n * Bundles a single test file into a self-contained IIFE string that can be\n * injected into a WebView via `Runtime.evaluate`. The bundle includes the\n * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and\n * the `runTestModule(factory)` entry point.\n *\n * ## How the wiring works\n *\n * The bundle exposes two exports on `globalThis.__testBundle`:\n * - `runTestModule` — the runtime's entry function.\n * - `__userFactory` — an async function whose body is the user's top-level\n * test registration code (describe/it/test calls).\n *\n * The Node-side RPC (`rpc.ts`) calls:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * `runTestModule` then installs `describe/it/test/expect` as globals, invokes\n * the factory (which registers all tests), runs them, and returns a `RunReport`.\n *\n * ## Why a factory wrapper is needed\n *\n * Naively adding the runtime to `entryPoints` and bundling the user file would\n * fail for two reasons:\n * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE\n * scope. The user's top-level `describe(...)` calls expect them as globals —\n * they are not globals until `runTestModule` installs them.\n * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation\n * time, before the RPC layer calls `runTestModule` to reset state and start\n * the test clock.\n *\n * The factory approach solves both: the user's registration code is deferred\n * into a function that `runTestModule` calls AFTER installing the globals.\n *\n * ## Factory extraction algorithm\n *\n * The `userFactoryPlugin` reads the user file and splits lines into:\n * - **top-level**: `import …` and re-export lines — kept at module scope\n * (the only valid position for static `import` in ESM).\n * - **body**: all other statements — moved into the body of the exported\n * `__userFactory` async function.\n *\n * esbuild processes the re-generated module, following each static import\n * through the normal dependency graph (including the SDK-redirect plugin).\n *\n * ## SDK redirect\n *\n * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via\n * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that\n * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.\n *\n * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.\n */\n\nimport { accessSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n// esbuild is imported for TYPES only at module scope; the runtime module is\n// loaded lazily inside `bundleTestFile` via dynamic import. esbuild runs a\n// startup invariant check (`TextEncoder().encode('') instanceof Uint8Array`)\n// that fails in a jsdom realm — a static import would break every MCP/test\n// module that merely *imports* this file's transitive graph (e.g. debug-server →\n// run_tests). Lazy load keeps esbuild off the import graph until a bundle is\n// actually built, and mirrors the cloudflared/chii dynamic-import precedent.\nimport type * as esbuild from 'esbuild';\n\n/** Options accepted by `bundleTestFile`. */\nexport interface BundleOptions {\n /**\n * Additional esbuild `external` patterns. The SDK package\n * (`@apps-in-toss/web-framework` and `@apps-in-toss/web-framework/*`) is\n * always handled by the SDK redirect plugin — callers may add more patterns\n * to be left as globals.\n */\n extraExternals?: string[];\n /**\n * Global name for the IIFE output object. Defaults to `__testBundle`.\n * The runtime entry uses this to call `__testBundle.runTestModule(__userFactory)`.\n */\n globalName?: string;\n}\n\n/**\n * The result of bundling a test file.\n * `code` is a self-contained IIFE string ready for `Runtime.evaluate`.\n */\nexport interface BundleResult {\n code: string;\n warnings: string[];\n}\n\n/** The SDK package name that mini-app test code imports from. */\nconst SDK_PACKAGE = '@apps-in-toss/web-framework';\n\n/**\n * Matches the bare SDK package and any sub-path import\n * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).\n * Built from {@link SDK_PACKAGE} so the package name has a single source.\n */\nconst SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`);\n\n/**\n * esbuild plugin that intercepts SDK imports and redirects them to the\n * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.\n *\n * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),\n * esbuild resolves it to a virtual module that re-exports all named exports\n * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not\n * be available in the test environment) while still making named imports work.\n *\n * If `window.__sdk` is absent (non-dog-food build), every access throws a\n * descriptive error rather than returning `undefined` silently.\n */\nfunction sdkRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'sdk-redirect',\n setup(build) {\n // Match the bare package and any sub-path imports\n build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({\n path: args.path,\n namespace: 'sdk-redirect',\n }));\n\n build.onLoad({ filter: /.*/, namespace: 'sdk-redirect' }, () => ({\n // Generate a virtual CommonJS-style module so that esbuild does NOT perform\n // strict named-export matching. When `format:'iife'` bundles a CJS module,\n // it wraps it with its own __toCommonJS helper and satisfies named imports\n // via property access on the module.exports object — which is our Proxy.\n // This means `import { getPlatformOS } from '...'` becomes\n // `__proxy.getPlatformOS` at runtime, which correctly reads from window.__sdk.\n contents: `\nvar __proxy = (typeof window !== 'undefined' && window.__sdk)\n ? window.__sdk\n : new Proxy({}, {\n get: function(_t, p) {\n throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));\n }\n });\nmodule.exports = __proxy;\n`,\n loader: 'js',\n }));\n },\n };\n}\n\n/**\n * esbuild plugin that transforms the user test file into a module that exports\n * an async `__userFactory` function. The factory defers the user's top-level\n * test registration code (describe/it/test calls) so it only runs when\n * `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has\n * installed describe/it/test/expect as globals.\n *\n * Algorithm:\n * - Lines matching import declarations or re-export statements are kept at\n * module top-level (the only valid ESM position for static `import`).\n * - All other lines (describe/it/test calls, local declarations, etc.) are\n * moved into the body of the exported async factory function.\n *\n * This preserves SDK import resolution (the sdk-redirect plugin processes\n * top-level imports normally) while deferring test registration to the factory.\n */\nfunction userFactoryPlugin(absPath: string): esbuild.Plugin {\n const NAMESPACE = 'user-test-factory';\n return {\n name: 'user-test-factory',\n setup(build) {\n // Resolve the virtual \"user-test-factory\" specifier to our namespace.\n build.onResolve({ filter: /^user-test-factory$/ }, () => ({\n path: absPath,\n namespace: NAMESPACE,\n }));\n\n // Load the user file, split imports from body, wrap body in the factory.\n build.onLoad({ filter: /.*/, namespace: NAMESPACE }, async (args) => {\n const source = await fs.readFile(args.path, 'utf8');\n const lines = source.split('\\n');\n\n const topLevelLines: string[] = [];\n const bodyLines: string[] = [];\n\n // Matches `export` value declarations that cannot appear inside a\n // function body. We strip the `export` keyword so they become plain\n // declarations inside the factory.\n const EXPORT_DECLARATION_RE =\n /^(export\\s+)(default\\s+|async\\s+function\\s+|function\\s+|class\\s+|const\\s+|let\\s+|var\\s+)/;\n\n for (const line of lines) {\n const trimmed = line.trimStart();\n const indent = line.slice(0, line.length - trimmed.length);\n\n // Static import declarations must stay at module top level\n // (the ESM spec forbids `import` inside a function body).\n if (\n trimmed.startsWith('import ') ||\n trimmed.startsWith('import{') ||\n trimmed.startsWith(\"import'\") ||\n trimmed.startsWith('import\"')\n ) {\n topLevelLines.push(line);\n } else if (trimmed.startsWith('export ')) {\n // Determine whether this is a re-export (stays top-level) or a value\n // declaration (goes into the factory, export keyword stripped).\n const m = trimmed.match(EXPORT_DECLARATION_RE);\n if (m) {\n // Value declaration — strip `export ` and move into factory body.\n // e.g. `export function hello()` → `function hello()`\n // `export const x = 1` → `const x = 1`\n bodyLines.push(indent + trimmed.slice('export '.length));\n } else {\n // Re-export or `export type { … }` — stays at top level.\n topLevelLines.push(line);\n }\n } else {\n bodyLines.push(line);\n }\n }\n\n const factoryContent = [\n ...topLevelLines,\n '',\n '// biome-ignore lint: generated factory wrapper',\n 'export default async function __userFactory(): Promise<void> {',\n ...bodyLines.map((l) => ` ${l}`),\n '}',\n ].join('\\n');\n\n return {\n contents: factoryContent,\n loader: 'ts',\n resolveDir: path.dirname(absPath),\n };\n });\n },\n };\n}\n\n/**\n * Returns the absolute path to the co-located runtime module.\n *\n * In the source tree (running via tsx / ts-node) the file is `runtime.ts`.\n * After `tsdown` compiles to `dist/test-runner/`, it becomes `runtime.js`.\n * We try both extensions to support both environments.\n */\nfunction getRuntimePath(): string {\n const dir = path.dirname(fileURLToPath(import.meta.url));\n for (const ext of ['.ts', '.js']) {\n const candidate = path.join(dir, `runtime${ext}`);\n try {\n accessSync(candidate);\n return candidate;\n } catch {\n // try next extension\n }\n }\n // Let esbuild produce a \"file not found\" error with a clear path.\n return path.join(dir, 'runtime.js');\n}\n\n/**\n * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.\n *\n * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:\n * - `runTestModule` — the runtime entry (from `runtime.ts`).\n * - `__userFactory` — an async function wrapping the user's test registration\n * code so it runs AFTER `runTestModule` installs the globals.\n *\n * Callers (rpc.ts) invoke:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * @param absPath - Absolute path to the user test file.\n * @param opts - Optional bundling overrides.\n */\nexport async function bundleTestFile(absPath: string, opts?: BundleOptions): Promise<BundleResult> {\n const globalName = opts?.globalName ?? '__testBundle';\n const extraExternals = opts?.extraExternals ?? [];\n\n // Lazy load esbuild at call time (see the module-scope import note).\n const esbuild = await import('esbuild');\n const runtimePath = getRuntimePath();\n\n // Stdin wrapper: import the runtime and the user factory, re-export both.\n // esbuild follows the static imports to include runtime.ts and the user file\n // (via the userFactoryPlugin) in the single IIFE output.\n const wrapperContent = [\n `import { runTestModule } from ${JSON.stringify(runtimePath)};`,\n `import __userFactory from \"user-test-factory\";`,\n `export { runTestModule, __userFactory };`,\n ].join('\\n');\n\n const result = await esbuild.build({\n stdin: {\n contents: wrapperContent,\n loader: 'ts',\n // resolveDir is used for relative imports from the wrapper. Since the\n // wrapper only imports absolute paths (runtimePath) and the virtual\n // \"user-test-factory\" specifier (resolved by plugin), the directory\n // doesn't matter — but we still provide a sensible default.\n resolveDir: path.dirname(absPath),\n },\n bundle: true,\n format: 'iife',\n globalName,\n platform: 'browser',\n target: 'es2022',\n write: false,\n plugins: [userFactoryPlugin(absPath), sdkRedirectPlugin()],\n external: extraExternals,\n treeShaking: true,\n // Ensure the IIFE result is always reachable via globalThis regardless of\n // the evaluation context. esbuild's `globalName` emits:\n // var __testBundle = (() => { ... })();\n // When `Runtime.evaluate` runs this bundle code inside an outer wrapper\n // (rpc.ts's async IIFE), `var` creates a local variable — NOT a global\n // property — so `globalThis.__testBundle` stays `undefined`. The footer\n // explicitly assigns the local variable to `globalThis` to close that gap.\n footer: {\n js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};`,\n },\n });\n\n const warnings = result.warnings.map(\n (w) =>\n `${path.relative(process.cwd(), w.location?.file ?? '')}:${w.location?.line ?? '?'}: ${w.text}`,\n );\n\n const outputFile = result.outputFiles?.[0];\n if (!outputFile) {\n throw new Error('bundleTestFile: esbuild produced no output — check entryPoints');\n }\n\n return { code: outputFile.text, warnings };\n}\n","/**\n * Node-side RPC helpers for injecting and collecting test execution over CDP.\n *\n * Uses the same IIFE + JSON.stringify envelope pattern as `buildCallSdkExpression`\n * in `src/mcp/tools.ts` to reliably shuttle structured results through\n * `Runtime.evaluate`'s `returnByValue: true` boundary.\n *\n * SECRET-HANDLING: bundle code, relay URLs, and result values are NOT logged.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { RunReport } from './runtime.js';\n\n/** Maximum milliseconds to wait for a single evaluate round-trip. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Wraps bundle code in a self-executing IIFE that:\n * 1. Evaluates the bundle (registering describe/it/test).\n * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.\n * 3. Returns a JSON-serialised `RunReport` string.\n *\n * The double-serialisation (RunReport → JSON string → returnByValue string)\n * is intentional: CDP `returnByValue` reliably transports strings; deeply\n * nested objects can lose fidelity across the Chii relay.\n *\n * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.\n */\nexport function buildRunTestsExpression(bundleCode: string): string {\n // We trust bundleCode is already a self-contained IIFE that installs\n // `window.__testBundle` (or `globalThis.__testBundle`).\n // We then call `__testBundle.runTestModule()` and return a JSON string.\n return (\n `(async () => {` +\n // Step 1: evaluate the bundle to register tests\n ` try { ${bundleCode} } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)});` +\n ` }` +\n // Step 2: check that the expected exports are present\n ` if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'});` +\n ` }` +\n // Step 3: run tests — pass the factory so runTestModule installs globals\n // first, then invokes the factory to register describe/it/test blocks.\n ` try {` +\n ` const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory);` +\n ` return JSON.stringify({ok:true,value:report});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Result of `injectAndRunBundle`.\n */\nexport type RpcRunResult = { ok: true; report: RunReport } | { ok: false; error: string };\n\n/**\n * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`\n * evaluate call into a typed `RpcRunResult`.\n *\n * Throws only on parse failure — an `ok:false` envelope is a normal result.\n *\n * SECRET-HANDLING: `rawValue` is not included in error messages.\n */\nexport function parseRunTestsResult(rawValue: unknown): RpcRunResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `rpc.parseRunTestsResult: unexpected return type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue — could contain secrets.\n throw new Error('rpc.parseRunTestsResult: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('rpc.parseRunTestsResult: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, report: obj.value as RunReport };\n }\n if (obj.ok === false) {\n return {\n ok: false,\n error: typeof obj.error === 'string' ? obj.error : String(obj.error),\n };\n }\n throw new Error('rpc.parseRunTestsResult: result missing \"ok\" field');\n}\n\n/**\n * Injects `bundleCode` into the attached page and awaits test execution.\n *\n * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the\n * async IIFE to settle. The 30-second CDP command timeout covers even\n * long-running test suites; split into smaller files if you hit it.\n *\n * @param connection - Active CDP connection (relay or local).\n * @param bundleCode - IIFE bundle string from `bundleTestFile`.\n * @param timeoutMs - Override the default 30 s timeout.\n *\n * SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.\n */\nexport async function injectAndRunBundle(\n connection: CdpConnection,\n bundleCode: string,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n): Promise<RpcRunResult> {\n const expression = buildRunTestsExpression(bundleCode);\n\n // Use AbortSignal-style timeout via Promise.race so we surface a clear\n // message rather than hanging indefinitely.\n const timeoutPromise = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs),\n );\n\n const evalPromise = connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\n });\n\n const cdpResult = await Promise.race([evalPromise, timeoutPromise]);\n\n if (cdpResult.exceptionDetails) {\n // Surface only the engine error string — not the expression or value.\n const msg =\n cdpResult.exceptionDetails.exception?.description ??\n cdpResult.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`rpc.injectAndRunBundle: ${msg}`);\n }\n\n return parseRunTestsResult(cdpResult.result.value);\n}\n","/**\n * Orchestrator: runs a list of test files sequentially over a CDP relay.\n *\n * Each file goes through: bundle → inject → run → collect.\n * This is the transport layer: it does NOT integrate with Vitest's pool or the\n * MCP surface. The Vitest custom pool (`pool.ts`) and the `run_tests` MCP tool\n * are separate callers that build on this orchestrator.\n *\n * Single-attach constraint: only one page is active at a time. Files run\n * sequentially; parallel execution across targets is out of scope.\n *\n * The 30-second per-file timeout is inherited from `injectAndRunBundle`.\n * For suites that exceed it, split the file into smaller pieces.\n *\n * SECRET-HANDLING: file paths are surfaced in reports; relay URLs are not.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport { type BundleOptions, bundleTestFile } from './bundle.js';\nimport { injectAndRunBundle } from './rpc.js';\nimport type { RunReport, TestResult } from './runtime.js';\n\n/** Per-file result in the aggregate `RunReport`. */\nexport interface FileResult {\n /** Absolute or relative path to the test file. */\n file: string;\n /** Full run report for this file, or an error if bundling/injection failed. */\n result: RunReport | { error: string };\n}\n\n/** Aggregate report returned by `runTestFilesOverRelay`. */\nexport interface RelayRunReport {\n /** ISO timestamp of when the run started. */\n startedAt: string;\n /** Total elapsed wall-clock milliseconds. */\n duration: number;\n /** Per-file results in execution order. */\n files: FileResult[];\n /** Flattened totals across all files. */\n totals: {\n passed: number;\n failed: number;\n skipped: number;\n total: number;\n };\n}\n\n/** Options for `runTestFilesOverRelay`. */\nexport interface RelayRunOptions {\n /**\n * Options forwarded to `bundleTestFile` for each file.\n */\n bundleOptions?: BundleOptions;\n /**\n * Per-file evaluate timeout in milliseconds. Defaults to 30 000.\n * Increase for long-running suites or split the file.\n */\n timeoutMs?: number;\n}\n\n/**\n * Runs all `files` sequentially over the given CDP `connection`.\n *\n * For each file:\n * 1. Bundle with esbuild (includes SDK shim + runtime).\n * 2. Inject into the attached page via `Runtime.evaluate`.\n * 3. Await the `RunReport` JSON response.\n * 4. Accumulate results.\n *\n * Returns a `RelayRunReport` with per-file results and flattened totals.\n *\n * This function does NOT open or manage the relay connection — the caller\n * is responsible for attaching and closing it.\n *\n * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here\n * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.\n *\n * @param connection - Active CDP connection (relay or local kind).\n * @param files - Absolute paths to test files, run in order.\n * @param opts - Optional per-run overrides.\n */\nexport async function runTestFilesOverRelay(\n connection: CdpConnection,\n files: string[],\n opts?: RelayRunOptions,\n): Promise<RelayRunReport> {\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const fileResults: FileResult[] = [];\n\n for (const file of files) {\n let fileEntry: FileResult;\n try {\n const { code } = await bundleTestFile(file, opts?.bundleOptions);\n const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);\n if (rpcResult.ok) {\n fileEntry = { file, result: rpcResult.report };\n } else {\n fileEntry = { file, result: { error: rpcResult.error } };\n }\n } catch (e) {\n // Capture bundle/inject errors per-file so subsequent files still run.\n fileEntry = {\n file,\n result: {\n error: e instanceof Error ? e.message : String(e),\n },\n };\n }\n fileResults.push(fileEntry);\n }\n\n const totals = fileResults.reduce(\n (acc, { result }) => {\n if ('error' in result) {\n // Treat whole-file errors as a single failure.\n acc.failed += 1;\n acc.total += 1;\n } else {\n acc.passed += result.passed;\n acc.failed += result.failed;\n acc.skipped += result.skipped;\n acc.total += result.passed + result.failed + result.skipped;\n }\n return acc;\n },\n { passed: 0, failed: 0, skipped: 0, total: 0 },\n );\n\n return {\n startedAt,\n duration: Date.now() - wallStart,\n files: fileResults,\n totals,\n };\n}\n\n/**\n * Flattens all test results from a `RelayRunReport` into a single array.\n * Files that errored during bundle/inject produce a synthetic failed entry.\n */\nexport function flattenResults(report: RelayRunReport): Array<TestResult & { file: string }> {\n const out: Array<TestResult & { file: string }> = [];\n for (const { file, result } of report.files) {\n if ('error' in result) {\n out.push({\n file,\n name: `<bundle/inject error>`,\n status: 'fail',\n duration: 0,\n error: result.error,\n });\n } else {\n for (const t of result.tests) {\n out.push({ ...t, file });\n }\n }\n }\n return out;\n}\n","/**\n * `devtools-test` CLI.\n *\n * Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)\n * and exposes `runWithConnection` — the pure run core that bundles, injects, and\n * collects each file over a CDP connection. Today the run path that has a live\n * connection is the `run_tests` MCP tool (it runs these files against the\n * daemon's attached page); the CLI's own standalone relay attach (resolve CDP\n * URL → attach → run → close) is not wired yet, so `main()` resolves the matched\n * files and points the operator at the MCP tool.\n *\n * NOTE: no shebang in this source file — the tsdown entry's `banner` option\n * injects `#!/usr/bin/env node` into the compiled output (same pattern as\n * `src/mcp/cli.ts`).\n */\n\nimport { parseArgs } from 'node:util';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport { discoverTestFiles } from './discover.js';\nimport type { RelayRunOptions, RelayRunReport } from './relay-worker.js';\nimport { runTestFilesOverRelay } from './relay-worker.js';\n\n/* -------------------------------------------------------------------------- */\n/* CLI help */\n/* -------------------------------------------------------------------------- */\n\nconst USAGE = `\ndevtools-test — run mini-app tests on a real device WebView over the CDP relay\n\nUSAGE\n devtools-test <glob> [<glob> ...] [options]\n\nOPTIONS\n --timeout <ms> Per-file evaluate timeout in ms (default: 30000)\n --help, -h Show this help message\n\nDESCRIPTION\n Bundles each matched test file with esbuild (SDK imports redirected to\n window.__sdk), injects the bundle into the attached WebView via\n Runtime.evaluate, and returns a RunReport.\n\n A live CDP relay connection must be active before running tests. Use the\n \\`run_tests\\` MCP tool (via \\`devtools-mcp\\` / \\`/ait debug\\`) to run these files\n against an attached page — the CLI's own standalone relay attach is not wired\n yet (it currently resolves the matched files and defers to that tool).\n\nEXAMPLE\n devtools-test 'src/**/*.phone.test.ts' --timeout 60000\n\n`.trimStart();\n\n/* -------------------------------------------------------------------------- */\n/* Pure run function (testable without a real relay) */\n/* -------------------------------------------------------------------------- */\n\n/** Options for `runWithConnection`. */\nexport interface RunWithConnectionOptions extends RelayRunOptions {\n /** If true, print a summary to stdout. Defaults to false in tests. */\n printSummary?: boolean;\n}\n\n/**\n * Runs `files` over `connection` and returns the aggregate report.\n * This pure function is the testable core of the CLI (and is what the\n * `run_tests` MCP tool calls against the daemon's attached connection); it is\n * separate from `main()` so tests can call it without spawning a subprocess.\n *\n * A standalone CLI relay attach/detach lifecycle (connect via Chii relay URL,\n * `enableDomains`, run, then close) is not wired into `main()` yet.\n */\nexport async function runWithConnection(\n connection: CdpConnection,\n files: string[],\n opts?: RunWithConnectionOptions,\n): Promise<RelayRunReport> {\n const report = await runTestFilesOverRelay(connection, files, opts);\n\n if (opts?.printSummary) {\n const { totals } = report;\n process.stdout.write(\n `\\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\\n`,\n );\n }\n\n return report;\n}\n\n/* -------------------------------------------------------------------------- */\n/* main() — CLI entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * CLI entry point.\n *\n * Resolves the matched test files and prints a \"relay attach required\" notice:\n * the CLI's own standalone relay attach (resolve CDP URL, attach, run, close) is\n * not wired yet, so today these files run via the `run_tests` MCP tool against\n * the daemon's attached page.\n */\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<void> {\n let parsed: ReturnType<typeof parseArgs>;\n try {\n parsed = parseArgs({\n args: argv,\n options: {\n help: { type: 'boolean', short: 'h' },\n timeout: { type: 'string' },\n },\n allowPositionals: true,\n });\n } catch (e) {\n process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\\n`);\n process.exitCode = 1;\n return;\n }\n\n if (parsed.values.help || argv.length === 0) {\n process.stdout.write(USAGE);\n return;\n }\n\n // Discovery is shared with the `run_tests` MCP tool via `discoverTestFiles`,\n // so both expand patterns identically. We resolve the matched files here to\n // give the operator concrete feedback before deferring to the MCP run path.\n const files = await discoverTestFiles(parsed.positionals, process.cwd());\n if (files.length === 0) {\n process.stderr.write(`devtools-test: no test files matched ${parsed.positionals.join(', ')}\\n`);\n process.exitCode = 1;\n return;\n }\n\n // The CLI's standalone relay attach (resolve CDP URL, attach, close) is not\n // wired yet, so it cannot run on its own. The `run_tests` MCP tool already\n // runs these files against the daemon's attached connection.\n process.stderr.write(\n `devtools-test: matched ${files.length} test file(s), but direct CLI relay attach is not yet wired.\\n` +\n ` Use the devtools-mcp server (\\`devtools-mcp\\`) to start a debug session,\\n` +\n ` then the \\`run_tests\\` MCP tool to run these files against the attached page.\\n`,\n );\n process.exitCode = 1;\n}\n\n// Run main() when executed as a binary (not imported as a module).\n// Node ESM: `import.meta.url === pathToFileURL(process.argv[1]).href` is the\n// canonical \"am I the main module?\" check.\nif (import.meta.url === new URL(process.argv[1], 'file://').href) {\n main().catch((e: unknown) => {\n process.stderr.write(\n `devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\\n`,\n );\n process.exitCode = 1;\n });\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { i as createRelayPool, n as RelayConnectionFactory, t as RELAY_POOL_NAME } from "../pool-Dkp7I9Bf.js";
1
+ import { i as createRelayPool, n as RelayConnectionFactory, t as RELAY_POOL_NAME } from "../pool-CuVMzWGB.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-Dkp7I9Bf.js";
1
+ import { i as createRelayPool, n as RelayConnectionFactory, r as RelayPoolOptions, t as RELAY_POOL_NAME } from "../pool-CuVMzWGB.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-BzFQ3fv9.js";
1
+ import { a as runTestFilesOverRelay, i as flattenResults, n as RelayRunOptions, r as RelayRunReport, t as FileResult } from "../relay-worker-xxanNQGs.js";
2
2
  export { FileResult, RelayRunOptions, RelayRunReport, flattenResults, runTestFilesOverRelay };
@@ -1 +1 @@
1
- {"version":3,"file":"relay-worker.js","names":[],"sources":["../../src/test-runner/relay-worker.ts"],"sourcesContent":["/**\n * Orchestrator: runs a list of test files sequentially over a CDP relay.\n *\n * Each file goes through: bundle → inject → run → collect.\n * This is the MVP transport layer. Full Vitest pool integration (issue #645)\n * and `run_tests` MCP tool (issue #646) are NOT implemented here.\n *\n * Single-attach constraint: only one page is active at a time. Files run\n * sequentially; parallel execution across targets is a post-MVP concern.\n *\n * The 30-second per-file timeout is inherited from `injectAndRunBundle`.\n * For suites that exceed it, split the file into smaller pieces.\n *\n * SECRET-HANDLING: file paths are surfaced in reports; relay URLs are not.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport { type BundleOptions, bundleTestFile } from './bundle.js';\nimport { injectAndRunBundle } from './rpc.js';\nimport type { RunReport, TestResult } from './runtime.js';\n\n/** Per-file result in the aggregate `RunReport`. */\nexport interface FileResult {\n /** Absolute or relative path to the test file. */\n file: string;\n /** Full run report for this file, or an error if bundling/injection failed. */\n result: RunReport | { error: string };\n}\n\n/** Aggregate report returned by `runTestFilesOverRelay`. */\nexport interface RelayRunReport {\n /** ISO timestamp of when the run started. */\n startedAt: string;\n /** Total elapsed wall-clock milliseconds. */\n duration: number;\n /** Per-file results in execution order. */\n files: FileResult[];\n /** Flattened totals across all files. */\n totals: {\n passed: number;\n failed: number;\n skipped: number;\n total: number;\n };\n}\n\n/** Options for `runTestFilesOverRelay`. */\nexport interface RelayRunOptions {\n /**\n * Options forwarded to `bundleTestFile` for each file.\n */\n bundleOptions?: BundleOptions;\n /**\n * Per-file evaluate timeout in milliseconds. Defaults to 30 000.\n * Increase for long-running suites or split the file.\n */\n timeoutMs?: number;\n}\n\n/**\n * Runs all `files` sequentially over the given CDP `connection`.\n *\n * For each file:\n * 1. Bundle with esbuild (includes SDK shim + runtime).\n * 2. Inject into the attached page via `Runtime.evaluate`.\n * 3. Await the `RunReport` JSON response.\n * 4. Accumulate results.\n *\n * Returns a `RelayRunReport` with per-file results and flattened totals.\n *\n * This function does NOT open or manage the relay connection — the caller\n * is responsible for attaching and closing it.\n *\n * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here\n * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.\n *\n * @param connection - Active CDP connection (relay or local kind).\n * @param files - Absolute paths to test files, run in order.\n * @param opts - Optional per-run overrides.\n */\nexport async function runTestFilesOverRelay(\n connection: CdpConnection,\n files: string[],\n opts?: RelayRunOptions,\n): Promise<RelayRunReport> {\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const fileResults: FileResult[] = [];\n\n for (const file of files) {\n let fileEntry: FileResult;\n try {\n const { code } = await bundleTestFile(file, opts?.bundleOptions);\n const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);\n if (rpcResult.ok) {\n fileEntry = { file, result: rpcResult.report };\n } else {\n fileEntry = { file, result: { error: rpcResult.error } };\n }\n } catch (e) {\n // Capture bundle/inject errors per-file so subsequent files still run.\n fileEntry = {\n file,\n result: {\n error: e instanceof Error ? e.message : String(e),\n },\n };\n }\n fileResults.push(fileEntry);\n }\n\n const totals = fileResults.reduce(\n (acc, { result }) => {\n if ('error' in result) {\n // Treat whole-file errors as a single failure.\n acc.failed += 1;\n acc.total += 1;\n } else {\n acc.passed += result.passed;\n acc.failed += result.failed;\n acc.skipped += result.skipped;\n acc.total += result.passed + result.failed + result.skipped;\n }\n return acc;\n },\n { passed: 0, failed: 0, skipped: 0, total: 0 },\n );\n\n return {\n startedAt,\n duration: Date.now() - wallStart,\n files: fileResults,\n totals,\n };\n}\n\n/**\n * Flattens all test results from a `RelayRunReport` into a single array.\n * Files that errored during bundle/inject produce a synthetic failed entry.\n */\nexport function flattenResults(report: RelayRunReport): Array<TestResult & { file: string }> {\n const out: Array<TestResult & { file: string }> = [];\n for (const { file, result } of report.files) {\n if ('error' in result) {\n out.push({\n file,\n name: `<bundle/inject error>`,\n status: 'fail',\n duration: 0,\n error: result.error,\n });\n } else {\n for (const t of result.tests) {\n out.push({ ...t, file });\n }\n }\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgFA,eAAsB,sBACpB,YACA,OACA,MACyB;CACzB,MAAM,YAAY,KAAK,KAAK;CAC5B,MAAM,YAAY,IAAI,KAAK,UAAU,CAAC,aAAa;CACnD,MAAM,cAA4B,EAAE;AAEpC,MAAK,MAAM,QAAQ,OAAO;EACxB,IAAI;AACJ,MAAI;GACF,MAAM,EAAE,SAAS,MAAM,eAAe,MAAM,MAAM,cAAc;GAChE,MAAM,YAAY,MAAM,mBAAmB,YAAY,MAAM,MAAM,UAAU;AAC7E,OAAI,UAAU,GACZ,aAAY;IAAE;IAAM,QAAQ,UAAU;IAAQ;OAE9C,aAAY;IAAE;IAAM,QAAQ,EAAE,OAAO,UAAU,OAAO;IAAE;WAEnD,GAAG;AAEV,eAAY;IACV;IACA,QAAQ,EACN,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,EAClD;IACF;;AAEH,cAAY,KAAK,UAAU;;CAG7B,MAAM,SAAS,YAAY,QACxB,KAAK,EAAE,aAAa;AACnB,MAAI,WAAW,QAAQ;AAErB,OAAI,UAAU;AACd,OAAI,SAAS;SACR;AACL,OAAI,UAAU,OAAO;AACrB,OAAI,UAAU,OAAO;AACrB,OAAI,WAAW,OAAO;AACtB,OAAI,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO;;AAEtD,SAAO;IAET;EAAE,QAAQ;EAAG,QAAQ;EAAG,SAAS;EAAG,OAAO;EAAG,CAC/C;AAED,QAAO;EACL;EACA,UAAU,KAAK,KAAK,GAAG;EACvB,OAAO;EACP;EACD;;;;;;AAOH,SAAgB,eAAe,QAA8D;CAC3F,MAAM,MAA4C,EAAE;AACpD,MAAK,MAAM,EAAE,MAAM,YAAY,OAAO,MACpC,KAAI,WAAW,OACb,KAAI,KAAK;EACP;EACA,MAAM;EACN,QAAQ;EACR,UAAU;EACV,OAAO,OAAO;EACf,CAAC;KAEF,MAAK,MAAM,KAAK,OAAO,MACrB,KAAI,KAAK;EAAE,GAAG;EAAG;EAAM,CAAC;AAI9B,QAAO"}
1
+ {"version":3,"file":"relay-worker.js","names":[],"sources":["../../src/test-runner/relay-worker.ts"],"sourcesContent":["/**\n * Orchestrator: runs a list of test files sequentially over a CDP relay.\n *\n * Each file goes through: bundle → inject → run → collect.\n * This is the transport layer: it does NOT integrate with Vitest's pool or the\n * MCP surface. The Vitest custom pool (`pool.ts`) and the `run_tests` MCP tool\n * are separate callers that build on this orchestrator.\n *\n * Single-attach constraint: only one page is active at a time. Files run\n * sequentially; parallel execution across targets is out of scope.\n *\n * The 30-second per-file timeout is inherited from `injectAndRunBundle`.\n * For suites that exceed it, split the file into smaller pieces.\n *\n * SECRET-HANDLING: file paths are surfaced in reports; relay URLs are not.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport { type BundleOptions, bundleTestFile } from './bundle.js';\nimport { injectAndRunBundle } from './rpc.js';\nimport type { RunReport, TestResult } from './runtime.js';\n\n/** Per-file result in the aggregate `RunReport`. */\nexport interface FileResult {\n /** Absolute or relative path to the test file. */\n file: string;\n /** Full run report for this file, or an error if bundling/injection failed. */\n result: RunReport | { error: string };\n}\n\n/** Aggregate report returned by `runTestFilesOverRelay`. */\nexport interface RelayRunReport {\n /** ISO timestamp of when the run started. */\n startedAt: string;\n /** Total elapsed wall-clock milliseconds. */\n duration: number;\n /** Per-file results in execution order. */\n files: FileResult[];\n /** Flattened totals across all files. */\n totals: {\n passed: number;\n failed: number;\n skipped: number;\n total: number;\n };\n}\n\n/** Options for `runTestFilesOverRelay`. */\nexport interface RelayRunOptions {\n /**\n * Options forwarded to `bundleTestFile` for each file.\n */\n bundleOptions?: BundleOptions;\n /**\n * Per-file evaluate timeout in milliseconds. Defaults to 30 000.\n * Increase for long-running suites or split the file.\n */\n timeoutMs?: number;\n}\n\n/**\n * Runs all `files` sequentially over the given CDP `connection`.\n *\n * For each file:\n * 1. Bundle with esbuild (includes SDK shim + runtime).\n * 2. Inject into the attached page via `Runtime.evaluate`.\n * 3. Await the `RunReport` JSON response.\n * 4. Accumulate results.\n *\n * Returns a `RelayRunReport` with per-file results and flattened totals.\n *\n * This function does NOT open or manage the relay connection — the caller\n * is responsible for attaching and closing it.\n *\n * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here\n * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.\n *\n * @param connection - Active CDP connection (relay or local kind).\n * @param files - Absolute paths to test files, run in order.\n * @param opts - Optional per-run overrides.\n */\nexport async function runTestFilesOverRelay(\n connection: CdpConnection,\n files: string[],\n opts?: RelayRunOptions,\n): Promise<RelayRunReport> {\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const fileResults: FileResult[] = [];\n\n for (const file of files) {\n let fileEntry: FileResult;\n try {\n const { code } = await bundleTestFile(file, opts?.bundleOptions);\n const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);\n if (rpcResult.ok) {\n fileEntry = { file, result: rpcResult.report };\n } else {\n fileEntry = { file, result: { error: rpcResult.error } };\n }\n } catch (e) {\n // Capture bundle/inject errors per-file so subsequent files still run.\n fileEntry = {\n file,\n result: {\n error: e instanceof Error ? e.message : String(e),\n },\n };\n }\n fileResults.push(fileEntry);\n }\n\n const totals = fileResults.reduce(\n (acc, { result }) => {\n if ('error' in result) {\n // Treat whole-file errors as a single failure.\n acc.failed += 1;\n acc.total += 1;\n } else {\n acc.passed += result.passed;\n acc.failed += result.failed;\n acc.skipped += result.skipped;\n acc.total += result.passed + result.failed + result.skipped;\n }\n return acc;\n },\n { passed: 0, failed: 0, skipped: 0, total: 0 },\n );\n\n return {\n startedAt,\n duration: Date.now() - wallStart,\n files: fileResults,\n totals,\n };\n}\n\n/**\n * Flattens all test results from a `RelayRunReport` into a single array.\n * Files that errored during bundle/inject produce a synthetic failed entry.\n */\nexport function flattenResults(report: RelayRunReport): Array<TestResult & { file: string }> {\n const out: Array<TestResult & { file: string }> = [];\n for (const { file, result } of report.files) {\n if ('error' in result) {\n out.push({\n file,\n name: `<bundle/inject error>`,\n status: 'fail',\n duration: 0,\n error: result.error,\n });\n } else {\n for (const t of result.tests) {\n out.push({ ...t, file });\n }\n }\n }\n return out;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAiFA,eAAsB,sBACpB,YACA,OACA,MACyB;CACzB,MAAM,YAAY,KAAK,KAAK;CAC5B,MAAM,YAAY,IAAI,KAAK,UAAU,CAAC,aAAa;CACnD,MAAM,cAA4B,EAAE;AAEpC,MAAK,MAAM,QAAQ,OAAO;EACxB,IAAI;AACJ,MAAI;GACF,MAAM,EAAE,SAAS,MAAM,eAAe,MAAM,MAAM,cAAc;GAChE,MAAM,YAAY,MAAM,mBAAmB,YAAY,MAAM,MAAM,UAAU;AAC7E,OAAI,UAAU,GACZ,aAAY;IAAE;IAAM,QAAQ,UAAU;IAAQ;OAE9C,aAAY;IAAE;IAAM,QAAQ,EAAE,OAAO,UAAU,OAAO;IAAE;WAEnD,GAAG;AAEV,eAAY;IACV;IACA,QAAQ,EACN,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,EAClD;IACF;;AAEH,cAAY,KAAK,UAAU;;CAG7B,MAAM,SAAS,YAAY,QACxB,KAAK,EAAE,aAAa;AACnB,MAAI,WAAW,QAAQ;AAErB,OAAI,UAAU;AACd,OAAI,SAAS;SACR;AACL,OAAI,UAAU,OAAO;AACrB,OAAI,UAAU,OAAO;AACrB,OAAI,WAAW,OAAO;AACtB,OAAI,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO;;AAEtD,SAAO;IAET;EAAE,QAAQ;EAAG,QAAQ;EAAG,SAAS;EAAG,OAAO;EAAG,CAC/C;AAED,QAAO;EACL;EACA,UAAU,KAAK,KAAK,GAAG;EACvB,OAAO;EACP;EACD;;;;;;AAOH,SAAgB,eAAe,QAA8D;CAC3F,MAAM,MAA4C,EAAE;AACpD,MAAK,MAAM,EAAE,MAAM,YAAY,OAAO,MACpC,KAAI,WAAW,OACb,KAAI,KAAK;EACP;EACA,MAAM;EACN,QAAQ;EACR,UAAU;EACV,OAAO,OAAO;EACf,CAAC;KAEF,MAAK,MAAM,KAAK,OAAO,MACrB,KAAI,KAAK;EAAE,GAAG;EAAG;EAAM,CAAC;AAI9B,QAAO"}
@@ -1,5 +1,5 @@
1
1
  import { t as CdpConnection } from "../cdp-connection-C0AP0tH2.js";
2
- import { t as RunReport } from "../runtime-ORdrpizY.js";
2
+ import { t as RunReport } from "../runtime-Wi5d6Ywz.js";
3
3
 
4
4
  //#region src/test-runner/rpc.d.ts
5
5
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"rpc.d.ts","names":[],"sources":["../../src/test-runner/rpc.ts"],"mappings":";;;;;;;;;;;;;AAkEA;;;iBAtCgB,uBAAA,CAAwB,UAAA;;AAgFxC;;KApDY,YAAA;EAAiB,EAAA;EAAU,MAAA,EAAQ,SAAA;AAAA;EAAgB,EAAA;EAAW,KAAA;AAAA;;;;;;;;;iBAU1D,mBAAA,CAAoB,QAAA,YAAoB,YAAA;;;;;;;;;;;;;;iBA0ClC,kBAAA,CACpB,UAAA,EAAY,aAAA,EACZ,UAAA,UACA,SAAA,YACC,OAAA,CAAQ,YAAA"}
1
+ {"version":3,"file":"rpc.d.ts","names":[],"sources":["../../src/test-runner/rpc.ts"],"mappings":";;;;;;;;;;;;;AAmEA;;;iBAvCgB,uBAAA,CAAwB,UAAA;;AAiFxC;;KApDY,YAAA;EAAiB,EAAA;EAAU,MAAA,EAAQ,SAAA;AAAA;EAAgB,EAAA;EAAW,KAAA;AAAA;;;;;;;;;iBAU1D,mBAAA,CAAoB,QAAA,YAAoB,YAAA;;;;;;;;;;;;;;iBA0ClC,kBAAA,CACpB,UAAA,EAAY,aAAA,EACZ,UAAA,UACA,SAAA,YACC,OAAA,CAAQ,YAAA"}
@@ -14,7 +14,7 @@ const DEFAULT_TIMEOUT_MS = 3e4;
14
14
  * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
15
15
  */
16
16
  function buildRunTestsExpression(bundleCode) {
17
- 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') { return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule is not a function'}); } try { const report = await globalThis.__testBundle.runTestModule(); return JSON.stringify({ok:true,value:report}); } catch(e) { return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)}); }})()`;
17
+ 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)}); }})()`;
18
18
  }
19
19
  /**
20
20
  * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
@@ -1 +1 @@
1
- {"version":3,"file":"rpc.js","names":[],"sources":["../../src/test-runner/rpc.ts"],"sourcesContent":["/**\n * Node-side RPC helpers for injecting and collecting test execution over CDP.\n *\n * Uses the same IIFE + JSON.stringify envelope pattern as `buildCallSdkExpression`\n * in `src/mcp/tools.ts` to reliably shuttle structured results through\n * `Runtime.evaluate`'s `returnByValue: true` boundary.\n *\n * SECRET-HANDLING: bundle code, relay URLs, and result values are NOT logged.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { RunReport } from './runtime.js';\n\n/** Maximum milliseconds to wait for a single evaluate round-trip. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Wraps bundle code in a self-executing IIFE that:\n * 1. Evaluates the bundle (registering describe/it/test).\n * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.\n * 3. Returns a JSON-serialised `RunReport` string.\n *\n * The double-serialisation (RunReport → JSON string → returnByValue string)\n * is intentional: CDP `returnByValue` reliably transports strings; deeply\n * nested objects can lose fidelity across the Chii relay.\n *\n * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.\n */\nexport function buildRunTestsExpression(bundleCode: string): string {\n // We trust bundleCode is already a self-contained IIFE that installs\n // `window.__testBundle` (or `globalThis.__testBundle`).\n // We then call `__testBundle.runTestModule()` and return a JSON string.\n return (\n `(async () => {` +\n // Step 1: evaluate the bundle to register tests\n ` try { ${bundleCode} } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)});` +\n ` }` +\n // Step 2: check that the expected export is present\n ` if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule is not a function'});` +\n ` }` +\n // Step 3: run tests\n ` try {` +\n ` const report = await globalThis.__testBundle.runTestModule();` +\n ` return JSON.stringify({ok:true,value:report});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Result of `injectAndRunBundle`.\n */\nexport type RpcRunResult = { ok: true; report: RunReport } | { ok: false; error: string };\n\n/**\n * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`\n * evaluate call into a typed `RpcRunResult`.\n *\n * Throws only on parse failure — an `ok:false` envelope is a normal result.\n *\n * SECRET-HANDLING: `rawValue` is not included in error messages.\n */\nexport function parseRunTestsResult(rawValue: unknown): RpcRunResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `rpc.parseRunTestsResult: unexpected return type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue — could contain secrets.\n throw new Error('rpc.parseRunTestsResult: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('rpc.parseRunTestsResult: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, report: obj.value as RunReport };\n }\n if (obj.ok === false) {\n return {\n ok: false,\n error: typeof obj.error === 'string' ? obj.error : String(obj.error),\n };\n }\n throw new Error('rpc.parseRunTestsResult: result missing \"ok\" field');\n}\n\n/**\n * Injects `bundleCode` into the attached page and awaits test execution.\n *\n * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the\n * async IIFE to settle. The 30-second CDP command timeout covers even\n * long-running test suites; split into smaller files if you hit it.\n *\n * @param connection - Active CDP connection (relay or local).\n * @param bundleCode - IIFE bundle string from `bundleTestFile`.\n * @param timeoutMs - Override the default 30 s timeout.\n *\n * SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.\n */\nexport async function injectAndRunBundle(\n connection: CdpConnection,\n bundleCode: string,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n): Promise<RpcRunResult> {\n const expression = buildRunTestsExpression(bundleCode);\n\n // Use AbortSignal-style timeout via Promise.race so we surface a clear\n // message rather than hanging indefinitely.\n const timeoutPromise = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs),\n );\n\n const evalPromise = connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\n });\n\n const cdpResult = await Promise.race([evalPromise, timeoutPromise]);\n\n if (cdpResult.exceptionDetails) {\n // Surface only the engine error string — not the expression or value.\n const msg =\n cdpResult.exceptionDetails.exception?.description ??\n cdpResult.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`rpc.injectAndRunBundle: ${msg}`);\n }\n\n return parseRunTestsResult(cdpResult.result.value);\n}\n"],"mappings":";;AAcA,MAAM,qBAAqB;;;;;;;;;;;;;AAc3B,SAAgB,wBAAwB,YAA4B;AAIlE,QACE,yBAEW,WAAW;;;;;;;;;;AA+B1B,SAAgB,oBAAoB,UAAiC;AACnE,KAAI,OAAO,aAAa,SACtB,OAAM,IAAI,MACR,oDAAoD,OAAO,SAAS,0BACrE;CAEH,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,SAAS;SACvB;AAEN,QAAM,IAAI,MAAM,2DAA2D;;AAE7E,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MAAM,0DAA0D;CAE5E,MAAM,MAAM;AACZ,KAAI,IAAI,OAAO,KACb,QAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAoB;AAErD,KAAI,IAAI,OAAO,MACb,QAAO;EACL,IAAI;EACJ,OAAO,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ,OAAO,IAAI,MAAM;EACrE;AAEH,OAAM,IAAI,MAAM,uDAAqD;;;;;;;;;;;;;;;AAgBvE,eAAsB,mBACpB,YACA,YACA,YAAY,oBACW;CACvB,MAAM,aAAa,wBAAwB,WAAW;CAItD,MAAM,iBAAiB,IAAI,SAAgB,GAAG,WAC5C,iBAAiB,uBAAO,IAAI,MAAM,iCAAiC,UAAU,IAAI,CAAC,EAAE,UAAU,CAC/F;CAED,MAAM,cAAc,WAAW,KAAK,oBAAoB;EACtD;EACA,eAAe;EACf,cAAc;EACf,CAAC;CAEF,MAAM,YAAY,MAAM,QAAQ,KAAK,CAAC,aAAa,eAAe,CAAC;AAEnE,KAAI,UAAU,kBAAkB;EAE9B,MAAM,MACJ,UAAU,iBAAiB,WAAW,eACtC,UAAU,iBAAiB,QAC3B;AACF,QAAM,IAAI,MAAM,2BAA2B,MAAM;;AAGnD,QAAO,oBAAoB,UAAU,OAAO,MAAM"}
1
+ {"version":3,"file":"rpc.js","names":[],"sources":["../../src/test-runner/rpc.ts"],"sourcesContent":["/**\n * Node-side RPC helpers for injecting and collecting test execution over CDP.\n *\n * Uses the same IIFE + JSON.stringify envelope pattern as `buildCallSdkExpression`\n * in `src/mcp/tools.ts` to reliably shuttle structured results through\n * `Runtime.evaluate`'s `returnByValue: true` boundary.\n *\n * SECRET-HANDLING: bundle code, relay URLs, and result values are NOT logged.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { RunReport } from './runtime.js';\n\n/** Maximum milliseconds to wait for a single evaluate round-trip. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Wraps bundle code in a self-executing IIFE that:\n * 1. Evaluates the bundle (registering describe/it/test).\n * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.\n * 3. Returns a JSON-serialised `RunReport` string.\n *\n * The double-serialisation (RunReport → JSON string → returnByValue string)\n * is intentional: CDP `returnByValue` reliably transports strings; deeply\n * nested objects can lose fidelity across the Chii relay.\n *\n * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.\n */\nexport function buildRunTestsExpression(bundleCode: string): string {\n // We trust bundleCode is already a self-contained IIFE that installs\n // `window.__testBundle` (or `globalThis.__testBundle`).\n // We then call `__testBundle.runTestModule()` and return a JSON string.\n return (\n `(async () => {` +\n // Step 1: evaluate the bundle to register tests\n ` try { ${bundleCode} } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)});` +\n ` }` +\n // Step 2: check that the expected exports are present\n ` if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'});` +\n ` }` +\n // Step 3: run tests — pass the factory so runTestModule installs globals\n // first, then invokes the factory to register describe/it/test blocks.\n ` try {` +\n ` const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory);` +\n ` return JSON.stringify({ok:true,value:report});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Result of `injectAndRunBundle`.\n */\nexport type RpcRunResult = { ok: true; report: RunReport } | { ok: false; error: string };\n\n/**\n * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`\n * evaluate call into a typed `RpcRunResult`.\n *\n * Throws only on parse failure — an `ok:false` envelope is a normal result.\n *\n * SECRET-HANDLING: `rawValue` is not included in error messages.\n */\nexport function parseRunTestsResult(rawValue: unknown): RpcRunResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `rpc.parseRunTestsResult: unexpected return type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue — could contain secrets.\n throw new Error('rpc.parseRunTestsResult: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('rpc.parseRunTestsResult: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, report: obj.value as RunReport };\n }\n if (obj.ok === false) {\n return {\n ok: false,\n error: typeof obj.error === 'string' ? obj.error : String(obj.error),\n };\n }\n throw new Error('rpc.parseRunTestsResult: result missing \"ok\" field');\n}\n\n/**\n * Injects `bundleCode` into the attached page and awaits test execution.\n *\n * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the\n * async IIFE to settle. The 30-second CDP command timeout covers even\n * long-running test suites; split into smaller files if you hit it.\n *\n * @param connection - Active CDP connection (relay or local).\n * @param bundleCode - IIFE bundle string from `bundleTestFile`.\n * @param timeoutMs - Override the default 30 s timeout.\n *\n * SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.\n */\nexport async function injectAndRunBundle(\n connection: CdpConnection,\n bundleCode: string,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n): Promise<RpcRunResult> {\n const expression = buildRunTestsExpression(bundleCode);\n\n // Use AbortSignal-style timeout via Promise.race so we surface a clear\n // message rather than hanging indefinitely.\n const timeoutPromise = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs),\n );\n\n const evalPromise = connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\n });\n\n const cdpResult = await Promise.race([evalPromise, timeoutPromise]);\n\n if (cdpResult.exceptionDetails) {\n // Surface only the engine error string — not the expression or value.\n const msg =\n cdpResult.exceptionDetails.exception?.description ??\n cdpResult.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`rpc.injectAndRunBundle: ${msg}`);\n }\n\n return parseRunTestsResult(cdpResult.result.value);\n}\n"],"mappings":";;AAcA,MAAM,qBAAqB;;;;;;;;;;;;;AAc3B,SAAgB,wBAAwB,YAA4B;AAIlE,QACE,yBAEW,WAAW;;;;;;;;;;AAgC1B,SAAgB,oBAAoB,UAAiC;AACnE,KAAI,OAAO,aAAa,SACtB,OAAM,IAAI,MACR,oDAAoD,OAAO,SAAS,0BACrE;CAEH,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,SAAS;SACvB;AAEN,QAAM,IAAI,MAAM,2DAA2D;;AAE7E,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MAAM,0DAA0D;CAE5E,MAAM,MAAM;AACZ,KAAI,IAAI,OAAO,KACb,QAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAoB;AAErD,KAAI,IAAI,OAAO,MACb,QAAO;EACL,IAAI;EACJ,OAAO,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ,OAAO,IAAI,MAAM;EACrE;AAEH,OAAM,IAAI,MAAM,uDAAqD;;;;;;;;;;;;;;;AAgBvE,eAAsB,mBACpB,YACA,YACA,YAAY,oBACW;CACvB,MAAM,aAAa,wBAAwB,WAAW;CAItD,MAAM,iBAAiB,IAAI,SAAgB,GAAG,WAC5C,iBAAiB,uBAAO,IAAI,MAAM,iCAAiC,UAAU,IAAI,CAAC,EAAE,UAAU,CAC/F;CAED,MAAM,cAAc,WAAW,KAAK,oBAAoB;EACtD;EACA,eAAe;EACf,cAAc;EACf,CAAC;CAEF,MAAM,YAAY,MAAM,QAAQ,KAAK,CAAC,aAAa,eAAe,CAAC;AAEnE,KAAI,UAAU,kBAAkB;EAE9B,MAAM,MACJ,UAAU,iBAAiB,WAAW,eACtC,UAAU,iBAAiB,QAC3B;AACF,QAAM,IAAI,MAAM,2BAA2B,MAAM;;AAGnD,QAAO,oBAAoB,UAAU,OAAO,MAAM"}
@@ -1,4 +1,4 @@
1
- import { t as RunReport } from "../runtime-ORdrpizY.js";
1
+ import { t as RunReport } from "../runtime-Wi5d6Ywz.js";
2
2
  import { File, TaskEventPack, TaskResultPack } from "@vitest/runner";
3
3
 
4
4
  //#region src/test-runner/task-graph.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ait-co/devtools",
3
- "version": "0.1.108",
3
+ "version": "0.1.109",
4
4
  "description": "Development tools for Apps in Toss mini-apps — mock SDK, floating devtools panel, and universal bundler plugin",
5
5
  "type": "module",
6
6
  "engines": {