@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
@@ -0,0 +1,96 @@
1
+ //#region src/test-runner/bundle.d.ts
2
+ /**
3
+ * esbuild-based bundler for user test files.
4
+ *
5
+ * Bundles a single test file into a self-contained IIFE string that can be
6
+ * injected into a WebView via `Runtime.evaluate`. The bundle includes the
7
+ * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and
8
+ * the `runTestModule(factory)` entry point.
9
+ *
10
+ * ## How the wiring works
11
+ *
12
+ * The bundle exposes two exports on `globalThis.__testBundle`:
13
+ * - `runTestModule` — the runtime's entry function.
14
+ * - `__userFactory` — an async function whose body is the user's top-level
15
+ * test registration code (describe/it/test calls).
16
+ *
17
+ * The Node-side RPC (`rpc.ts`) calls:
18
+ * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
19
+ *
20
+ * `runTestModule` then installs `describe/it/test/expect` as globals, invokes
21
+ * the factory (which registers all tests), runs them, and returns a `RunReport`.
22
+ *
23
+ * ## Why a factory wrapper is needed
24
+ *
25
+ * Naively adding the runtime to `entryPoints` and bundling the user file would
26
+ * fail for two reasons:
27
+ * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE
28
+ * scope. The user's top-level `describe(...)` calls expect them as globals —
29
+ * they are not globals until `runTestModule` installs them.
30
+ * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation
31
+ * time, before the RPC layer calls `runTestModule` to reset state and start
32
+ * the test clock.
33
+ *
34
+ * The factory approach solves both: the user's registration code is deferred
35
+ * into a function that `runTestModule` calls AFTER installing the globals.
36
+ *
37
+ * ## Factory extraction algorithm
38
+ *
39
+ * The `userFactoryPlugin` reads the user file and splits lines into:
40
+ * - **top-level**: `import …` and re-export lines — kept at module scope
41
+ * (the only valid position for static `import` in ESM).
42
+ * - **body**: all other statements — moved into the body of the exported
43
+ * `__userFactory` async function.
44
+ *
45
+ * esbuild processes the re-generated module, following each static import
46
+ * through the normal dependency graph (including the SDK-redirect plugin).
47
+ *
48
+ * ## SDK redirect
49
+ *
50
+ * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via
51
+ * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that
52
+ * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.
53
+ *
54
+ * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
55
+ */
56
+ /** Options accepted by `bundleTestFile`. */
57
+ interface BundleOptions {
58
+ /**
59
+ * Additional esbuild `external` patterns. The SDK package
60
+ * (`@apps-in-toss/web-framework` and `@apps-in-toss/web-framework/*`) is
61
+ * always handled by the SDK redirect plugin — callers may add more patterns
62
+ * to be left as globals.
63
+ */
64
+ extraExternals?: string[];
65
+ /**
66
+ * Global name for the IIFE output object. Defaults to `__testBundle`.
67
+ * The runtime entry uses this to call `__testBundle.runTestModule(__userFactory)`.
68
+ */
69
+ globalName?: string;
70
+ }
71
+ /**
72
+ * The result of bundling a test file.
73
+ * `code` is a self-contained IIFE string ready for `Runtime.evaluate`.
74
+ */
75
+ interface BundleResult {
76
+ code: string;
77
+ warnings: string[];
78
+ }
79
+ /**
80
+ * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
81
+ *
82
+ * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
83
+ * - `runTestModule` — the runtime entry (from `runtime.ts`).
84
+ * - `__userFactory` — an async function wrapping the user's test registration
85
+ * code so it runs AFTER `runTestModule` installs the globals.
86
+ *
87
+ * Callers (rpc.ts) invoke:
88
+ * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
89
+ *
90
+ * @param absPath - Absolute path to the user test file.
91
+ * @param opts - Optional bundling overrides.
92
+ */
93
+ declare function bundleTestFile(absPath: string, opts?: BundleOptions): Promise<BundleResult>;
94
+ //#endregion
95
+ export { BundleResult as n, bundleTestFile as r, BundleOptions as t };
96
+ //# sourceMappingURL=bundle-KFs4t-wc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-KFs4t-wc.d.ts","names":[],"sources":["../src/test-runner/bundle.ts"],"mappings":";;AAqEA;;;;;AAmBA;;;;;AA2LA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA9MiB,aAAA;;;;;;;EAOf,cAAA;;;;;EAKA,UAAA;AAAA;;;;;UAOe,YAAA;EACf,IAAA;EACA,QAAA;AAAA;;;;;;;;;;;;;;;iBAyLoB,cAAA,CAAe,OAAA,UAAiB,IAAA,GAAO,aAAA,GAAgB,OAAA,CAAQ,YAAA"}
package/dist/mcp/cli.js CHANGED
@@ -2,13 +2,14 @@
2
2
  import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-WY6l0ysP.js";
