@ait-co/devtools 0.1.128 → 0.1.129

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 (66) hide show
  1. package/dist/{attach-orchestrator-CpE0dMew.js → attach-orchestrator-Bdt5Zdo9.js} +3 -94
  2. package/dist/attach-orchestrator-Bdt5Zdo9.js.map +1 -0
  3. package/dist/{attach-orchestrator-CPMQWkGV.js → attach-orchestrator-DvrgUIx7.js} +4 -95
  4. package/dist/attach-orchestrator-DvrgUIx7.js.map +1 -0
  5. package/dist/{bundle-Cb3GFuw6.d.ts → bundle-BKqyhEK9.d.ts} +1 -1
  6. package/dist/{bundle-Cb3GFuw6.d.ts.map → bundle-BKqyhEK9.d.ts.map} +1 -1
  7. package/dist/{capture-B1zfGyTo.d.ts → capture-ltuV0gZa.d.ts} +1 -1
  8. package/dist/{capture-B1zfGyTo.d.ts.map → capture-ltuV0gZa.d.ts.map} +1 -1
  9. package/dist/{cdp-connection-Dxw_EY8s.d.ts → cdp-connection-D7AYQck2.d.ts} +1 -1
  10. package/dist/{cdp-connection-Dxw_EY8s.d.ts.map → cdp-connection-D7AYQck2.d.ts.map} +1 -1
  11. package/dist/{cell-D0dJSGHi.js → cell-DA_lcpDI.js} +2 -2
  12. package/dist/{cell-D0dJSGHi.js.map → cell-DA_lcpDI.js.map} +1 -1
  13. package/dist/{cell-CX92lDI0.js → cell-DLRv4iG6.js} +2 -2
  14. package/dist/{cell-CX92lDI0.js.map → cell-DLRv4iG6.js.map} +1 -1
  15. package/dist/{debug-server-Dctc9GAh.js → debug-server--Y0QZVJS.js} +57 -58
  16. package/dist/debug-server--Y0QZVJS.js.map +1 -0
  17. package/dist/debug-server-Bu3pCG9c.js +436 -0
  18. package/dist/debug-server-Bu3pCG9c.js.map +1 -0
  19. package/dist/debug-server-e1qPe0ZB.js +379 -0
  20. package/dist/{debug-server-BzzNkETg.js.map → debug-server-e1qPe0ZB.js.map} +1 -1
  21. package/dist/log-CUik_tHp.js +95 -0
  22. package/dist/log-CUik_tHp.js.map +1 -0
  23. package/dist/mcp/cli.js +1152 -1099
  24. package/dist/mcp/cli.js.map +1 -1
  25. package/dist/mcp/server.js +1 -1
  26. package/dist/panel/index.js +1 -1
  27. package/dist/{pool-DBLKNeRC.d.ts → pool-C0pc7T2p.d.ts} +4 -4
  28. package/dist/{pool-DBLKNeRC.d.ts.map → pool-C0pc7T2p.d.ts.map} +1 -1
  29. package/dist/{qr-http-server-Bo8LMOJQ.js → qr-http-server-HANhOFwI.js} +1 -1
  30. package/dist/{qr-http-server-Bo8LMOJQ.js.map → qr-http-server-HANhOFwI.js.map} +1 -1
  31. package/dist/{relay-factory-Dug7W6Ao.js → relay-factory-Ck3vBonr.js} +6 -6
  32. package/dist/{relay-factory-Dug7W6Ao.js.map → relay-factory-Ck3vBonr.js.map} +1 -1
  33. package/dist/{relay-secret-store-DKxs7zwq.js → relay-secret-store-DzxDsK6S.js} +1 -1
  34. package/dist/{relay-secret-store-DKxs7zwq.js.map → relay-secret-store-DzxDsK6S.js.map} +1 -1
  35. package/dist/{relay-url-store-xmUuTjXA.js → relay-url-store-D3rY-GJJ.js} +2 -2
  36. package/dist/{relay-url-store-xmUuTjXA.js.map → relay-url-store-D3rY-GJJ.js.map} +1 -1
  37. package/dist/{debug-server-BzzNkETg.js → relay-worker-Bt0QYKQH.js} +212 -346
  38. package/dist/relay-worker-Bt0QYKQH.js.map +1 -0
  39. package/dist/{relay-worker-TReZtzGl.d.ts → relay-worker-o0M2ndyJ.d.ts} +7 -7
  40. package/dist/relay-worker-o0M2ndyJ.d.ts.map +1 -0
  41. package/dist/{runtime-ozyPnRbU.d.ts → runtime-9xhN9pr8.d.ts} +1 -1
  42. package/dist/{runtime-ozyPnRbU.d.ts.map → runtime-9xhN9pr8.d.ts.map} +1 -1
  43. package/dist/test-runner/bin.js +706 -13
  44. package/dist/test-runner/bin.js.map +1 -1
  45. package/dist/test-runner/bundle.d.ts +1 -1
  46. package/dist/test-runner/capture.d.ts +1 -1
  47. package/dist/test-runner/config.d.ts +1 -1
  48. package/dist/test-runner/config.js +1 -1
  49. package/dist/test-runner/pool.d.ts +1 -1
  50. package/dist/test-runner/pool.js +1 -1
  51. package/dist/test-runner/relay-factory.d.ts +1 -1
  52. package/dist/test-runner/relay-factory.js +1 -1
  53. package/dist/test-runner/relay-worker.d.ts +1 -1
  54. package/dist/test-runner/relay-worker.js +1 -187
  55. package/dist/test-runner/report.d.ts +3 -3
  56. package/dist/test-runner/rpc.d.ts +2 -2
  57. package/dist/test-runner/runtime.d.ts +1 -1
  58. package/dist/test-runner/task-graph.d.ts +1 -1
  59. package/package.json +1 -1
  60. package/dist/attach-orchestrator-CPMQWkGV.js.map +0 -1
  61. package/dist/attach-orchestrator-CpE0dMew.js.map +0 -1
  62. package/dist/debug-server-1dhDA3aw.js +0 -980
  63. package/dist/debug-server-1dhDA3aw.js.map +0 -1
  64. package/dist/debug-server-Dctc9GAh.js.map +0 -1
  65. package/dist/relay-worker-TReZtzGl.d.ts.map +0 -1
  66. package/dist/test-runner/relay-worker.js.map +0 -1
