@ait-co/devtools 0.1.127 → 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.
- package/dist/{attach-orchestrator-CpE0dMew.js → attach-orchestrator-Bdt5Zdo9.js} +3 -94
- package/dist/attach-orchestrator-Bdt5Zdo9.js.map +1 -0
- package/dist/{attach-orchestrator-CPMQWkGV.js → attach-orchestrator-DvrgUIx7.js} +4 -95
- package/dist/attach-orchestrator-DvrgUIx7.js.map +1 -0
- package/dist/{bundle-Cb3GFuw6.d.ts → bundle-BKqyhEK9.d.ts} +1 -1
- package/dist/{bundle-Cb3GFuw6.d.ts.map → bundle-BKqyhEK9.d.ts.map} +1 -1
- package/dist/{capture-B1zfGyTo.d.ts → capture-ltuV0gZa.d.ts} +1 -1
- package/dist/{capture-B1zfGyTo.d.ts.map → capture-ltuV0gZa.d.ts.map} +1 -1
- package/dist/{cdp-connection-Dxw_EY8s.d.ts → cdp-connection-D7AYQck2.d.ts} +1 -1
- package/dist/{cdp-connection-Dxw_EY8s.d.ts.map → cdp-connection-D7AYQck2.d.ts.map} +1 -1
- package/dist/{cell-D0dJSGHi.js → cell-DA_lcpDI.js} +2 -2
- package/dist/{cell-D0dJSGHi.js.map → cell-DA_lcpDI.js.map} +1 -1
- package/dist/{cell-CX92lDI0.js → cell-DLRv4iG6.js} +2 -2
- package/dist/{cell-CX92lDI0.js.map → cell-DLRv4iG6.js.map} +1 -1
- package/dist/{debug-server-Dctc9GAh.js → debug-server--Y0QZVJS.js} +57 -58
- package/dist/debug-server--Y0QZVJS.js.map +1 -0
- package/dist/debug-server-Bu3pCG9c.js +436 -0
- package/dist/debug-server-Bu3pCG9c.js.map +1 -0
- package/dist/debug-server-e1qPe0ZB.js +379 -0
- package/dist/{debug-server-BzzNkETg.js.map → debug-server-e1qPe0ZB.js.map} +1 -1
- package/dist/log-CUik_tHp.js +95 -0
- package/dist/log-CUik_tHp.js.map +1 -0
- package/dist/mcp/cli.js +1296 -1230
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +1 -1
- package/dist/{pool-DBLKNeRC.d.ts → pool-C0pc7T2p.d.ts} +4 -4
- package/dist/{pool-DBLKNeRC.d.ts.map → pool-C0pc7T2p.d.ts.map} +1 -1
- package/dist/{qr-http-server-Bo8LMOJQ.js → qr-http-server-HANhOFwI.js} +1 -1
- package/dist/{qr-http-server-Bo8LMOJQ.js.map → qr-http-server-HANhOFwI.js.map} +1 -1
- package/dist/{relay-factory-Dug7W6Ao.js → relay-factory-Ck3vBonr.js} +6 -6
- package/dist/{relay-factory-Dug7W6Ao.js.map → relay-factory-Ck3vBonr.js.map} +1 -1
- package/dist/{relay-secret-store-DKxs7zwq.js → relay-secret-store-DzxDsK6S.js} +1 -1
- package/dist/{relay-secret-store-DKxs7zwq.js.map → relay-secret-store-DzxDsK6S.js.map} +1 -1
- package/dist/{relay-url-store-xmUuTjXA.js → relay-url-store-D3rY-GJJ.js} +2 -2
- package/dist/{relay-url-store-xmUuTjXA.js.map → relay-url-store-D3rY-GJJ.js.map} +1 -1
- package/dist/{debug-server-BzzNkETg.js → relay-worker-Bt0QYKQH.js} +212 -346
- package/dist/relay-worker-Bt0QYKQH.js.map +1 -0
- package/dist/{relay-worker-TReZtzGl.d.ts → relay-worker-o0M2ndyJ.d.ts} +7 -7
- package/dist/relay-worker-o0M2ndyJ.d.ts.map +1 -0
- package/dist/{runtime-ozyPnRbU.d.ts → runtime-9xhN9pr8.d.ts} +1 -1
- package/dist/{runtime-ozyPnRbU.d.ts.map → runtime-9xhN9pr8.d.ts.map} +1 -1
- package/dist/test-runner/bin.js +721 -15
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/bundle.d.ts +1 -1
- package/dist/test-runner/capture.d.ts +1 -1
- package/dist/test-runner/config.d.ts +1 -1
- package/dist/test-runner/config.js +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/pool.js +1 -1
- package/dist/test-runner/relay-factory.d.ts +1 -1
- package/dist/test-runner/relay-factory.js +1 -1
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/relay-worker.js +1 -183
- package/dist/test-runner/report.d.ts +3 -3
- package/dist/test-runner/rpc.d.ts +2 -2
- package/dist/test-runner/rpc.js +12 -3
- package/dist/test-runner/rpc.js.map +1 -1
- package/dist/test-runner/runtime.d.ts +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/package.json +1 -1
- package/dist/attach-orchestrator-CPMQWkGV.js.map +0 -1
- package/dist/attach-orchestrator-CpE0dMew.js.map +0 -1
- package/dist/debug-server-1dhDA3aw.js +0 -980
- package/dist/debug-server-1dhDA3aw.js.map +0 -1
- package/dist/debug-server-Dctc9GAh.js.map +0 -1
- package/dist/relay-worker-TReZtzGl.d.ts.map +0 -1
- package/dist/test-runner/relay-worker.js.map +0 -1
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
import { n as
|
|
3
|
-
import "./test-runner/
|
|
4
|
-
import {
|
|
5
|
-
import "./
|
|
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
|
|
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/
|
|
572
|
+
//#region src/test-runner/relay-worker.ts
|
|
561
573
|
/**
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
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
|
-
*
|
|
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
|
|
580
|
+
const EVALUATE_TIMEOUT_MARKER = "rpc: evaluate timed out after";
|
|
620
581
|
/**
|
|
621
|
-
*
|
|
582
|
+
* Runs all `files` sequentially over the given CDP `connection`.
|
|
622
583
|
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
|
|
627
|
-
|
|
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 `
|
|
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
|
-
*
|
|
647
|
-
*
|
|
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
|
-
*
|
|
653
|
-
*
|
|
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
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
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
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
738
|
-
|
|
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
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
710
|
+
if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
|
|
711
|
+
fileResults.push(fileEntry);
|
|
764
712
|
}
|
|
765
|
-
}
|
|
766
|
-
|
|
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
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
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
|
|
829
|
-
return
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
*
|
|
836
|
-
*
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
781
|
+
else for (const t of result.tests) out.push({
|
|
782
|
+
...t,
|
|
783
|
+
file
|
|
867
784
|
});
|
|
868
|
-
|
|
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 {
|
|
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=
|
|
790
|
+
//# sourceMappingURL=relay-worker-Bt0QYKQH.js.map
|