3
3
  import { t as loadRelaySecretReadOnly } from "../relay-secret-store-DhzAnnj-.js";
4
4
  import { createRequire } from "node:module";
5
- import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
5
+ import { accessSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { argv } from "node:process";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
10
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { parseArgs } from "node:util";
12
+ import * as fs from "node:fs/promises";
12
13
  import { glob } from "node:fs/promises";
13
14
  import * as path from "node:path";
14
15
  import { isAbsolute, join, resolve } from "node:path";
@@ -158,11 +159,53 @@ async function discoverTestFiles(patterns, cwd) {
158
159
  * esbuild-based bundler for user test files.
159
160
  *
160
161
  * Bundles a single test file into a self-contained IIFE string that can be
161
- * injected into a WebView via `Runtime.evaluate`. The user's SDK imports
162
- * (`@apps-in-toss/web-framework` and sub-paths) are intercepted via an
163
- * esbuild plugin that redirects them to `window.__sdk`, which the in-app
164
- * debug gate (`src/in-app/auto.ts`) installs as a namespace mirror of the
165
- * SDK exports (works for both 2.x and 3.x SDK).
162
+ * injected into a WebView via `Runtime.evaluate`. The bundle includes the
163
+ * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and
164
+ * the `runTestModule(factory)` entry point.
165
+ *
166
+ * ## How the wiring works
167
+ *
168
+ * The bundle exposes two exports on `globalThis.__testBundle`:
169
+ * - `runTestModule` — the runtime's entry function.
170
+ * - `__userFactory` — an async function whose body is the user's top-level
171
+ * test registration code (describe/it/test calls).
172
+ *
173
+ * The Node-side RPC (`rpc.ts`) calls:
174
+ * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
175
+ *
176
+ * `runTestModule` then installs `describe/it/test/expect` as globals, invokes
177
+ * the factory (which registers all tests), runs them, and returns a `RunReport`.
178
+ *
179
+ * ## Why a factory wrapper is needed
180
+ *
181
+ * Naively adding the runtime to `entryPoints` and bundling the user file would
182
+ * fail for two reasons:
183
+ * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE
184
+ * scope. The user's top-level `describe(...)` calls expect them as globals —
185
+ * they are not globals until `runTestModule` installs them.
186
+ * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation
187
+ * time, before the RPC layer calls `runTestModule` to reset state and start
188
+ * the test clock.
189
+ *
190
+ * The factory approach solves both: the user's registration code is deferred
191
+ * into a function that `runTestModule` calls AFTER installing the globals.
192
+ *
193
+ * ## Factory extraction algorithm
194
+ *
195
+ * The `userFactoryPlugin` reads the user file and splits lines into:
196
+ * - **top-level**: `import …` and re-export lines — kept at module scope
197
+ * (the only valid position for static `import` in ESM).
198
+ * - **body**: all other statements — moved into the body of the exported
199
+ * `__userFactory` async function.
200
+ *
201
+ * esbuild processes the re-generated module, following each static import
202
+ * through the normal dependency graph (including the SDK-redirect plugin).
203
+ *
204
+ * ## SDK redirect
205
+ *
206
+ * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via
207
+ * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that
208
+ * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.
166
209
  *
167
210
  * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
168
211
  */
@@ -212,10 +255,90 @@ module.exports = __proxy;
212
255
  };
213
256
  }