@@ -1,17 +1,10 @@
1
- #!/usr/bin/env node
2
- import { n as buildRelayVerifyAuth, r as generateTotp, t as assertRelayAuthConfigured } from "./totp-DvOYkYim.js";
3
- import "./test-runner/bin.js";
4
- import { a as startTunnelHealthProbe, i as startQuickTunnel, n as makeTunnelStatus, o as logError, r as printAttachBanner, s as logInfo, t as generateAttachToken } from "./attach-orchestrator-CpE0dMew.js";
5
- import "./cell-CX92lDI0.js";
6
- import "./qr-http-server-s1iW_5VQ.js";
7
- import "./relay-secret-store-CFc9n0OA.js";
8
- import { createRequire } from "node:module";
9
- import "@modelcontextprotocol/sdk/server/index.js";
10
- import "@modelcontextprotocol/sdk/server/stdio.js";
11
- import "@modelcontextprotocol/sdk/types.js";
1
+ import { r as generateTotp } from "./totp-DfekTBk3.js";
2
+ import { n as logInfo } from "./log-CUik_tHp.js";
3
+ import { bundleTestFile } from "./test-runner/bundle.js";
4
+ import { parseCaptureLines } from "./test-runner/capture.js";
5
+ import { injectAndRunBundle } from "./test-runner/rpc.js";
12
6
  import { EventEmitter } from "node:events";
13
- import { WebSocket, WebSocketServer } from "ws";
14
- import { createServer } from "node:http";
7
+ import { WebSocket } from "ws";
15
8
  //#region src/shared/relay-auth-close.ts
16
9
  /**
17
10
  * Shared constants for the relay's named TOTP-auth rejection (issue #478).
@@ -75,6 +68,25 @@ const RELAY_AUTH_REJECT_REASON = "totp-rejected";
75
68
  */
76
69
  /** Max events retained per domain ring buffer. */
77
70
  const DEFAULT_BUFFER_SIZE = 500;
71
+ /**
72
+ * Substrings that mark a "relay websocket is dead" class error, as produced by
73
+ * `handleDisconnect('relay WebSocket 연결이 끊겼습니다')` (ws close handler) and
74
+ * the fail-fast `sendCommand` rejection (`relay에 연결되어 있지 않습니다 (...)`).
75
+ *
76
+ * Exported so callers outside this module (e.g. `test-runner/relay-worker.ts`)
77
+ * can detect the same error class without hardcoding a fragile full-sentence
78
+ * match — mirrors the `EVALUATE_TIMEOUT_MARKER` precedent in
79
+ * `test-runner/relay-worker.ts`. Kept in sync with `classifyToolError`'s
80
+ * `relayDisconnectError` branch in `mcp/errors.ts`.
81
+ */
82
+ const RELAY_DISCONNECT_MARKERS = ["relay WebSocket", "relay에 연결되어 있지 않습니다"];
83
+ /**
84
+ * Returns true when `message` matches the relay-websocket-dead error class
85
+ * (socket closed, socket errored, or fail-fast "not connected" rejection).
86
+ */
87
+ function isRelayDisconnectMessage(message) {
88
+ return RELAY_DISCONNECT_MARKERS.some((marker) => message.includes(marker));
89
+ }
78
90
  function isObject(value) {
79
91
  return typeof value === "object" && value !== null;
80
92
  }
