@ait-co/devtools 0.1.106 → 0.1.108

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 (91) hide show
  1. package/dist/bundle-BJm5jk56.d.ts +49 -0
  2. package/dist/bundle-BJm5jk56.d.ts.map +1 -0
  3. package/dist/cdp-connection-C0AP0tH2.d.ts +277 -0
  4. package/dist/cdp-connection-C0AP0tH2.d.ts.map +1 -0
  5. package/dist/in-app/auto.d.ts +17 -0
  6. package/dist/in-app/auto.d.ts.map +1 -1
  7. package/dist/in-app/auto.js +76 -0
  8. package/dist/in-app/auto.js.map +1 -1
  9. package/dist/in-app/index.d.ts +48 -1
  10. package/dist/in-app/index.d.ts.map +1 -1
  11. package/dist/in-app/index.js +60 -1
  12. package/dist/in-app/index.js.map +1 -1
  13. package/dist/mcp/cli.js +507 -11
  14. package/dist/mcp/cli.js.map +1 -1
  15. package/dist/mcp/server.d.ts.map +1 -1
  16. package/dist/mcp/server.js +60 -3
  17. package/dist/mcp/server.js.map +1 -1
  18. package/dist/panel/index.js +1 -1
  19. package/dist/pool-Dkp7I9Bf.d.ts +14577 -0
  20. package/dist/pool-Dkp7I9Bf.d.ts.map +1 -0
  21. package/dist/{relay-secret-store-I5q2Wvvv.cjs → relay-secret-store-CPBBlV3J.cjs} +2 -2
  22. package/dist/{relay-secret-store-I5q2Wvvv.cjs.map → relay-secret-store-CPBBlV3J.cjs.map} +1 -1
  23. package/dist/{relay-secret-store-Bns5rndt.js → relay-secret-store-DBwzoCXQ.js} +2 -2
  24. package/dist/{relay-secret-store-Bns5rndt.js.map → relay-secret-store-DBwzoCXQ.js.map} +1 -1
  25. package/dist/{relay-secret-store-B0DH-8Qb.js → relay-secret-store-DhzAnnj-.js} +2 -2
  26. package/dist/{relay-secret-store-B0DH-8Qb.js.map → relay-secret-store-DhzAnnj-.js.map} +1 -1
  27. package/dist/{relay-url-store-CvmnevcO.cjs → relay-url-store-C9QLhB2p.cjs} +2 -2
  28. package/dist/{relay-url-store-CvmnevcO.cjs.map → relay-url-store-C9QLhB2p.cjs.map} +1 -1
  29. package/dist/{relay-url-store-BPeUZsiY.js → relay-url-store-CwKT7i04.js} +2 -2
  30. package/dist/{relay-url-store-BPeUZsiY.js.map → relay-url-store-CwKT7i04.js.map} +1 -1
  31. package/dist/{relay-url-store-DJHZjk8o.js → relay-url-store-j16TRTiJ.js} +2 -2
  32. package/dist/{relay-url-store-DJHZjk8o.js.map → relay-url-store-j16TRTiJ.js.map} +1 -1
  33. package/dist/relay-worker-BzFQ3fv9.d.ts +74 -0
  34. package/dist/relay-worker-BzFQ3fv9.d.ts.map +1 -0
  35. package/dist/runtime-ORdrpizY.d.ts +50 -0
  36. package/dist/runtime-ORdrpizY.d.ts.map +1 -0
  37. package/dist/test-runner/bundle.d.ts +2 -0
  38. package/dist/test-runner/bundle.js +95 -0
  39. package/dist/test-runner/bundle.js.map +1 -0
  40. package/dist/test-runner/cli.d.ts +417 -0
  41. package/dist/test-runner/cli.d.ts.map +1 -0
  42. package/dist/test-runner/cli.js +377 -0
  43. package/dist/test-runner/cli.js.map +1 -0
  44. package/dist/test-runner/config.d.ts +80 -0
  45. package/dist/test-runner/config.d.ts.map +1 -0
  46. package/dist/test-runner/config.js +54 -0
  47. package/dist/test-runner/config.js.map +1 -0
  48. package/dist/test-runner/pool.d.ts +2 -0
  49. package/dist/test-runner/pool.js +136 -0
  50. package/dist/test-runner/pool.js.map +1 -0
  51. package/dist/test-runner/relay-worker.d.ts +2 -0
  52. package/dist/test-runner/relay-worker.js +96 -0
  53. package/dist/test-runner/relay-worker.js.map +1 -0
  54. package/dist/test-runner/rpc.d.ts +53 -0
  55. package/dist/test-runner/rpc.d.ts.map +1 -0
  56. package/dist/test-runner/rpc.js +78 -0
  57. package/dist/test-runner/rpc.js.map +1 -0
  58. package/dist/test-runner/task-graph.d.ts +38 -0
  59. package/dist/test-runner/task-graph.d.ts.map +1 -0
  60. package/dist/test-runner/task-graph.js +182 -0
  61. package/dist/test-runner/task-graph.js.map +1 -0
  62. package/dist/{totp-BmKSPb5d.js → totp-95OAa20j.js} +2 -2
  63. package/dist/totp-95OAa20j.js.map +1 -0
  64. package/dist/{totp-BwDZ6dUT.cjs → totp-BjtoQNfu.cjs} +2 -2
  65. package/dist/totp-BjtoQNfu.cjs.map +1 -0
  66. package/dist/totp-D1pulXLa.js +3 -0
  67. package/dist/{totp-DYdP9N3o.js → totp-DIbrZtI7.js} +2 -2
  68. package/dist/totp-DIbrZtI7.js.map +1 -0
  69. package/dist/{totp-CNw0w89F.cjs → totp-Df252ZdA.cjs} +2 -2
  70. package/dist/totp-Df252ZdA.cjs.map +1 -0
  71. package/dist/{totp-Xq3ACwkm.js → totp-WY6l0ysP.js} +2 -2
  72. package/dist/totp-WY6l0ysP.js.map +1 -0
  73. package/dist/{tunnel-BmDcTrnU.js → tunnel-BjJROkcj.js} +2 -2
  74. package/dist/{tunnel-BmDcTrnU.js.map → tunnel-BjJROkcj.js.map} +1 -1
  75. package/dist/{tunnel-RB5zB8IK.cjs → tunnel-d_G9AIFn.cjs} +2 -2
  76. package/dist/{tunnel-RB5zB8IK.cjs.map → tunnel-d_G9AIFn.cjs.map} +1 -1
  77. package/dist/unplugin/index.cjs +4 -4
  78. package/dist/unplugin/index.d.cts +13 -32
  79. package/dist/unplugin/index.d.cts.map +1 -1
  80. package/dist/unplugin/index.d.ts +13 -32
  81. package/dist/unplugin/index.d.ts.map +1 -1
  82. package/dist/unplugin/index.js +4 -4
  83. package/dist/unplugin/tunnel.cjs +1 -1
  84. package/dist/unplugin/tunnel.js +1 -1
  85. package/package.json +13 -3
  86. package/dist/totp-BcBNRoDD.js +0 -3
  87. package/dist/totp-BmKSPb5d.js.map +0 -1
  88. package/dist/totp-BwDZ6dUT.cjs.map +0 -1
  89. package/dist/totp-CNw0w89F.cjs.map +0 -1
  90. package/dist/totp-DYdP9N3o.js.map +0 -1
  91. package/dist/totp-Xq3ACwkm.js.map +0 -1
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { glob } from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { isAbsolute, resolve } from "node:path";
6
+ //#region src/test-runner/discover.ts
7
+ /**
8
+ * Test-file discovery shared by the `devtools-test` CLI and the `run_tests`
9
+ * MCP tool, so both expand glob patterns with identical semantics.
10
+ *
11
+ * Uses Node's built-in `fs/promises` `glob` (Node 22+) — no extra dependency,
12
+ * which keeps the MCP daemon install graph lean (a plain glob lib would land in
13
+ * the `npx … devtools-mcp` path for no benefit).
14
+ *
15
+ * Pure Node IO only (`node:fs/promises` + `node:path`) — react-free, so it is
16
+ * safe to import from the MCP daemon graph.
17
+ */
18
+ /**
19
+ * Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of
20
+ * ABSOLUTE test file paths, resolved relative to `cwd`.
21
+ *
22
+ * A plain (non-glob) path passes through when it matches a real file; a glob
23
+ * expands against `cwd`. Absolute matches are kept as-is; relative matches are
24
+ * resolved against `cwd`. `bundleTestFile` requires an absolute path, so the
25
+ * absolute output feeds it directly.
26
+ *
27
+ * @param patterns Glob patterns or file paths (e.g. `['src/**\/*.phone.test.ts']`).
28
+ * @param cwd Base directory for relative patterns/results.
29
+ * @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.
30
+ */
31
+ async function discoverTestFiles(patterns, cwd) {
32
+ const out = /* @__PURE__ */ new Set();
33
+ for await (const match of glob(patterns, { cwd })) out.add(isAbsolute(match) ? match : resolve(cwd, match));
34
+ return [...out].sort();
35
+ }
36
+ //#endregion
37
+ //#region src/test-runner/bundle.ts
38
+ /**
39
+ * esbuild-based bundler for user test files.
40
+ *
41
+ * 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).
47
+ *
48
+ * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
49
+ */
50
+ /**
51
+ * Matches the bare SDK package and any sub-path import
52
+ * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).
53
+ * Built from {@link SDK_PACKAGE} so the package name has a single source.
54
+ */
55
+ const SDK_IMPORT_FILTER = new RegExp(`^${"@apps-in-toss/web-framework".replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
56
+ /**
57
+ * esbuild plugin that intercepts SDK imports and redirects them to the
58
+ * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.
59
+ *
60
+ * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),
61
+ * esbuild resolves it to a virtual module that re-exports all named exports
62
+ * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not
63
+ * be available in the test environment) while still making named imports work.
64
+ *
65
+ * If `window.__sdk` is absent (non-dog-food build), every access throws a
66
+ * descriptive error rather than returning `undefined` silently.
67
+ */
68
+ function sdkRedirectPlugin() {
69
+ return {
70
+ name: "sdk-redirect",
71
+ setup(build) {
72
+ build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({
73
+ path: args.path,
74
+ namespace: "sdk-redirect"
75
+ }));
76
+ build.onLoad({
77
+ filter: /.*/,
78
+ namespace: "sdk-redirect"
79
+ }, () => ({
80
+ contents: `
81
+ var __proxy = (typeof window !== 'undefined' && window.__sdk)
82
+ ? window.__sdk
83
+ : new Proxy({}, {
84
+ get: function(_t, p) {
85
+ throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
86
+ }
87
+ });
88
+ module.exports = __proxy;
89
+ `,
90
+ loader: "js"
91
+ }));
92
+ }
93
+ };
94
+ }
95
+ /**
96
+ * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
97
+ *
98
+ * The IIFE installs `window.__testBundle` (or the custom `globalName`) with
99
+ * `runTestModule` as the callable entry point.
100
+ *
101
+ * @param absPath - Absolute path to the user test file.
102
+ * @param opts - Optional bundling overrides.
103
+ */
104
+ async function bundleTestFile(absPath, opts) {
105
+ const globalName = opts?.globalName ?? "__testBundle";
106
+ const extraExternals = opts?.extraExternals ?? [];
107
+ const result = await (await import("esbuild")).build({
108
+ entryPoints: [absPath],
109
+ bundle: true,
110
+ format: "iife",
111
+ globalName,
112
+ platform: "browser",
113
+ target: "es2022",
114
+ write: false,
115
+ plugins: [sdkRedirectPlugin()],
116
+ external: extraExternals,
117
+ treeShaking: true
118
+ });
119
+ const warnings = result.warnings.map((w) => `${path.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
120
+ const outputFile = result.outputFiles?.[0];
121
+ if (!outputFile) throw new Error("bundleTestFile: esbuild produced no output — check entryPoints");
122
+ return {
123
+ code: outputFile.text,
124
+ warnings
125
+ };
126
+ }
127
+ //#endregion
128
+ //#region src/test-runner/rpc.ts
129
+ /** Maximum milliseconds to wait for a single evaluate round-trip. */
130
+ const DEFAULT_TIMEOUT_MS = 3e4;
131
+ /**
132
+ * Wraps bundle code in a self-executing IIFE that:
133
+ * 1. Evaluates the bundle (registering describe/it/test).
134
+ * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.
135
+ * 3. Returns a JSON-serialised `RunReport` string.
136
+ *
137
+ * The double-serialisation (RunReport → JSON string → returnByValue string)
138
+ * is intentional: CDP `returnByValue` reliably transports strings; deeply
139
+ * nested objects can lose fidelity across the Chii relay.
140
+ *
141
+ * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
142
+ */
143
+ 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)}); }})()`;
145
+ }
146
+ /**
147
+ * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
148
+ * evaluate call into a typed `RpcRunResult`.
149
+ *
150
+ * Throws only on parse failure — an `ok:false` envelope is a normal result.
151
+ *
152
+ * SECRET-HANDLING: `rawValue` is not included in error messages.
153
+ */
154
+ function parseRunTestsResult(rawValue) {
155
+ if (typeof rawValue !== "string") throw new Error(`rpc.parseRunTestsResult: unexpected return type "${typeof rawValue}" — expected JSON string`);
156
+ let parsed;
157
+ try {
158
+ parsed = JSON.parse(rawValue);
159
+ } catch {
160
+ throw new Error("rpc.parseRunTestsResult: bridge returned non-JSON string");
161
+ }
162
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("rpc.parseRunTestsResult: parsed result is not an object");
163
+ const obj = parsed;
164
+ if (obj.ok === true) return {
165
+ ok: true,
166
+ report: obj.value
167
+ };
168
+ if (obj.ok === false) return {
169
+ ok: false,
170
+ error: typeof obj.error === "string" ? obj.error : String(obj.error)
171
+ };
172
+ throw new Error("rpc.parseRunTestsResult: result missing \"ok\" field");
173
+ }
174
+ /**
175
+ * Injects `bundleCode` into the attached page and awaits test execution.
176
+ *
177
+ * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the
178
+ * async IIFE to settle. The 30-second CDP command timeout covers even
179
+ * long-running test suites; split into smaller files if you hit it.
180
+ *
181
+ * @param connection - Active CDP connection (relay or local).
182
+ * @param bundleCode - IIFE bundle string from `bundleTestFile`.
183
+ * @param timeoutMs - Override the default 30 s timeout.
184
+ *
185
+ * SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.
186
+ */
187
+ async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TIMEOUT_MS) {
188
+ const expression = buildRunTestsExpression(bundleCode);
189
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs));
190
+ const evalPromise = connection.send("Runtime.evaluate", {
191
+ expression,
192
+ returnByValue: true,
193
+ awaitPromise: true
194
+ });
195
+ const cdpResult = await Promise.race([evalPromise, timeoutPromise]);
196
+ if (cdpResult.exceptionDetails) {
197
+ const msg = cdpResult.exceptionDetails.exception?.description ?? cdpResult.exceptionDetails.text ?? "Runtime.evaluate threw an exception";
198
+ throw new Error(`rpc.injectAndRunBundle: ${msg}`);
199
+ }
200
+ return parseRunTestsResult(cdpResult.result.value);
201
+ }
202
+ //#endregion
203
+ //#region src/test-runner/relay-worker.ts
204
+ /**
205
+ * Runs all `files` sequentially over the given CDP `connection`.
206
+ *
207
+ * For each file:
208
+ * 1. Bundle with esbuild (includes SDK shim + runtime).
209
+ * 2. Inject into the attached page via `Runtime.evaluate`.
210
+ * 3. Await the `RunReport` JSON response.
211
+ * 4. Accumulate results.
212
+ *
213
+ * Returns a `RelayRunReport` with per-file results and flattened totals.
214
+ *
215
+ * This function does NOT open or manage the relay connection — the caller
216
+ * is responsible for attaching and closing it.
217
+ *
218
+ * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
219
+ * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
220
+ *
221
+ * @param connection - Active CDP connection (relay or local kind).
222
+ * @param files - Absolute paths to test files, run in order.
223
+ * @param opts - Optional per-run overrides.
224
+ */
225
+ async function runTestFilesOverRelay(connection, files, opts) {
226
+ const wallStart = Date.now();
227
+ const startedAt = new Date(wallStart).toISOString();
228
+ const fileResults = [];
229
+ for (const file of files) {
230
+ let fileEntry;
231
+ try {
232
+ const { code } = await bundleTestFile(file, opts?.bundleOptions);
233
+ const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
234
+ if (rpcResult.ok) fileEntry = {
235
+ file,
236
+ result: rpcResult.report
237
+ };
238
+ else fileEntry = {
239
+ file,
240
+ result: { error: rpcResult.error }
241
+ };
242
+ } catch (e) {
243
+ fileEntry = {
244
+ file,
245
+ result: { error: e instanceof Error ? e.message : String(e) }
246
+ };
247
+ }
248
+ fileResults.push(fileEntry);
249
+ }
250
+ const totals = fileResults.reduce((acc, { result }) => {
251
+ if ("error" in result) {
252
+ acc.failed += 1;
253
+ acc.total += 1;
254
+ } else {
255
+ acc.passed += result.passed;
256
+ acc.failed += result.failed;
257
+ acc.skipped += result.skipped;
258
+ acc.total += result.passed + result.failed + result.skipped;
259
+ }
260
+ return acc;
261
+ }, {
262
+ passed: 0,
263
+ failed: 0,
264
+ skipped: 0,
265
+ total: 0
266
+ });
267
+ return {
268
+ startedAt,
269
+ duration: Date.now() - wallStart,
270
+ files: fileResults,
271
+ totals
272
+ };
273
+ }
274
+ //#endregion
275
+ //#region src/test-runner/cli.ts
276
+ /**
277
+ * `devtools-test` CLI — MVP skeleton.
278
+ *
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.
285
+ *
286
+ * NOTE: no shebang in this source file — the tsdown entry's `banner` option
287
+ * injects `#!/usr/bin/env node` into the compiled output (same pattern as
288
+ * `src/mcp/cli.ts`).
289
+ */
290
+ const USAGE = `
291
+ devtools-test — run mini-app tests on a real device WebView over the CDP relay
292
+
293
+ USAGE
294
+ devtools-test <glob> [<glob> ...] [options]
295
+
296
+ OPTIONS
297
+ --timeout <ms> Per-file evaluate timeout in ms (default: 30000)
298
+ --help, -h Show this help message
299
+
300
+ DESCRIPTION
301
+ Bundles each matched test file with esbuild (SDK imports redirected to
302
+ window.__sdk), injects the bundle into the attached WebView via
303
+ Runtime.evaluate, and returns a RunReport.
304
+
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.
311
+
312
+ EXAMPLE
313
+ devtools-test 'src/**/*.phone.test.ts' --timeout 60000
314
+
315
+ `.trimStart();
316
+ /**
317
+ * 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.
320
+ *
321
+ * TODO (#645): add real relay attach/detach lifecycle here (connect via
322
+ * Chii relay URL, call enableDomains, run, then close).
323
+ */
324
+ async function runWithConnection(connection, files, opts) {
325
+ const report = await runTestFilesOverRelay(connection, files, opts);
326
+ if (opts?.printSummary) {
327
+ const { totals } = report;
328
+ process.stdout.write(`\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\n`);
329
+ }
330
+ return report;
331
+ }
332
+ /**
333
+ * CLI entry point.
334
+ *
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.
337
+ */
338
+ async function main(argv = process.argv.slice(2)) {
339
+ let parsed;
340
+ try {
341
+ parsed = parseArgs({
342
+ args: argv,
343
+ options: {
344
+ help: {
345
+ type: "boolean",
346
+ short: "h"
347
+ },
348
+ timeout: { type: "string" }
349
+ },
350
+ allowPositionals: true
351
+ });
352
+ } catch (e) {
353
+ process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\n`);
354
+ process.exitCode = 1;
355
+ return;
356
+ }
357
+ if (parsed.values.help || argv.length === 0) {
358
+ process.stdout.write(USAGE);
359
+ return;
360
+ }
361
+ const files = await discoverTestFiles(parsed.positionals, process.cwd());
362
+ if (files.length === 0) {
363
+ process.stderr.write(`devtools-test: no test files matched ${parsed.positionals.join(", ")}\n`);
364
+ process.exitCode = 1;
365
+ return;
366
+ }
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`);
368
+ process.exitCode = 1;
369
+ }
370
+ if (import.meta.url === new URL(process.argv[1], "file://").href) main().catch((e) => {
371
+ process.stderr.write(`devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\n`);
372
+ process.exitCode = 1;
373
+ });
374
+ //#endregion
375
+ export { main, runWithConnection };
376
+
377
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,80 @@
1
+ import { i as createRelayPool, n as RelayConnectionFactory, t as RELAY_POOL_NAME } from "../pool-Dkp7I9Bf.js";
2
+
3
+ //#region src/test-runner/config.d.ts
4
+ /**
5
+ * Resolved phone-test configuration returned by `definePhoneTestConfig`.
6
+ */
7
+ interface PhoneTestConfig {
8
+ /**
9
+ * Glob patterns (relative to cwd) for test files to run on the device.
10
+ * Defaults to `['**\/*.phone.test.ts']`.
11
+ */
12
+ include: string[];
13
+ /**
14
+ * Per-file evaluate timeout in milliseconds. Defaults to 30 000.
15
+ */
16
+ timeoutMs: number;
17
+ /**
18
+ * Additional esbuild `external` patterns for bundling.
19
+ * The SDK package is always external; add more here if needed.
20
+ */
21
+ extraExternals: string[];
22
+ }
23
+ /** User-facing config shape accepted by `definePhoneTestConfig`. */
24
+ interface PhoneTestUserConfig {
25
+ include?: string[];
26
+ timeoutMs?: number;
27
+ extraExternals?: string[];
28
+ }
29
+ /**
30
+ * Define a phone-relay test configuration.
31
+ *
32
+ * Merges user overrides with sensible defaults and returns a resolved
33
+ * `PhoneTestConfig`. This object can be passed to `runTestFilesOverRelay`
34
+ * (via the CLI or custom scripts) to run tests on a real device WebView.
35
+ */
36
+ declare function definePhoneTestConfig(userConfig?: PhoneTestUserConfig): PhoneTestConfig;
37
+ /**
38
+ * The slice of a Vitest `test` config this helper produces — `pool`,
39
+ * `include`, and `testTimeout`. Typed structurally (not against Vitest's
40
+ * `InlineConfig`) so consumers can spread it into their own `defineConfig`
41
+ * without this module taking a value dependency on `vitest`.
42
+ */
43
+ interface PhoneVitestTestConfig {
44
+ /** The relay `PoolRunnerInitializer`, matched by `getFilePoolName`. */
45
+ pool: ReturnType<typeof createRelayPool>;
46
+ /** Glob patterns for device test files. */
47
+ include: string[];
48
+ /** Per-test timeout in ms (mirrors the relay per-file evaluate timeout). */
49
+ testTimeout: number;
50
+ }
51
+ /** User config accepted by {@link definePhoneVitestConfig}. */
52
+ interface PhoneVitestUserConfig extends PhoneTestUserConfig {
53
+ /**
54
+ * Opens/closes the CDP relay connection that tests run over. Required —
55
+ * without it there is no device to dispatch files to.
56
+ *
57
+ * SECRET-HANDLING: the factory owns the relay wss/TOTP; this config object
58
+ * never stores or logs those values.
59
+ */
60
+ connection: RelayConnectionFactory;
61
+ }
62
+ /**
63
+ * Build the Vitest `test` config slice for running tests on a device over the
64
+ * relay. Spread the result into your Vitest config:
65
+ *
66
+ * @example
67
+ * // vitest.config.ts
68
+ * import { defineConfig } from 'vitest/config';
69
+ * import { definePhoneVitestConfig } from '@ait-co/devtools/test-runner';
70
+ * export default defineConfig({
71
+ * test: definePhoneVitestConfig({ connection: myRelayFactory }),
72
+ * });
73
+ *
74
+ * Files matching `include` are dispatched to the relay pool (named
75
+ * {@link RELAY_POOL_NAME}); everything else runs in Vitest's default pool.
76
+ */
77
+ declare function definePhoneVitestConfig(userConfig: PhoneVitestUserConfig): PhoneVitestTestConfig;
78
+ //#endregion
79
+ export { PhoneTestConfig, PhoneTestUserConfig, PhoneVitestTestConfig, PhoneVitestUserConfig, RELAY_POOL_NAME, type RelayConnectionFactory, createRelayPool, definePhoneTestConfig, definePhoneVitestConfig };
80
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","names":[],"sources":["../../src/test-runner/config.ts"],"mappings":";;;;;AA6DA;UArCiB,eAAA;;;;;EAKf,OAAA;EAgCsF;;AAcxF;EA1CE,SAAA;;;;;EAKA,cAAA;AAAA;;UAIe,mBAAA;EACf,OAAA;EACA,SAAA;EACA,cAAA;AAAA;;;;;;;AAkEF;iBAlDgB,qBAAA,CAAsB,UAAA,GAAa,mBAAA,GAAsB,eAAA;;;;;;;UAcxD,qBAAA;;EAEf,IAAA,EAAM,UAAA,QAAkB,eAAA;;EAExB,OAAA;;EAEA,WAAA;AAAA;;UAIe,qBAAA,SAA8B,mBAAA;;;;;;;;EAQ7C,UAAA,EAAY,sBAAA;AAAA;;;;;;;;;;;;;;;;iBAkBE,uBAAA,CAAwB,UAAA,EAAY,qBAAA,GAAwB,qBAAA"}
@@ -0,0 +1,54 @@
1
+ import { RELAY_POOL_NAME, createRelayPool } from "./pool.js";
2
+ //#region src/test-runner/config.ts
3
+ const DEFAULT_CONFIG = {
4
+ include: ["**/*.phone.test.ts"],
5
+ timeoutMs: 3e4,
6
+ extraExternals: []
7
+ };
8
+ /**
9
+ * Define a phone-relay test configuration.
10
+ *
11
+ * Merges user overrides with sensible defaults and returns a resolved
12
+ * `PhoneTestConfig`. This object can be passed to `runTestFilesOverRelay`
13
+ * (via the CLI or custom scripts) to run tests on a real device WebView.
14
+ */
15
+ function definePhoneTestConfig(userConfig) {
16
+ return {
17
+ include: userConfig?.include ?? DEFAULT_CONFIG.include,
18
+ timeoutMs: userConfig?.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,
19
+ extraExternals: userConfig?.extraExternals ?? DEFAULT_CONFIG.extraExternals
20
+ };
21
+ }
22
+ /**
23
+ * Build the Vitest `test` config slice for running tests on a device over the
24
+ * relay. Spread the result into your Vitest config:
25
+ *
26
+ * @example
27
+ * // vitest.config.ts
28
+ * import { defineConfig } from 'vitest/config';
29
+ * import { definePhoneVitestConfig } from '@ait-co/devtools/test-runner';
30
+ * export default defineConfig({
31
+ * test: definePhoneVitestConfig({ connection: myRelayFactory }),
32
+ * });
33
+ *
34
+ * Files matching `include` are dispatched to the relay pool (named
35
+ * {@link RELAY_POOL_NAME}); everything else runs in Vitest's default pool.
36
+ */
37
+ function definePhoneVitestConfig(userConfig) {
38
+ const resolved = definePhoneTestConfig(userConfig);
39
+ return {
40
+ pool: createRelayPool({
41
+ connection: userConfig.connection,
42
+ run: {
43
+ timeoutMs: resolved.timeoutMs,
44
+ bundleOptions: { extraExternals: resolved.extraExternals }
45
+ }
46
+ }),
47
+ include: resolved.include,
48
+ testTimeout: resolved.timeoutMs
49
+ };
50
+ }
51
+ //#endregion
52
+ export { RELAY_POOL_NAME, createRelayPool, definePhoneTestConfig, definePhoneVitestConfig };
53
+
54
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":[],"sources":["../../src/test-runner/config.ts"],"sourcesContent":["/**\n * Configuration helper for phone-relay test runs.\n *\n * `definePhoneTestConfig` is the user-facing entry point for configuring the\n * relay test runner. It mirrors the pattern of Vitest's `defineConfig` so\n * users can write:\n *\n * // phone-test.config.ts\n * import { definePhoneTestConfig } from '@ait-co/devtools/test-runner';\n * export default definePhoneTestConfig({ ... });\n *\n * The helper resolves user config and, when given a relay connection factory,\n * builds the Vitest `pool` (`createRelayPool`) so Vitest's own config\n * resolution routes matching files to the device over the relay (#645). Without\n * a connection factory it still returns the resolved config for use with the\n * lower-level `runTestFilesOverRelay` transport (#644).\n */\n\nimport type { RelayConnectionFactory } from './pool.js';\nimport { createRelayPool, RELAY_POOL_NAME } from './pool.js';\n\n/**\n * Resolved phone-test configuration returned by `definePhoneTestConfig`.\n */\nexport interface PhoneTestConfig {\n /**\n * Glob patterns (relative to cwd) for test files to run on the device.\n * Defaults to `['**\\/*.phone.test.ts']`.\n */\n include: string[];\n /**\n * Per-file evaluate timeout in milliseconds. Defaults to 30 000.\n */\n timeoutMs: number;\n /**\n * Additional esbuild `external` patterns for bundling.\n * The SDK package is always external; add more here if needed.\n */\n extraExternals: string[];\n}\n\n/** User-facing config shape accepted by `definePhoneTestConfig`. */\nexport interface PhoneTestUserConfig {\n include?: string[];\n timeoutMs?: number;\n extraExternals?: string[];\n}\n\nconst DEFAULT_CONFIG: PhoneTestConfig = {\n include: ['**/*.phone.test.ts'],\n timeoutMs: 30_000,\n extraExternals: [],\n};\n\n/**\n * Define a phone-relay test configuration.\n *\n * Merges user overrides with sensible defaults and returns a resolved\n * `PhoneTestConfig`. This object can be passed to `runTestFilesOverRelay`\n * (via the CLI or custom scripts) to run tests on a real device WebView.\n */\nexport function definePhoneTestConfig(userConfig?: PhoneTestUserConfig): PhoneTestConfig {\n return {\n include: userConfig?.include ?? DEFAULT_CONFIG.include,\n timeoutMs: userConfig?.timeoutMs ?? DEFAULT_CONFIG.timeoutMs,\n extraExternals: userConfig?.extraExternals ?? DEFAULT_CONFIG.extraExternals,\n };\n}\n\n/**\n * The slice of a Vitest `test` config this helper produces — `pool`,\n * `include`, and `testTimeout`. Typed structurally (not against Vitest's\n * `InlineConfig`) so consumers can spread it into their own `defineConfig`\n * without this module taking a value dependency on `vitest`.\n */\nexport interface PhoneVitestTestConfig {\n /** The relay `PoolRunnerInitializer`, matched by `getFilePoolName`. */\n pool: ReturnType<typeof createRelayPool>;\n /** Glob patterns for device test files. */\n include: string[];\n /** Per-test timeout in ms (mirrors the relay per-file evaluate timeout). */\n testTimeout: number;\n}\n\n/** User config accepted by {@link definePhoneVitestConfig}. */\nexport interface PhoneVitestUserConfig extends PhoneTestUserConfig {\n /**\n * Opens/closes the CDP relay connection that tests run over. Required —\n * without it there is no device to dispatch files to.\n *\n * SECRET-HANDLING: the factory owns the relay wss/TOTP; this config object\n * never stores or logs those values.\n */\n connection: RelayConnectionFactory;\n}\n\n/**\n * Build the Vitest `test` config slice for running tests on a device over the\n * relay. Spread the result into your Vitest config:\n *\n * @example\n * // vitest.config.ts\n * import { defineConfig } from 'vitest/config';\n * import { definePhoneVitestConfig } from '@ait-co/devtools/test-runner';\n * export default defineConfig({\n * test: definePhoneVitestConfig({ connection: myRelayFactory }),\n * });\n *\n * Files matching `include` are dispatched to the relay pool (named\n * {@link RELAY_POOL_NAME}); everything else runs in Vitest's default pool.\n */\nexport function definePhoneVitestConfig(userConfig: PhoneVitestUserConfig): PhoneVitestTestConfig {\n const resolved = definePhoneTestConfig(userConfig);\n return {\n pool: createRelayPool({\n connection: userConfig.connection,\n run: {\n timeoutMs: resolved.timeoutMs,\n bundleOptions: { extraExternals: resolved.extraExternals },\n },\n }),\n include: resolved.include,\n testTimeout: resolved.timeoutMs,\n };\n}\n\nexport type { RelayConnectionFactory };\nexport { createRelayPool, RELAY_POOL_NAME };\n"],"mappings":";;AAgDA,MAAM,iBAAkC;CACtC,SAAS,CAAC,qBAAqB;CAC/B,WAAW;CACX,gBAAgB,EAAE;CACnB;;;;;;;;AASD,SAAgB,sBAAsB,YAAmD;AACvF,QAAO;EACL,SAAS,YAAY,WAAW,eAAe;EAC/C,WAAW,YAAY,aAAa,eAAe;EACnD,gBAAgB,YAAY,kBAAkB,eAAe;EAC9D;;;;;;;;;;;;;;;;;AA6CH,SAAgB,wBAAwB,YAA0D;CAChG,MAAM,WAAW,sBAAsB,WAAW;AAClD,QAAO;EACL,MAAM,gBAAgB;GACpB,YAAY,WAAW;GACvB,KAAK;IACH,WAAW,SAAS;IACpB,eAAe,EAAE,gBAAgB,SAAS,gBAAgB;IAC3D;GACF,CAAC;EACF,SAAS,SAAS;EAClB,aAAa,SAAS;EACvB"}
@@ -0,0 +1,2 @@
1
+ import { i as createRelayPool, n as RelayConnectionFactory, r as RelayPoolOptions, t as RELAY_POOL_NAME } from "../pool-Dkp7I9Bf.js";
2
+ export { RELAY_POOL_NAME, RelayConnectionFactory, RelayPoolOptions, createRelayPool };