214
257
  /**
258
+ * esbuild plugin that transforms the user test file into a module that exports
259
+ * an async `__userFactory` function. The factory defers the user's top-level
260
+ * test registration code (describe/it/test calls) so it only runs when
261
+ * `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has
262
+ * installed describe/it/test/expect as globals.
263
+ *
264
+ * Algorithm:
265
+ * - Lines matching import declarations or re-export statements are kept at
266
+ * module top-level (the only valid ESM position for static `import`).
267
+ * - All other lines (describe/it/test calls, local declarations, etc.) are
268
+ * moved into the body of the exported async factory function.
269
+ *
270
+ * This preserves SDK import resolution (the sdk-redirect plugin processes
271
+ * top-level imports normally) while deferring test registration to the factory.
272
+ */
273
+ function userFactoryPlugin(absPath) {
274
+ const NAMESPACE = "user-test-factory";
275
+ return {
276
+ name: "user-test-factory",
277
+ setup(build) {
278
+ build.onResolve({ filter: /^user-test-factory$/ }, () => ({
279
+ path: absPath,
280
+ namespace: NAMESPACE
281
+ }));
282
+ build.onLoad({
283
+ filter: /.*/,
284
+ namespace: NAMESPACE
285
+ }, async (args) => {
286
+ const lines = (await fs.readFile(args.path, "utf8")).split("\n");
287
+ const topLevelLines = [];
288
+ const bodyLines = [];
289
+ const EXPORT_DECLARATION_RE = /^(export\s+)(default\s+|async\s+function\s+|function\s+|class\s+|const\s+|let\s+|var\s+)/;
290
+ for (const line of lines) {
291
+ const trimmed = line.trimStart();
292
+ const indent = line.slice(0, line.length - trimmed.length);
293
+ if (trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import'") || trimmed.startsWith("import\"")) topLevelLines.push(line);
294
+ else if (trimmed.startsWith("export ")) if (trimmed.match(EXPORT_DECLARATION_RE)) bodyLines.push(indent + trimmed.slice(7));
295
+ else topLevelLines.push(line);
296
+ else bodyLines.push(line);
297
+ }
298
+ return {
299
+ contents: [
300
+ ...topLevelLines,
301
+ "",
302
+ "// biome-ignore lint: generated factory wrapper",
303
+ "export default async function __userFactory(): Promise<void> {",
304
+ ...bodyLines.map((l) => ` ${l}`),
305
+ "}"
306
+ ].join("\n"),
307
+ loader: "ts",
308
+ resolveDir: path.dirname(absPath)
309
+ };
310
+ });
311
+ }
312
+ };
313
+ }
314
+ /**
315
+ * Returns the absolute path to the co-located runtime module.
316
+ *
317
+ * In the source tree (running via tsx / ts-node) the file is `runtime.ts`.
318
+ * After `tsdown` compiles to `dist/test-runner/`, it becomes `runtime.js`.
319
+ * We try both extensions to support both environments.
320
+ */
321
+ function getRuntimePath() {
322
+ const dir = path.dirname(fileURLToPath(import.meta.url));
323
+ for (const ext of [".ts", ".js"]) {
324
+ const candidate = path.join(dir, `runtime${ext}`);
325
+ try {
326
+ accessSync(candidate);
327
+ return candidate;
328
+ } catch {}
329
+ }
330
+ return path.join(dir, "runtime.js");
331
+ }
332
+ /**
215
333
  * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
216
334
  *
217
- * The IIFE installs `window.__testBundle` (or the custom `globalName`) with
218
- * `runTestModule` as the callable entry point.
335
+ * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
336
+ * - `runTestModule` the runtime entry (from `runtime.ts`).
337
+ * - `__userFactory` — an async function wrapping the user's test registration
338
+ * code so it runs AFTER `runTestModule` installs the globals.
339
+ *
340
+ * Callers (rpc.ts) invoke:
341
+ * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
219
342
  *
220
343
  * @param absPath - Absolute path to the user test file.
221
344
  * @param opts - Optional bundling overrides.
@@ -223,17 +346,29 @@ module.exports = __proxy;
223
346
  async function bundleTestFile(absPath, opts) {
224
347
  const globalName = opts?.globalName ?? "__testBundle";
225
348
  const extraExternals = opts?.extraExternals ?? [];
226
- const result = await (await import("esbuild")).build({
227
- entryPoints: [absPath],
349
+ const esbuild = await import("esbuild");
350
+ const runtimePath = getRuntimePath();
351
+ const wrapperContent = [
352
+ `import { runTestModule } from ${JSON.stringify(runtimePath)};`,
353
+ `import __userFactory from "user-test-factory";`,
354
+ `export { runTestModule, __userFactory };`
355
+ ].join("\n");
356
+ const result = await esbuild.build({
357
+ stdin: {
358
+ contents: wrapperContent,
359
+ loader: "ts",
360
+ resolveDir: path.dirname(absPath)
361
+ },
228
362
  bundle: true,
229
363
  format: "iife",
230
364
  globalName,
231
365
  platform: "browser",
232
366
  target: "es2022",
233
367
  write: false,
234
- plugins: [sdkRedirectPlugin()],
368
+ plugins: [userFactoryPlugin(absPath), sdkRedirectPlugin()],
235
369
  external: extraExternals,
236
- treeShaking: true
370
+ treeShaking: true,
371
+ footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
237
372
  });
238
373
  const warnings = result.warnings.map((w) => `${path.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
239
374
  const outputFile = result.outputFiles?.[0];
@@ -260,7 +395,7 @@ const DEFAULT_TIMEOUT_MS = 3e4;
260
395
  * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
261
396
  */
262
397
  function buildRunTestsExpression(bundleCode) {
263
- 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)}); }})()`;
398
+ 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)}); }})()`;
264
399
  }