@@ -557,368 +569,222 @@ var ChiiCdpConnection = class {
557
569
  }
558
570
  };
559
571
  //#endregion
560
- //#region src/mcp/chii-relay.ts
572
+ //#region src/test-runner/relay-worker.ts
561
573
  /**
562
- * Boots the local Chii relay server.
563
- *
564
- * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome
565
- * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.
566
- * The relay accepts a `target` websocket from the phone's injected `target.js`
567
- * and `client` websockets from CDP frontends (our MCP connection).
568
- *
569
- * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app
570
- * entries.
571
- *
572
- * TOTP auth (relay-side, authoritative gate):
573
- * When `verifyAuth` is provided, this module gates both inbound surfaces:
574
- *
575
- * - HTTP 'request': a listener registered BEFORE `chii.start({server})`.
576
- * Node's `http.Server` calls listeners in registration order; the first
577
- * to call `res.end()` wins. Invalid auth → 401 + CORS header + a tiny
578
- * JSON body (`{"error":"totp-rejected"}`) so a cross-origin script
579
- * `fetch()` probe can READ the status (issue #478). Valid auth → return
580
- * without side-effect (chii's Koa handler serves it).
581
- *
582
- * - WS 'upgrade': after `chii.start()` has registered chii's own upgrade
583
- * listener, we take over the upgrade chain (remove chii's listeners,
584
- * re-dispatch manually). Invalid auth → accept-then-close: complete the
585
- * handshake via a `noServer` WebSocketServer, then immediately close
586
- * with code 4401 reason 'totp-rejected' (issue #478). A raw 401 +
587
- * `socket.destroy()` only ever surfaced as close code 1006 in the
588
- * browser — indistinguishable from a tunnel failure, which left the
589
- * env-2 phone UI silent. The explicit dispatch (not listener ordering)
590
- * is what keeps chii away from rejected sockets: accept-then-close
591
- * leaves the socket alive, so an order-based early-return would let
592
- * chii's later listener complete a SECOND handshake on the same socket
593
- * — an auth bypass. Valid auth → forward to chii's captured listeners.
574
+ * Sentinel string embedded in the error message by `injectAndRunBundle` when
575
+ * the per-file evaluate race hits the timeout. Used by the retry guard so only
576
+ * genuine timeouts get a second attempt not bundle errors or parse failures.
594
577
  *
595
- * TOTP code transports (issue #466) two equivalent ways to carry the code:
596
- * 1. Query param `at=<code>` — used by the daemon-side `/client` connection
597
- * (`chii-connection.ts` appends it; it holds the secret).
598
- * 2. Path prefix `/at/<code>/…` — used by the phone-side target. Chii's
599
- * stock `target.js` derives its WS endpoint from the script `src`
600
- * (`scriptEl.src.replace('target.js','')`), so the only way for the
601
- * phone to carry a code is to embed it in the script URL path. The
602
- * in-app attach injects `https://<host>/at/<code>/target.js`; both the
603
- * script fetch and the derived `wss://<host>/at/<code>/target/<id>` WS
604
- * dial then carry the prefix. The listeners below rewrite the prefix
605
- * into the query form (`rewriteAtPathPrefix`) and MUTATE `req.url`
606
- * before chii's own handlers (registered later) parse it — chii only
607
- * ever sees the stripped URL.
608
- *
609
- * Threat model: "URL leak" — someone obtains the tunnel URL (Slack paste, QR
610
- * screenshot, shoulder-surfing) but does not have the shared TOTP secret.
611
- * Rotating 6-digit code makes the URL stale after 30 s.
612
- * A determined attacker who extracts the secret from the dogfood bundle can
613
- * still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).
614
- *
615
- * SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear
616
- * in any log, error message, or process output. `verifyAuth` is a black-box
617
- * predicate from the caller's perspective; this module only forwards pass/fail.
578
+ * Exported for unit tests that assert the retry path is taken.
618
579
  */