265
400
  /**
266
401
  * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
@@ -393,14 +528,15 @@ async function runTestFilesOverRelay(connection, files, opts) {
393
528
  //#endregion
394
529
  //#region src/test-runner/cli.ts
395
530
  /**
396
- * `devtools-test` CLI — MVP skeleton.
397
- *
398
- * Parses argv, prints usage, and delegates to `runTestFilesOverRelay` when
399
- * a live CDP connection is provided. The relay connection wiring
400
- * (attach → run → detach) is tracked in issue #645 / #646.
531
+ * `devtools-test` CLI.
401
532
  *
402
- * MVP contract: `--help` works, `runWithConnection` is a testable pure
403
- * function, and the binary entry exists in package.json.
533
+ * Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)
534
+ * and exposes `runWithConnection` — the pure run core that bundles, injects, and
535
+ * collects each file over a CDP connection. Today the run path that has a live
536
+ * connection is the `run_tests` MCP tool (it runs these files against the
537
+ * daemon's attached page); the CLI's own standalone relay attach (resolve CDP
538
+ * URL → attach → run → close) is not wired yet, so `main()` resolves the matched
539
+ * files and points the operator at the MCP tool.
404
540
  *
405
541
  * NOTE: no shebang in this source file — the tsdown entry's `banner` option
406
542
  * injects `#!/usr/bin/env node` into the compiled output (same pattern as
@@ -421,12 +557,10 @@ DESCRIPTION
421
557
  window.__sdk), injects the bundle into the attached WebView via
422
558
  Runtime.evaluate, and returns a RunReport.
423
559
 
424
- A live CDP relay connection must be active before running tests.
425
- Use \`/ait debug\` (devtools-mcp) to attach and then call this CLI from
426
- the same process context.
427
-
428
- Full Vitest pool integration and the \`run_tests\` MCP tool are tracked in
429
- issues #645 and #646 respectively. This MVP provides the transport layer.
560
+ A live CDP relay connection must be active before running tests. Use the
561
+ \`run_tests\` MCP tool (via \`devtools-mcp\` / \`/ait debug\`) to run these files
562
+ against an attached page — the CLI's own standalone relay attach is not wired
563
+ yet (it currently resolves the matched files and defers to that tool).
430
564
 
431
565
  EXAMPLE
432
566
  devtools-test 'src/**/*.phone.test.ts' --timeout 60000