619
- const require = createRequire(import.meta.url);
580
+ const EVALUATE_TIMEOUT_MARKER = "rpc: evaluate timed out after";
620
581
  /**
621
- * WS keepalive ping interval (ms).
582
+ * Runs all `files` sequentially over the given CDP `connection`.
622
583
  *
623
- * Cloudflare proxied connections are dropped after ~100 s of no traffic.
624
- * 45 s comfortably fits inside that window and lets both the phone-target leg
625
- * and the daemon-client leg survive idle CDP sessions.
626
- */
627
- const DEFAULT_KEEPALIVE_INTERVAL_MS = 45e3;
628
- /**
629
- * Loads chii's internal WebSocketServer class and returns it together with a
630
- * flag indicating whether the real class was found.
584
+ * For each file:
585
+ * 1. Bundle with esbuild (includes SDK shim + runtime).
586
+ * 2. Inject into the attached page via `Runtime.evaluate`.
587
+ * 3. Await the `RunReport` JSON response.
588
+ * 4. Accumulate results.
631
589
  *
632
- * Returns `null` if the internal path is not resolvable (future chii release
633
- * changes the layout) — callers skip keepalive gracefully.
634
- */
635
- function tryLoadChiiWssClass() {
636
- try {
637
- const mod = require("chii/server/lib/WebSocketServer");
638
- if (typeof mod === "function") return mod;
639
- } catch {}
640
- return null;
641
- }
642
- /**
643
- * Calls `chii.start()` and returns the chii `WebSocketServer` instance that
644
- * was constructed during the call.
590
+ * Returns a `RelayRunReport` with per-file results and flattened totals.
645
591
  *
646
- * How: `chii/server/index.js`'s `start()` creates `new WebSocketServer()`
647
- * where `WebSocketServer` is captured from `require('./lib/WebSocketServer')`
648
- * at module load time. The class reference is stable, so we can temporarily
649
- * patch `ChiiWssClass.prototype.start` — which runs *on the instance* —
650
- * to record `this` before the original `start` runs.
592
+ * This function does NOT open or manage the relay connection — the caller
593
+ * is responsible for attaching and closing it.
651
594
  *
652
- * The patch is installed before `chii.start()` and removed (via `finally`)
653
- * immediately after, so concurrent `startChiiRelay` calls nest correctly: each
654
- * call's patch overrides the previous in the prototype chain for the duration
655
- * of its own `chii.start()` call, restoring the prior descriptor on exit.
595
+ * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
596
+ * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
656
597
  *
657
- * If `ChiiWssClass` is null (internal path changed in a future chii release),
658
- * `chii.start()` runs unpatched and the function returns null callers skip
659
- * keepalive gracefully without affecting relay correctness.
598
+ * @param connection - Active CDP connection (relay or local kind).
599
+ * @param files - Absolute paths to test files, run in order.
600
+ * @param opts - Optional per-run overrides.
660
601
  */