@@ -434,11 +568,12 @@ EXAMPLE
434
568
  `.trimStart();
435
569
  /**
436
570
  * Runs `files` over `connection` and returns the aggregate report.
437
- * This pure function is the testable core of the CLI; it is separate from
438
- * `main()` so tests can call it without spawning a subprocess.
571
+ * This pure function is the testable core of the CLI (and is what the
572
+ * `run_tests` MCP tool calls against the daemon's attached connection); it is
573
+ * separate from `main()` so tests can call it without spawning a subprocess.
439
574
  *
440
- * TODO (#645): add real relay attach/detach lifecycle here (connect via
441
- * Chii relay URL, call enableDomains, run, then close).
575
+ * A standalone CLI relay attach/detach lifecycle (connect via Chii relay URL,
576
+ * `enableDomains`, run, then close) is not wired into `main()` yet.
442
577
  */
443
578
  async function runWithConnection(connection, files, opts) {
444
579
  const report = await runTestFilesOverRelay(connection, files, opts);
@@ -451,8 +586,10 @@ async function runWithConnection(connection, files, opts) {
451
586
  /**
452
587
  * CLI entry point.
453
588
  *
454
- * MVP: prints usage and a "relay attach required" notice. Real relay wiring
455
- * (resolve CDP URL, attach, run, close) is tracked in issues #645 / #646.
589
+ * Resolves the matched test files and prints a "relay attach required" notice:
590
+ * the CLI's own standalone relay attach (resolve CDP URL, attach, run, close) is
591
+ * not wired yet, so today these files run via the `run_tests` MCP tool against
592
+ * the daemon's attached page.
456
593
  */
457
594
  async function main$1(argv = process.argv.slice(2)) {
458
595
  let parsed;
@@ -483,7 +620,7 @@ async function main$1(argv = process.argv.slice(2)) {
483
620
  process.exitCode = 1;
484
621
  return;
485
622
  }
486
- 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`);
623
+ 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`);
487
624
  process.exitCode = 1;
488
625
  }
489
626
  if (import.meta.url === new URL(process.argv[1], "file://").href) main$1().catch((e) => {
@@ -4412,7 +4549,7 @@ const DEBUG_TOOL_DEFINITIONS = [
4412
4549
  },
4413
4550
  {
4414
4551
  name: "run_tests",
4415
- description: "Runs mini-app test files on the attached page over CDP (Runtime.evaluate). Each matched file is bundled with esbuild (SDK imports redirected to the live mock/SDK), injected into the attached WebView, and executed; returns per-file results plus flattened totals (passed/failed/skipped/total). Requires an attached page — call list_pages first to confirm one is attached. Files run SEQUENTIALLY (single-attach model: the relay/local target serves one page), and one run_tests call runs at a time (a concurrent call is rejected). Test verification (assert/snapshot) is delegated to the in-page Vitest runtime; this tool is the transport + report. The per-file results array is the progress record — on partial failure you see exactly which files passed/failed/timed-out. In a relay-live session this is a state-mutating injection and is blocked unless confirm=true (ignored in mock/local/relay-dev/relay-mobile). debug-mode only — dev-mode (--mode=dev) has no CDP. Tier C (both mock/local and relay). Use the test-runner CLI (devtools-test) for the same run outside MCP.",
4552
+ description: "Runs mini-app test files on the attached page over CDP (Runtime.evaluate). Each matched file is bundled with esbuild (SDK imports redirected to the live mock/SDK), injected into the attached WebView, and executed; returns per-file results plus flattened totals (passed/failed/skipped/total). Requires an attached page — call list_pages first to confirm one is attached. Files run SEQUENTIALLY (single-attach model: the relay/local target serves one page), and one run_tests call runs at a time (a concurrent call is rejected). Test verification (assert/snapshot) is delegated to the in-page Vitest runtime; this tool is the transport + report. The per-file results array is the progress record — on partial failure you see exactly which files passed/failed/timed-out. In a relay-live session this is a state-mutating injection and is blocked unless confirm=true (confirm is ignored in every non-live session: mock/local, relay-dev, relay-mobile). debug-mode only — dev-mode (--mode=dev) has no CDP. Tier C (both mock/local and relay). The devtools-test CLI shares this run core and file discovery, but its standalone relay attach is not wired yet — run via this tool for now.",
4416
4553
  inputSchema: {
4417
4554
  type: "object",
4418
4555
  properties: {
@@ -4431,7 +4568,7 @@ const DEBUG_TOOL_DEFINITIONS = [
4431
4568
  },
4432
4569
  confirm: {
4433
4570
  type: "boolean",
4434
- description: "Required (true) to run in a relay-live session — test injection mutates page state. Ignored in mock/local/relay-dev/relay-mobile sessions."
4571
+ description: "Required (true) to run in a relay-live session — test injection mutates page state. Ignored in every non-live session (mock/local, relay-dev, relay-mobile)."
4435
4572
  }
4436
4573
  },
4437
4574
  required: ["files"]
@@ -5259,7 +5396,7 @@ async function readMcpSdkVersion() {
5259
5396
  * some test environments that skip the build step).
5260
5397
  */
5261
5398
  function readDevtoolsVersion() {
5262
- return "0.1.108";
5399
+ return "0.1.109";
5263
5400
  }
5264
5401
  /**
5265
5402
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -5507,9 +5644,11 @@ async function renderQr(text) {
5507
5644
  return `${lines.join("\n")}\n`;
5508
5645
  }
5509
5646
  /**
5510
- * Renders the attach banner (relay URL + ASCII QR) as a string.
5647
+ * Renders the attach banner (relay URL + unicode half-block QR) as a string.
5511
5648
  *
5512
- * The QR encodes the base `wssUrl` only. When `totpEnabled` is true, a note
5649
+ * The QR is produced by `renderQr` (a half-block matrix, not the
5650
+ * `qrcode-terminal` ASCII art used by the unplugin banner) and encodes the
5651
+ * base `wssUrl` only. When `totpEnabled` is true, a note
5513
5652
  * is added that attach URLs generated by `build_attach_url` will include a
5514
5653
  * live TOTP code (`at=`) appended at call time.
5515
5654
  *
@@ -5809,6 +5948,12 @@ let runTestsInFlight = false;
5809
5948
  * to resolve before the relay had observed the first inbound CDP message from
5810
5949
  * the phone.
5811
5950
  *
5951
+ * Timeout note: callers (e.g. the `build_attach_url` path) always pass an
5952
+ * explicit `timeoutMs`, sourced from the factory's `waitForAttachTimeoutMs`
5953
+ * (default 60 000). That value is forwarded to `waitForFirstTarget`, so it
5954
+ * overrides that method's own 90 000 signature default — the effective
5955
+ * wait on the tool path is 60 s, not 90 s.
5956
+ *
5812
5957
  * @param connection - The CDP connection (production or fake).
5813
5958
  * @param filterFn - Resolves when this predicate is satisfied.
5814
5959
  * @param timeoutMs - Maximum wait time in ms.
@@ -5861,7 +6006,7 @@ function createDebugServer(deps) {
5861
6006
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5862
6007
  const server = new Server({
5863
6008
  name: "ait-debug",
5864
- version: "0.1.108"
6009
+ version: "0.1.109"
5865
6010
  }, { capabilities: { tools: { listChanged: true } } });
5866
6011
  server.setRequestHandler(ListToolsRequestSchema, () => {
5867
6012
  const conn = router.active;
@@ -6495,6 +6640,7 @@ function toRunTestsResult(report) {
6495
6640
  error: f.result.error
6496
6641
  } : {
6497
6642
  file: f.file,
6643
+ duration: f.result.duration,
6498
6644
  passed: f.result.passed,
6499
6645
  failed: f.result.failed,
6500
6646
  skipped: f.result.skipped,
@@ -8081,7 +8227,7 @@ function createDevServer(deps = {}) {
8081
8227
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
8082
8228
  const server = new Server({
8083
8229
  name: "ait-devtools",
8084
- version: "0.1.108"
8230
+ version: "0.1.109"
8085
8231
  }, { capabilities: { tools: {} } });
8086
8232
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
8087
8233
  server.setRequestHandler(CallToolRequestSchema, async (request) => {