661
- async function startChiiWithCapture(chii, startOptions, ChiiWssClass) {
662
- if (ChiiWssClass === null) {
663
- await chii.start(startOptions);
664
- return null;
665
- }
666
- let captured = null;
667
- const proto = ChiiWssClass.prototype;
668
- const originalStart = proto.start;
669
- proto.start = function(server) {
670
- captured = this;
671
- return originalStart.call(this, server);
672
- };
602
+ async function runTestFilesOverRelay(connection, files, opts) {
603
+ const wallStart = Date.now();
604
+ const startedAt = new Date(wallStart).toISOString();
605
+ const fileResults = [];
606
+ let domainsEnabled = false;
673
607
  try {
674
- await chii.start(startOptions);
675
- } finally {
676
- proto.start = originalStart;
608
+ await connection.enableDomains();
609
+ domainsEnabled = true;
610
+ } catch (e) {
611
+ process.stderr.write(`relay-worker: enableDomains() failed before run — console capture may be empty (${e instanceof Error ? e.message : String(e)})\n`);
677
612
  }
678
- return captured;
679
- }
680
- function loadChiiServer() {
681
- const mod = require("chii");
682
- if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
683
- throw new Error("chii server module did not expose start()");
684
- }
685
- /**
686
- * Rewrites a `/at/<code>/…` path-prefixed request URL into the equivalent
687
- * query-based form, e.g.:
688
- *
689
- * `/at/123456/target.js` → `/target.js?at=123456`
690
- * `/at/123456/target/x?url=u` → `/target/x?url=u&at=123456`
691
- * `/at/123456/` → `/?at=123456`
692
- *
693
- * Returns `null` when the URL does not carry the prefix (including an empty
694
- * code segment) callers fall back to the unmodified URL and the existing
695
- * query-based auth path.
696
- *
697
- * Pure string surgerythis function knows nothing about secrets or code
698
- * validity; verification stays inside the caller-provided `verifyAuth`
699
- * predicate (which parses the query). The raw path segment is appended
700
- * verbatim to the query: both path segments and query values are
701
- * percent-decoded exactly once by their consumers, so no re-encoding is
702
- * needed (TOTP codes are 6 digits and never percent-encoded in practice).
703
- */
704
- function rewriteAtPathPrefix(rawUrl) {
705
- const match = /^\/at\/([^/?]+)(\/[^?]*)?(\?.*)?$/.exec(rawUrl);
706
- if (match === null) return null;
707
- const code = match[1];
708
- const path = match[2] === void 0 || match[2] === "" ? "/" : match[2];
709
- const query = match[3] ?? "";
710
- return `${path}${query}${query === "" ? "?" : "&"}at=${code}`;
711
- }
712
- /**
713
- * Starts the Chii relay and resolves once listening.
714
- *
715
- * Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral
716
- * port on every start, so a stale cloudflared orphan holding any particular
717
- * port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`
718
- * always reflect the actual bound port.
719
- *
720
- * chii.start() is called with `server` (our pre-created httpServer) BEFORE
721
- * httpServer.listen(). This is intentional: chii attaches its Koa handler and
722
- * WS upgrade listener to the server object, but the actual TCP bind is
723
- * performed by our httpServer.listen() call below. The `port`/`domain` values
724
- * passed to chii.start() are used for display/banner purposes inside chii and
725
- * do not affect which port the server binds. The connection path (clients
726
- * connecting to `relay.baseUrl`) always uses the post-listen confirmed port.
727
- */
728
- async function startChiiRelay(options = {}) {
729
- const requestedPort = options.port ?? 0;
730
- const host = options.host ?? "127.0.0.1";
731
- const { verifyAuth, onAuthReject } = options;
732
- const keepaliveIntervalMs = options.keepaliveIntervalMs !== void 0 ? options.keepaliveIntervalMs : DEFAULT_KEEPALIVE_INTERVAL_MS;
733
- const httpServer = createServer();
734
- const notifyAuthReject = (kind) => {
735
- if (onAuthReject === void 0) return;
613
+ const collectCaptures = opts?.collectCaptures === true;
614
+ const liveConsole = [];
615
+ let unsubscribeConsole;
616
+ if (collectCaptures && domainsEnabled) unsubscribeConsole = connection.on("Runtime.consoleAPICalled", (event) => {
617
+ liveConsole.push(event);
618
+ });
619
+ let pendingReconnectCheck = false;
620
+ /**
621
+ * Attempts one `enableDomains()` reconnect. Logs the attempt and outcome to
622
+ * stderr. Never throws — a failed reconnect just means the caller proceeds
623
+ * as today (each remaining file / the retry will fail-fast on the still-dead
624
+ * socket).
625
+ *
626
+ * `context` selects the fixed log literal: 'next-file' for the between-files
627
+ * case (a file's FINAL result was a WS-dead-class error — the primary #731
628
+ * scenario), 'retry-precheck' for the defensive reconnect before retrying a
629
+ * timed-out file (cheap no-op via `enableDomains()`'s idempotency when the
630
+ * socket never actually died).
631
+ *
632
+ * SECRET-HANDLING: fixed literals only no relay wss URL, TOTP code, or
633
+ * tunnel host.
634
+ */
635
+ const attemptReconnect = async (context) => {
636
+ process.stderr.write(context === "next-file" ? "relay-worker: relay connection lost attempting reconnect before next file\n" : "relay-worker: evaluate timed out — attempting reconnect before retry\n");
736
637
  try {
737
- onAuthReject({ kind });
738
- } catch {}
638
+ await connection.enableDomains();
639
+ process.stderr.write("relay-worker: reconnect succeeded\n");
640
+ } catch (e) {
641
+ process.stderr.write(`relay-worker: reconnect failed (${e instanceof Error ? e.message : String(e)}) — continuing, remaining files may fail\n`);
642
+ }
739
643
  };
740
- if (verifyAuth) httpServer.on("request", (req, res) => {
741
- const rewritten = rewriteAtPathPrefix(req.url ?? "");
742
- if (rewritten !== null) {
743
- req.url = rewritten;
744
- if (!verifyAuth(req)) {
745
- res.statusCode = 401;
746
- res.setHeader("Access-Control-Allow-Origin", "*");
747
- res.setHeader("Content-Type", "application/json");
748
- res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
749
- notifyAuthReject("http-request");
644
+ try {
645
+ for (const file of files) {
646
+ if (pendingReconnectCheck) {
647
+ await attemptReconnect("next-file");
648
+ pendingReconnectCheck = false;
750
649
  }
751
- return;
752
- }
753
- const pathname = (req.url ?? "").split("?")[0];
754
- if (pathname === "/targets" || pathname === "/targets/") {
755
- if (!verifyAuth(req)) {
756
- res.statusCode = 401;
757
- res.setHeader("Access-Control-Allow-Origin", "*");
758
- res.setHeader("Content-Type", "application/json");
759
- res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
760
- notifyAuthReject("http-request");
761
- return;
650
+ let fileEntry;
651
+ try {
652
+ const { code } = await bundleTestFile(file, opts?.bundleOptions);
653
+ /**
654
+ * Runs one evaluate attempt and returns a FileResult, or `null` when the
655
+ * result is a genuine timeout and the caller should retry.
656
+ *
657
+ * We need to distinguish:
658
+ * - `rpcResult.ok = false` + timeout error retry candidate (`return null`)
659
+ * - `rpcResult.ok = false` + other error → final error, no retry
660
+ * - `injectAndRunBundle` throws → CDP exceptionDetails OR a
661
+ * relay-disconnect rejection from `connection.send()` (page engine
662
+ * threw, or the ws died mid-evaluate); treated as a final
663
+ * (non-retryable) error either way.
664
+ *
665
+ * The Promise.race timeout in rpc.ts RETURNS `{ok:false, error: '…'}` (it
666
+ * does NOT throw/reject). Only genuine CDP `exceptionDetails` (or a dead
667
+ * `connection.send()`) cause a throw. This distinction is what makes the
668
+ * EVALUATE_TIMEOUT_MARKER gate below reachable — the timeout result
669
+ * surfaces as `rpcResult.ok=false` with the marker string, not as a
670
+ * caught exception.
671
+ */
672
+ const attempt = async () => {
673
+ let rpcResult;
674
+ try {
675
+ rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
676
+ } catch (e) {
677
+ return {
678
+ file,
679
+ result: { error: e instanceof Error ? e.message : String(e) }
680
+ };
681
+ }
682
+ if (rpcResult.ok) return {
683
+ file,
684
+ result: rpcResult.report
685
+ };
686
+ if (rpcResult.error.includes("rpc: evaluate timed out after")) return null;
687
+ return {
688
+ file,
689
+ result: { error: rpcResult.error }
690
+ };
691
+ };
692
+ const firstResult = await attempt();
693
+ if (firstResult !== null) fileEntry = firstResult;
694
+ else {
695
+ await attemptReconnect("retry-precheck");
696
+ process.stderr.write(`relay-worker: evaluate timed out for ${file} — retrying once\n`);
697
+ const retryResult = await attempt();
698
+ if (retryResult !== null) fileEntry = retryResult;
699
+ else fileEntry = {
700
+ file,
701
+ result: { error: `${EVALUATE_TIMEOUT_MARKER} ${opts?.timeoutMs ?? 6e4}ms (after retry)` }
702
+ };
703
+ }
704
+ } catch (e) {
705
+ fileEntry = {
706
+ file,
707
+ result: { error: e instanceof Error ? e.message : String(e) }
708
+ };
762
709
  }
763
- return;
710
+ if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
711
+ fileResults.push(fileEntry);
764
712
  }
765
- });
766
- const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;
767
- const capturedChiiWss = await startChiiWithCapture(loadChiiServer(), {
768
- server: httpServer,
769
- domain: `${host}:${requestedPort}`,
770
- port: requestedPort
771
- }, chiiWssClass);
772
- if (verifyAuth) {
773
- const chiiUpgradeListeners = httpServer.listeners("upgrade");
774
- httpServer.removeAllListeners("upgrade");
775
- const rejectWss = new WebSocketServer({ noServer: true });
776
- httpServer.on("upgrade", (req, socket, head) => {
777
- const rewritten = rewriteAtPathPrefix(req.url ?? "");
778
- if (rewritten !== null) req.url = rewritten;
779
- if (!verifyAuth(req)) {
780
- rejectWss.handleUpgrade(req, socket, head, (ws) => {
781
- ws.close(RELAY_AUTH_REJECT_CLOSE_CODE, RELAY_AUTH_REJECT_REASON);
782
- });
783
- notifyAuthReject("ws-upgrade");
784
- return;
785
- }
786
- for (const listener of chiiUpgradeListeners) listener(req, socket, head);
787
- });
713
+ } finally {
714
+ unsubscribeConsole?.();
788
715
  }
789
- const actualPort = await new Promise((resolve, reject) => {
790
- httpServer.once("error", reject);
791
- httpServer.listen(requestedPort, host, () => {
792
- httpServer.off("error", reject);
793
- resolve(httpServer.address().port);
794
- });
716
+ const captures = collectCaptures ? parseCaptureLines(liveConsole.map((e) => ({ text: renderConsoleLineText(e) }))) : [];
717
+ const totals = fileResults.reduce((acc, { result }) => {
718
+ if ("error" in result) {
719
+ acc.failed += 1;
720
+ acc.total += 1;
721
+ } else {
722
+ acc.passed += result.passed;
723
+ acc.failed += result.failed;
724
+ acc.skipped += result.skipped;
725
+ acc.total += result.passed + result.failed + result.skipped;
726
+ }
727
+ return acc;
728
+ }, {
729
+ passed: 0,
730
+ failed: 0,
731
+ skipped: 0,
732
+ total: 0
795
733
  });
796
- let keepaliveHandle = null;
797
- if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {
798
- const chiiWss = capturedChiiWss;
799
- keepaliveHandle = setInterval(() => {
800
- for (const client of chiiWss._wss.clients) if (client.readyState === 1) client.ping();
801
- }, keepaliveIntervalMs);
802
- }
803
734
  return {
804
- port: actualPort,
805
- baseUrl: `http://${host}:${actualPort}`,
806
- close: () => new Promise((resolve) => {
807
- if (keepaliveHandle !== null) {
808
- clearInterval(keepaliveHandle);
809
- keepaliveHandle = null;
810
- }
811
- httpServer.close(() => resolve());
812
- })
735
+ startedAt,
736
+ duration: Date.now() - wallStart,
737
+ files: fileResults,
738
+ totals,
739
+ captures
813
740
  };
814
741
  }
815
- //#endregion
816
- //#region src/mcp/debug-server.ts
817
742
  /**
818
- * Factory that constructs a `ChiiCdpConnection` for the given relay base URL.
819
- *
820
- * Introduced as a named seam so PR-2 (dual-connection, #348) can defer
821
- * construction to first-activation time by moving or replacing this call. Since
822
- * #396 every family (relay included) is constructed lazily on its first
823
- * `start_debug`, so this is always called from the lazy boot path.
824
- *
825
- * The relay base URL is only available after `startChiiRelay()` resolves, so
826
- * the factory is called right after that point (same as before this refactor).
743
+ * Renders one `Runtime.consoleAPICalled` event to a single line of text, the
744
+ * same way `tools.ts#normalizeConsoleMessage` does (args rendered + space-
745
+ * joined). Inlined here (≈8 lines) so this module avoids importing `tools.ts`,
746
+ * which would drag the heavy MCP/Node graph (server-lock, parent-watcher, …)
747
+ * onto the test-runner entry.
748
+ *
749
+ * SECRET-HANDLING: this only stringifies console args; the caller's
750
+ * allowlist-prefix parser then discards everything that is not a genuine
751
+ * `__AIT_CAPTURE__` line.
827
752
  */
828
- function createRelayConnection(relayBaseUrl) {
829
- return new ChiiCdpConnection({
830
- relayBaseUrl,
831
- totpSecret: process.env.AIT_DEBUG_TOTP_SECRET
832
- });
753
+ function renderConsoleLineText(event) {
754
+ return event.args.map((arg) => {
755
+ if (arg.value !== void 0) {
756
+ if (typeof arg.value === "string") return arg.value;
757
+ try {
758
+ return JSON.stringify(arg.value);
759
+ } catch {
760
+ return String(arg.value);
761
+ }
762
+ }
763
+ if (arg.description !== void 0) return arg.description;
764
+ if (arg.className !== void 0) return arg.className;
765
+ return arg.subtype ?? arg.type;
766
+ }).join(" ");
833
767
  }
834
768
  /**
835
- * Boots the relay family (issues #348, #356): starts the Chii relay on an
836
- * OS-assigned port (with optional TOTP gate), opens a cloudflared quick tunnel
837
- * to the relay's confirmed port in the background, prints the attach banner,
838
- * and arms the tunnel health probe. Returns a {@link BootedFamily} whose
839
- * `getTunnelStatus()` reflects the live tunnel (it flips up once the background
840
- * tunnel resolves and follows reissues).
841
- *
842
- * Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback
843
- * (symmetry with {@link bootLocalFamily}), at most once on the first
844
- * `start_debug({ mode: 'relay-staging' })` (all-lazy, #396 — every relay boot now
845
- * flows through `switchMode` after the project-local secret load). `relay-live`
846
- * removed (#665).
847
- *
848
- * The relay base URL is only known after `startChiiRelay()` resolves, so the
849
- * `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
850
- * this function, after the relay port is confirmed.
851
- *
852
- * SECRET-HANDLING: the TOTP secret rides only inside `verifyAuth`; the wssUrl
853
- * (relay host) is never logged here directly.
769
+ * Flattens all test results from a `RelayRunReport` into a single array.
770
+ * Files that errored during bundle/inject produce a synthetic failed entry.
854
771
  */
855
- async function bootRelayFamily(options = {}) {
856
- assertRelayAuthConfigured();
857
- const relayPort = options.relayPort ?? 0;
858
- const totpEnabled = options.verifyAuth !== void 0;
859
- const relay = await startChiiRelay({
860
- port: relayPort,
861
- verifyAuth: options.verifyAuth,
862
- onAuthReject: options.onAuthReject
772
+ function flattenResults(report) {
773
+ const out = [];
774
+ for (const { file, result } of report.files) if ("error" in result) out.push({
775
+ file,
776
+ name: `<bundle/inject error>`,
777
+ status: "fail",
778
+ duration: 0,
779
+ error: result.error
863
780
  });
864
- logInfo("server.start", {
865
- port: relay.port,
866
- totpEnabled
781
+ else for (const t of result.tests) out.push({
782
+ ...t,
783
+ file
867
784
  });
868
- let tunnel = null;
869
- let tunnelStatus = makeTunnelStatus(false, null);
870
- let tunnelProbe = null;
871
- generateAttachToken();
872
- startQuickTunnel(relay.port).then((t) => {
873
- tunnel = t;
874
- tunnelStatus = makeTunnelStatus(true, t.wssUrl);
875
- options.onWssUrl?.(t.wssUrl);
876
- if (t.childPid !== void 0) options.onTunnelChildPid?.(t.childPid);
877
- logInfo("tunnel.up", { totpEnabled });
878
- tunnelProbe = startTunnelHealthProbe(t, relay.port, {
879
- onReissue: (newTunnel) => {
880
- tunnel = newTunnel;
881
- tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);
882
- options.onWssUrl?.(newTunnel.wssUrl);
883
- if (newTunnel.childPid !== void 0) options.onTunnelChildPid?.(newTunnel.childPid);
884
- printAttachBanner({
885
- wssUrl: newTunnel.wssUrl,
886
- totpEnabled
887
- }).then(() => {
888
- logInfo("tunnel.up", {
889
- totpEnabled,
890
- reissued: true
891
- });
892
- });
893
- },
894
- onPermanentDrop: (droppedAt) => {
895
- tunnelStatus = makeTunnelStatus(false, null, droppedAt, 3);
896
- logError("tunnel.down", { msg: `tunnel permanently dropped (${droppedAt}). Restart: npx @ait-co/devtools devtools-mcp` });
897
- options.onTunnelDown?.();
898
- }
899
- });
900
- return printAttachBanner({
901
- wssUrl: t.wssUrl,
902
- totpEnabled
903
- });
904
- }, (err) => {
905
- logError("tunnel.down", { msg: `Failed to open cloudflared quick tunnel: ${err instanceof Error ? err.message : String(err)}. The relay is up locally; attach over the public URL is unavailable until the tunnel starts.` });
906
- });
907
- const connection = createRelayConnection(relay.baseUrl);
908
- return {
909
- connection,
910
- relayOrigin: "intoss-webview",
911
- relayHttpUrl: relay.baseUrl,
912
- getTunnelStatus: () => tunnelStatus,
913
- stop() {
914
- tunnelProbe?.stop();
915
- tunnel?.stop();
916
- connection.close();
917
- relay.close();
918
- }
919
- };
785
+ return out;
920
786
  }
921
787
  //#endregion
922
- export { bootRelayFamily, buildRelayVerifyAuth };
788
+ export { RELAY_AUTH_REJECT_CLOSE_CODE as a, ChiiCdpConnection as i, flattenResults as n, RELAY_AUTH_REJECT_REASON as o, runTestFilesOverRelay as r, EVALUATE_TIMEOUT_MARKER as t };
923
789
 
924
- //# sourceMappingURL=debug-server-BzzNkETg.js.map
790
+ //# sourceMappingURL=relay-worker-Bt0QYKQH.js.map