@ait-co/devtools 0.1.133 → 0.1.135
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/{cdp-connection-D7AYQck2.d.ts → cdp-connection-BYE9meXe.d.ts} +12 -2
- package/dist/{cdp-connection-D7AYQck2.d.ts.map → cdp-connection-BYE9meXe.d.ts.map} +1 -1
- package/dist/{debug-server-CS1BS2BX.js → debug-server-B2aCwwtV.js} +24 -9
- package/dist/debug-server-B2aCwwtV.js.map +1 -0
- package/dist/{debug-server-DnNCoNiR.js → debug-server-CBqMfcUi.js} +2 -2
- package/dist/{debug-server-DnNCoNiR.js.map → debug-server-CBqMfcUi.js.map} +1 -1
- package/dist/{debug-server-DmzuK2c6.js → debug-server-DR84Z243.js} +9 -4
- package/dist/debug-server-DR84Z243.js.map +1 -0
- package/dist/mcp/cli.js +105 -21
- 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-x28Wv1ty.d.ts → pool-C6TgrcyW.d.ts} +3 -3
- package/dist/{pool-x28Wv1ty.d.ts.map → pool-C6TgrcyW.d.ts.map} +1 -1
- package/dist/{qr-http-server-iH_Hh_si.cjs → qr-http-server-B1en4tTL.cjs} +60 -7
- package/dist/{qr-http-server-iH_Hh_si.cjs.map → qr-http-server-B1en4tTL.cjs.map} +1 -1
- package/dist/{qr-http-server-C_DTp9WU.js → qr-http-server-CLt72Dwe.js} +60 -8
- package/dist/{qr-http-server-DeRrKp_R.js.map → qr-http-server-CLt72Dwe.js.map} +1 -1
- package/dist/{qr-http-server-FVn61xAq.js → qr-http-server-DlNWJEaH.js} +60 -7
- package/dist/{qr-http-server-C_DTp9WU.js.map → qr-http-server-DlNWJEaH.js.map} +1 -1
- package/dist/{qr-http-server-DX_9UA-p.cjs → qr-http-server-hnuUw9TK.cjs} +60 -7
- package/dist/{qr-http-server-DX_9UA-p.cjs.map → qr-http-server-hnuUw9TK.cjs.map} +1 -1
- package/dist/{qr-http-server-V0xuU5TJ.js → qr-http-server-iBqsAKor.js} +60 -7
- package/dist/{qr-http-server-V0xuU5TJ.js.map → qr-http-server-iBqsAKor.js.map} +1 -1
- package/dist/{qr-http-server-DeRrKp_R.js → qr-http-server-yQ1EWTwf.js} +61 -7
- package/dist/{qr-http-server-FVn61xAq.js.map → qr-http-server-yQ1EWTwf.js.map} +1 -1
- package/dist/{relay-factory-BQfmK7RP.js → relay-factory-CjqAJ3Y8.js} +4 -4
- package/dist/relay-factory-CjqAJ3Y8.js.map +1 -0
- package/dist/{relay-worker-DtX_eszH.d.ts → relay-worker-Dw3aIzIK.d.ts} +3 -3
- package/dist/{relay-worker-DtX_eszH.d.ts.map → relay-worker-Dw3aIzIK.d.ts.map} +1 -1
- package/dist/{relay-worker-MZq4fLv8.js → relay-worker-UK0nBfDX.js} +18 -8
- package/dist/relay-worker-UK0nBfDX.js.map +1 -0
- package/dist/{runtime-9xhN9pr8.d.ts → runtime-DfHHZms2.d.ts} +34 -5
- package/dist/runtime-DfHHZms2.d.ts.map +1 -0
- package/dist/test-runner/bin.js +68 -15
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/config.d.ts +9 -1
- package/dist/test-runner/config.d.ts.map +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 +19 -1
- package/dist/test-runner/relay-factory.d.ts.map +1 -1
- package/dist/test-runner/relay-factory.js +3 -3
- package/dist/test-runner/relay-factory.js.map +1 -1
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/relay-worker.js +1 -1
- package/dist/test-runner/report.d.ts +2 -2
- package/dist/test-runner/rpc.d.ts +8 -5
- package/dist/test-runner/rpc.d.ts.map +1 -1
- package/dist/test-runner/rpc.js +15 -4
- package/dist/test-runner/rpc.js.map +1 -1
- package/dist/test-runner/runtime.d.ts +2 -2
- package/dist/test-runner/runtime.js +30 -4
- package/dist/test-runner/runtime.js.map +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/{tunnel-DP453L4e.cjs → tunnel-Cr_oTkaj.cjs} +2 -2
- package/dist/{tunnel-DP453L4e.cjs.map → tunnel-Cr_oTkaj.cjs.map} +1 -1
- package/dist/{tunnel-Djf-zmjJ.js → tunnel-DUCogcQi.js} +2 -2
- package/dist/{tunnel-Djf-zmjJ.js.map → tunnel-DUCogcQi.js.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +1 -1
- package/dist/unplugin/tunnel.js +1 -1
- package/package.json +1 -1
- package/dist/debug-server-CS1BS2BX.js.map +0 -1
- package/dist/debug-server-DmzuK2c6.js.map +0 -1
- package/dist/relay-factory-BQfmK7RP.js.map +0 -1
- package/dist/relay-worker-MZq4fLv8.js.map +0 -1
- package/dist/runtime-9xhN9pr8.d.ts.map +0 -1
|
@@ -29,7 +29,7 @@ function createRelayConnectionFactory(opts) {
|
|
|
29
29
|
const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-Cjdhir2U.js");
|
|
30
30
|
const { injectDebugIndicator, injectGlobals } = await import("../cell-D1y4shoV.js");
|
|
31
31
|
const { loadRelaySecretReadOnly } = await import("../relay-secret-store-BcVrWwTq.js");
|
|
32
|
-
const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-
|
|
32
|
+
const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-B2aCwwtV.js");
|
|
33
33
|
await loadRelaySecretReadOnly({ projectRoot });
|
|
34
34
|
let resolveTunnelUp;
|
|
35
35
|
const tunnelReady = new Promise((resolve) => {
|
|
@@ -59,7 +59,7 @@ function createRelayConnectionFactory(opts) {
|
|
|
59
59
|
canOpenBrowser: () => !headless
|
|
60
60
|
};
|
|
61
61
|
try {
|
|
62
|
-
const { startQrHttpServer } = await import("../qr-http-server-
|
|
62
|
+
const { startQrHttpServer } = await import("../qr-http-server-CLt72Dwe.js");
|
|
63
63
|
const getDashboardState = () => ({
|
|
64
64
|
tunnel: attachDeps.getTunnelStatus(),
|
|
65
65
|
pages: null,
|
|
@@ -68,7 +68,7 @@ function createRelayConnectionFactory(opts) {
|
|
|
68
68
|
phase,
|
|
69
69
|
manualPrompt
|
|
70
70
|
});
|
|
71
|
-
qrServer = await startQrHttpServer(getDashboardState);
|
|
71
|
+
qrServer = await startQrHttpServer(getDashboardState, { dashboardPort: opts.dashboardPort });
|
|
72
72
|
attachDeps.qrHttpServer = qrServer;
|
|
73
73
|
attachDeps.onAttachUrlBuilt = (parts) => {
|
|
74
74
|
lastAttachParts = parts;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relay-factory.js","names":[],"sources":["../../src/test-runner/relay-factory.ts"],"sourcesContent":["/**\n * Relay connection factory for the Vitest custom pool (devtools#696).\n *\n * The Vitest pool (`pool.ts`) takes a {@link RelayConnectionFactory} that knows\n * how to `open()` a live CDP relay connection (boot relay → QR → phone scan →\n * cell inject → enableDomains) and `close()` it. Before this module that exact\n * assembly lived only inside the `devtools-test` CLI's `main()`; the standalone\n * CLI and the Vitest pool would otherwise each hand-roll it and drift.\n *\n * This is the single source of that assembly. `cli.ts` is refactored to call\n * `createRelayConnectionFactory(...).open()`, and `definePhoneVitestConfig`\n * (config.ts) exposes it so a downstream `vitest.config.ts` can wire the pool:\n *\n * import { createRelayConnectionFactory } from '@ait-co/devtools/test-runner';\n * const connection = createRelayConnectionFactory({\n * schemeUrl,\n * cell,\n * // REQUIRED — own the stdout decision (the chunks carry the relay wss +\n * // TOTP code). Suppress on non-interactive stdout; print otherwise.\n * onQrContent: (chunks) => {\n * if (!process.stdout.isTTY) return;\n * for (const c of chunks) process.stdout.write(`${c}\\n`);\n * },\n * });\n * export default defineConfig({ test: definePhoneVitestConfig({ connection }) });\n *\n * The heavy boot graph (chii relay, cloudflared, ws, debug-server) is pulled via\n * DYNAMIC import inside `open()` — so merely importing this module (or the\n * `@ait-co/devtools/test-runner` barrel) does NOT statically drag that graph in.\n *\n * SECRET-HANDLING: relay wss URLs, scheme URLs, and the TOTP secret/code are\n * never logged. `open()` reads the project-local `.ait_relay` secret read-only\n * (never mints) and the minted TOTP code rides only inside the QR `at=` param.\n *\n * Node-only. react-free (CdpConnection + lazily-imported MCP boot helpers only).\n */\n\nimport type { AttachDeps, AttachUrlParts } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { DashboardState, QrHttpServer } from '../mcp/qr-http-server.js';\nimport type { RelayConnectionFactory } from './pool.js';\n\n// NOTE: every value import below is a DYNAMIC import inside `open()`. This module\n// keeps ONLY type-level static imports so that re-exporting it from the\n// `@ait-co/devtools/test-runner` barrel (config.ts) does NOT statically drag the\n// heavy MCP graph (cell.ts → attach-orchestrator.ts → tools.ts → server-lock,\n// plus chii/cloudflared via debug-server) onto that Node-config entry.\n\n/** Options for {@link createRelayConnectionFactory}. */\nexport interface RelayConnectionFactoryOptions {\n /**\n * intoss-private:// scheme URL from `ait deploy --scheme-only` (env3). The\n * phone cold-loads the candidate bundle this URL points at. SECRET-HANDLING:\n * never logged.\n */\n schemeUrl: string;\n /**\n * Project root for the `.ait_relay` secret lookup (read-only). Defaults to\n * `process.cwd()`.\n */\n projectRoot?: string;\n /**\n * Attach wait timeout in ms — how long `open()` waits for the phone to scan\n * the QR and attach. Omitted (the default) means **wait indefinitely** —\n * the runner stays up until the user stops it (Ctrl-C/SIGTERM). QR-scan\n * wait is a human-paced action; there is no sound default bound for it\n * (devtools#735). Pass an explicit value to opt into a bounded wait (CI/\n * headless callers — `--attach-timeout` on the CLI). (The per-file evaluate\n * timeout is separate, passed via the pool's `run` options.)\n */\n timeoutMs?: number;\n /** Disable browser auto-open of the QR dashboard (text QR only). */\n headless?: boolean;\n /**\n * Cell axes injected as `__AIT_CELL__` before the first test bundle runs, so\n * sdk-example's capture picks up the correct sdkLine/platform. Optional — when\n * omitted no cell is injected. The values are not secrets.\n */\n cell?: { sdkLine: string; platform: string };\n /**\n * Receives the QR/attach render content (text chunks) from\n * `renderAndMaybeWait`, so the caller decides whether to print them — e.g.\n * suppress on non-interactive stdout.\n *\n * REQUIRED (not optional) on purpose: these chunks contain the QR payload\n * (which encodes the relay wss + TOTP `at=` code) and the attach JSON block.\n * Making the hook mandatory means the factory never falls back to printing\n * them itself — a downstream `vitest.config.ts` consumer that wires this via\n * the `@ait-co/devtools/test-runner` barrel is forced to make an explicit\n * stdout decision rather than silently leaking the secret-bearing block.\n *\n * SECRET-HANDLING: when a non-interactive caller is detected the hook MUST\n * suppress the WHOLE chunk (not just `attachUrl`), since `relayUrl` rides in\n * the same block.\n */\n onQrContent: (textChunks: string[]) => void;\n}\n\n/**\n * Builds a {@link RelayConnectionFactory} that opens a standalone env3 relay\n * connection.\n *\n * `open()` performs the full attach lifecycle and BLOCKS while a human scans\n * the rendered QR with their phone — there is no way around the manual scan\n * for env3. By default the wait is UNBOUNDED (`opts.timeoutMs` omitted): the\n * runner stays up until the user stops it (Ctrl-C/SIGTERM), since QR-scan is\n * a human-paced action with no sound default bound (devtools#735). Passing an\n * explicit `timeoutMs` opts into the old bounded behavior (CI/headless\n * callers). It resolves with the live `CdpConnection` once a matching page\n * attaches; `close()` tears the relay family down.\n *\n * The factory holds the booted relay family in a closure so `close()` can stop\n * it. A second `open()` on the same factory boots a fresh family (the previous\n * one should have been `close()`d first).\n */\nexport function createRelayConnectionFactory(\n opts: RelayConnectionFactoryOptions,\n): RelayConnectionFactory {\n const projectRoot = opts.projectRoot ?? process.cwd();\n // Undefined/absent → Infinity (wait forever). An explicit finite value opts\n // into the old bounded behavior. Number.isFinite guards downstream\n // (renderAndMaybeWait / waitForFirstTarget) treat Infinity as \"no timer\".\n const timeoutMs = opts.timeoutMs ?? Number.POSITIVE_INFINITY;\n const headless = opts.headless === true;\n\n // Captured so close() can stop the family that open() booted.\n let family: { connection: CdpConnection; stop(): void } | undefined;\n // QR HTTP server — started during open() when not headless, closed in close().\n let qrServer: QrHttpServer | undefined;\n // Session-phase state (#730) — drives the dashboard's `phase` field so the\n // CLI's run start/complete and final teardown push an immediate SSE update\n // instead of the dashboard just going dark when the process exits.\n let phase: DashboardState['phase'] = 'active';\n // Manual-blocking prompt state (devtools#741) — drives the dashboard's\n // `manualPrompt` field. `null` outside a manual step.\n let manualPrompt: DashboardState['manualPrompt'] = null;\n\n return {\n async open(): Promise<CdpConnection> {\n // Dynamic imports: keep the chii/cloudflared/debug-server graph OFF the\n // static import graph of this module (and the test-runner config barrel).\n const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import(\n '../mcp/attach-orchestrator.js'\n );\n const { injectDebugIndicator, injectGlobals } = await import('./cell.js');\n const { loadRelaySecretReadOnly } = await import('../mcp/relay-secret-store.js');\n const { bootRelayFamily, buildRelayVerifyAuth } = await import('../mcp/debug-server.js');\n\n // Load the project-local .ait_relay secret into AIT_DEBUG_TOTP_SECRET\n // BEFORE booting the relay so assertRelayAuthConfigured()/buildRelayVerifyAuth()\n // at the boot site see it. Read-only — never mints. SECRET-HANDLING: the\n // value is never logged here.\n await loadRelaySecretReadOnly({ projectRoot });\n\n // PRIMARY FIX (devtools#714): bootRelayFamily starts the cloudflared tunnel\n // as a background promise and returns immediately with tunnel.up === false.\n // We must NOT call prepareAttach until the tunnel is up — it fails fast when\n // tunnel.up is false (attach-orchestrator.ts tunnel-down guard).\n //\n // Wire onWssUrl (mirroring the MCP daemon path in debug-server.ts) to:\n // 1. resolve a caller-held tunnel-ready promise so open() can await it, and\n // 2. re-push dashboard SSE on late tunnel-up events (notifyStateChange).\n //\n // SECRET-HANDLING: onWssUrl receives the relay wss URL — never log it.\n let resolveTunnelUp!: () => void;\n const tunnelReady = new Promise<void>((resolve) => {\n resolveTunnelUp = resolve;\n });\n\n const booted = await bootRelayFamily({\n verifyAuth: buildRelayVerifyAuth(),\n onWssUrl: () => {\n // Resolve the tunnel-ready gate so the prepareAttach call below is\n // unblocked. qrServer may not be set yet at call time (it's started\n // after bootRelayFamily), so we use optional chaining — if the tunnel\n // happens to come up after qrServer is started, notifyStateChange pushes\n // the freshly minted attachUrl to any waiting dashboard SSE clients.\n // SECRET-HANDLING: wssUrl is NOT forwarded here — the value travels only\n // inside the closure via getTunnelStatus().wssUrl (used by mintAttachUrl).\n resolveTunnelUp();\n qrServer?.notifyStateChange();\n },\n // #730: parity with the MCP daemon path (debug-server.ts's onTunnelDown\n // wiring) — without this, a permanent tunnel drop during a standalone\n // CLI run left the dashboard showing a dead-but-scannable QR until the\n // watchdog (up to ~210s) finally fired.\n onTunnelDown: () => {\n qrServer?.notifyStateChange();\n },\n });\n family = booted;\n\n // If the tunnel is already up (extremely fast boot or test double), resolve\n // immediately so we don't stall on a promise that will never fire.\n if (booted.getTunnelStatus?.().up) {\n resolveTunnelUp();\n }\n\n // Track the last-captured attach parts so getDashboardState can mint a\n // fresh attach URL on every dashboard request/SSE push (fresh TOTP at=).\n // SECRET-HANDLING: parts contain the relay wss + scheme URL — never logged.\n let lastAttachParts: AttachUrlParts | undefined;\n\n // Assemble AttachDeps. We set qrHttpServer and onAttachUrlBuilt below\n // (before prepareAttach) after optionally starting the web-QR server.\n const attachDeps: AttachDeps = {\n getTunnelStatus: booted.getTunnelStatus ?? (() => ({ up: false, wssUrl: null })),\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n qrHttpServer: undefined, // filled in below if web-QR server started\n onAttachUrlBuilt: undefined, // filled in below\n canOpenBrowser: () => !headless,\n };\n\n // Web-QR server: reuse the same loopback HTTP dashboard that the MCP\n // start_attach path uses (src/mcp/qr-http-server.ts). This makes the QR\n // scannable even when stdout is non-interactive (Claude Code `!` / CI),\n // because the browser is opened by URL — not via captured stdout.\n //\n // Headless decision: we start the server even in --headless mode so the\n // printed stderr URL can be opened manually. The existing\n // canOpenBrowser: () => !headless gate inside renderAndMaybeWait prevents\n // the auto-open; headless users see only the stderr URL.\n //\n // On failure we fall back gracefully to the text-QR path — do NOT crash.\n // SECRET-HANDLING: only http://127.0.0.1:<port>/ (no secrets) goes to stderr.\n try {\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n\n const getDashboardState = (): DashboardState => ({\n tunnel: attachDeps.getTunnelStatus(),\n pages: null, // CLI/pool: no page-list introspection needed\n attachUrl: lastAttachParts ? mintAttachUrl(attachDeps, lastAttachParts) : null,\n mode: 'relay-dev' as const,\n phase, // #730 — CLI-only 'running'/'complete' transitions via onSessionPhase\n manualPrompt, // #741 — CLI-only --manual-blocking transitions via onManualPrompt\n });\n\n qrServer = await startQrHttpServer(getDashboardState);\n\n // Wire the QR server into attachDeps BEFORE prepareAttach is called so\n // renderAndMaybeWait sees it and takes Path 2/3 (web-QR) instead of Path 4.\n attachDeps.qrHttpServer = qrServer;\n\n // Capture attach parts via onAttachUrlBuilt so getDashboardState can\n // mint a fresh URL (fresh TOTP at= code) on every dashboard render.\n attachDeps.onAttachUrlBuilt = (parts: AttachUrlParts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n };\n\n // Print the loopback dashboard URL to stderr — it carries no secrets\n // (TOTP codes and relay wss live only in the in-memory HTTP response).\n process.stderr.write(`devtools-test: QR dashboard: http://127.0.0.1:${qrServer.port}/\\n`);\n } catch {\n // startQrHttpServer failed (e.g. port conflict, import error). Fall back\n // to the existing text-QR path by leaving qrHttpServer: undefined.\n // qrServer remains undefined; close() handles that with optional chaining.\n }\n\n // PRIMARY FIX (devtools#714): await tunnel readiness before calling\n // prepareAttach. Race: a 15 s timeout is generous — cloudflared typically\n // comes up in < 5 s. On timeout we still call prepareAttach; it will hit\n // the tunnel-down guard and throw a secret-free error (same as before,\n // but now with a clear diagnostic instead of a silent WAITING freeze).\n //\n // Implementation: Promise.race against a 15 000 ms timeout signal. We do\n // NOT use a timer-based early-exit because the existing code already\n // surface-fails on tunnel-down inside prepareAttach with a secret-free\n // message. The timeout here is purely a \"give the tunnel a fair chance\"\n // gate — not a correctness boundary.\n const TUNNEL_BOOT_TIMEOUT_MS = 15_000;\n await Promise.race([\n tunnelReady,\n new Promise<void>((resolve) => setTimeout(resolve, TUNNEL_BOOT_TIMEOUT_MS)),\n ]);\n\n const prep = await prepareAttach(\n attachDeps,\n 'relay-dev',\n { scheme_url: opts.schemeUrl },\n booted.connection,\n );\n if (!prep.ok) {\n booted.stop();\n family = undefined;\n // SECONDARY FIX (devtools#714): close the QR server on the failure path\n // so the loopback port listener does not leak. The normal-exit path is\n // handled by close(); this mirrors that cleanup for the error path.\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING: do NOT surface `prep.error.content` in the thrown\n // message. Some prep error paths build their text from attach\n // components, and the CLI catch writes `e.message` to stderr — embedding\n // that text risks leaking the scheme/relay wss URL. The detailed\n // diagnostic is the daemon/dashboard's job; the factory throws a\n // secret-free message only.\n throw new Error(\n 'createRelayConnectionFactory: attach preparation failed — check the scheme_url and that the relay tunnel is up',\n );\n }\n\n // Render the QR + wait for the phone to attach. SECRET-HANDLING: this\n // function never logs scheme/wss/TOTP values.\n const waitResult = await renderAndMaybeWait(\n attachDeps,\n prep,\n true,\n timeoutMs,\n booted.connection,\n );\n if (waitResult.isError) {\n booted.stop();\n family = undefined;\n // SECONDARY FIX (devtools#714): close the QR server on the timeout path\n // (mirrors the prep.ok failure path above).\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING (BLOCKER fix): `waitResult.content` is the timeout\n // result built from `buildTimeoutError(baseText, ...)`, and `baseText`\n // is `JSON.stringify({ attachUrl, relayUrl, ... })` — `attachUrl` carries\n // the TOTP `at=` code and `relayUrl` is the relay `wss://` URL. The CLI\n // catch writes `e.message` to stderr, so extracting that text here would\n // leak the relay wss + TOTP code on every timeout. Throw a secret-free\n // message with only the timeout duration.\n const timeoutSec = Math.round(timeoutMs / 1000);\n throw new Error(\n `createRelayConnectionFactory: attach timed out after ${timeoutSec}s — phone did not scan the QR within the timeout`,\n );\n }\n\n // Surface the QR/attach render content to the caller, which owns the\n // stdout decision (suppress on non-interactive stdout). There is no\n // fallback that prints here itself: `onQrContent` is a required option so\n // the secret-bearing block (attachUrl TOTP + relayUrl wss) can never be\n // emitted without an explicit caller decision. SECRET-HANDLING.\n const qrChunks = waitResult.content\n .filter((c): c is { type: 'text'; text: string } => c.type === 'text')\n .map((c) => c.text);\n opts.onQrContent(qrChunks);\n\n // PAGE-READY GATE (devtools#720, fix (a)+(b)):\n //\n // FIX (a) — ORDER: enableDomains() MUST run before injectDebugIndicator()\n // and injectGlobals(). Both inject calls use Runtime.evaluate via\n // sendCommand(), which guards with `if (!this.ws) reject(\"Call\n // enableDomains() first\")`. With the old ordering (inject → inject →\n // enableDomains) injectDebugIndicator's throw was swallowed by cell.ts\n // try/catch but injectGlobals (no try/catch) propagated fatally — when\n // --cell was set open() hard-threw before enableDomains ever ran and the\n // CLI exited with 0 files executed.\n //\n // FIX (b) — BOUNDED RETRY: the window between waitForFirstTarget (non-\n // empty /targets) and enableDomains (page-level WS open) is where a\n // Cloudflare edge idle-drop can disconnect the phone. enableDomains()\n // calls refreshTargets() internally; if the target list is empty at that\n // point it throws \"No mini-app page attached\". We absorb up to\n // PAGE_READY_RETRIES transient failures by waiting briefly and retrying\n // the refreshTargets+enableDomains sequence. enableDomains() is\n // idempotent (concurrent callers share the in-flight promise), so retries\n // are safe. Only after all retries fail do we surface a secret-free\n // error. This is the CLI equivalent of the MCP daemon's soft-fail\n // cushion in relay-worker.ts.\n //\n // Neither fix touches chii-connection.ts or the MCP daemon path.\n const PAGE_READY_RETRIES = 3;\n const PAGE_READY_RETRY_DELAY_MS = 1_500;\n\n let lastEnableError: Error | undefined;\n for (let attempt = 1; attempt <= PAGE_READY_RETRIES; attempt++) {\n try {\n // FIX (a): enableDomains first — opens the page-level CDP websocket.\n await booted.connection.enableDomains();\n lastEnableError = undefined;\n break;\n } catch (err) {\n lastEnableError = err instanceof Error ? err : new Error(String(err));\n if (attempt < PAGE_READY_RETRIES) {\n // FIX (b): brief pause then re-poll /targets before retrying\n // enableDomains. Use a non-leaking approach: wait, then fall\n // through to the next loop iteration.\n await new Promise<void>((resolve) => setTimeout(resolve, PAGE_READY_RETRY_DELAY_MS));\n }\n }\n }\n\n if (lastEnableError !== undefined) {\n booted.stop();\n family = undefined;\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING: message contains only a duration + attempt count.\n // No relay wss URL, scheme URL, or TOTP code is included.\n throw new Error(\n `createRelayConnectionFactory: page did not become ready after ${PAGE_READY_RETRIES} attempts (${Math.round((PAGE_READY_RETRIES * PAGE_READY_RETRY_DELAY_MS) / 1000)}s) — the mini-app page may have disconnected before enableDomains() could open the CDP websocket`,\n );\n }\n\n // FIX (a): inject AFTER enableDomains so the page-level CDP websocket is\n // open and sendCommand() will not hit the ws===null guard.\n\n // Show the on-phone \"Debugger Connected\" badge. injectDebugIndicator\n // swallows its own errors (cell.ts try/catch), so a badge failure is\n // non-fatal — test execution proceeds regardless.\n await injectDebugIndicator(booted.connection);\n\n // Inject the cell globals before any test bundle runs (session-global).\n // injectGlobals() does NOT swallow errors — a genuine type/eval failure\n // here surfaces clearly instead of silently skipping cell injection.\n if (opts.cell !== undefined) {\n await injectGlobals(booted.connection, { __AIT_CELL__: opts.cell });\n }\n\n return booted.connection;\n },\n\n // #730: drives the dashboard's `phase` field so the CLI's run start/end\n // push an immediate SSE update instead of the dashboard just going dark.\n onSessionPhase(next: 'running' | 'complete'): void {\n phase = next;\n qrServer?.notifyStateChange();\n },\n\n // #741: drives the dashboard's `manualPrompt` field for --manual-blocking\n // — the CLI calls this immediately before injecting each manual file\n // (prompt set) and once more with `null` after the last one (prompt\n // cleared). Push is immediate (not waited on for ack) — v1 does not block\n // on dashboard acknowledgment, only on the file's own (long) evaluate.\n onManualPrompt(next: { file: string; index: number; total: number } | null): void {\n manualPrompt = next;\n qrServer?.notifyStateChange();\n },\n\n async close(connection: CdpConnection): Promise<void> {\n // #730: flip the on-phone badge to disconnected AND push the terminal\n // dashboard frame BEFORE tearing anything down, so neither surface goes\n // dark without explanation when the CLI exits (dog-food gaps #1 + #2).\n // Both are best-effort over the still-open channels — the ordering\n // (before family.stop()/qrServer.close()) is load-bearing:\n // - qrServer.notifyStateChange() writes synchronously to the still-open\n // SSE sockets, so the 'complete' frame is on the wire before the HTTP\n // server is closed below.\n // - injectDebugIndicator runs over the still-open CDP channel, so the\n // phone actually receives the disconnected-state update before the\n // relay/tunnel are torn down.\n if (phase !== 'complete') {\n phase = 'complete';\n qrServer?.notifyStateChange();\n }\n try {\n const { injectDebugIndicator } = await import('./cell.js');\n await injectDebugIndicator(connection, { state: 'disconnected' });\n } catch {\n // Channel may already be down (e.g. attach never completed) — non-fatal,\n // the badge is informational UI only.\n }\n // family.stop() is synchronous best-effort: closes the CDP connection and\n // shuts down the relay + cloudflared child.\n family?.stop();\n family = undefined;\n // Close the web-QR HTTP server if one was started during open().\n // close() is idempotent via optional chaining + reassignment to undefined.\n await qrServer?.close();\n qrServer = undefined;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"relay-factory.js","names":[],"sources":["../../src/test-runner/relay-factory.ts"],"sourcesContent":["/**\n * Relay connection factory for the Vitest custom pool (devtools#696).\n *\n * The Vitest pool (`pool.ts`) takes a {@link RelayConnectionFactory} that knows\n * how to `open()` a live CDP relay connection (boot relay → QR → phone scan →\n * cell inject → enableDomains) and `close()` it. Before this module that exact\n * assembly lived only inside the `devtools-test` CLI's `main()`; the standalone\n * CLI and the Vitest pool would otherwise each hand-roll it and drift.\n *\n * This is the single source of that assembly. `cli.ts` is refactored to call\n * `createRelayConnectionFactory(...).open()`, and `definePhoneVitestConfig`\n * (config.ts) exposes it so a downstream `vitest.config.ts` can wire the pool:\n *\n * import { createRelayConnectionFactory } from '@ait-co/devtools/test-runner';\n * const connection = createRelayConnectionFactory({\n * schemeUrl,\n * cell,\n * // REQUIRED — own the stdout decision (the chunks carry the relay wss +\n * // TOTP code). Suppress on non-interactive stdout; print otherwise.\n * onQrContent: (chunks) => {\n * if (!process.stdout.isTTY) return;\n * for (const c of chunks) process.stdout.write(`${c}\\n`);\n * },\n * });\n * export default defineConfig({ test: definePhoneVitestConfig({ connection }) });\n *\n * The heavy boot graph (chii relay, cloudflared, ws, debug-server) is pulled via\n * DYNAMIC import inside `open()` — so merely importing this module (or the\n * `@ait-co/devtools/test-runner` barrel) does NOT statically drag that graph in.\n *\n * SECRET-HANDLING: relay wss URLs, scheme URLs, and the TOTP secret/code are\n * never logged. `open()` reads the project-local `.ait_relay` secret read-only\n * (never mints) and the minted TOTP code rides only inside the QR `at=` param.\n *\n * Node-only. react-free (CdpConnection + lazily-imported MCP boot helpers only).\n */\n\nimport type { AttachDeps, AttachUrlParts } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { DashboardState, QrHttpServer } from '../mcp/qr-http-server.js';\nimport type { RelayConnectionFactory } from './pool.js';\n\n// NOTE: every value import below is a DYNAMIC import inside `open()`. This module\n// keeps ONLY type-level static imports so that re-exporting it from the\n// `@ait-co/devtools/test-runner` barrel (config.ts) does NOT statically drag the\n// heavy MCP graph (cell.ts → attach-orchestrator.ts → tools.ts → server-lock,\n// plus chii/cloudflared via debug-server) onto that Node-config entry.\n\n/** Options for {@link createRelayConnectionFactory}. */\nexport interface RelayConnectionFactoryOptions {\n /**\n * intoss-private:// scheme URL from `ait deploy --scheme-only` (env3). The\n * phone cold-loads the candidate bundle this URL points at. SECRET-HANDLING:\n * never logged.\n */\n schemeUrl: string;\n /**\n * Project root for the `.ait_relay` secret lookup (read-only). Defaults to\n * `process.cwd()`.\n */\n projectRoot?: string;\n /**\n * Attach wait timeout in ms — how long `open()` waits for the phone to scan\n * the QR and attach. Omitted (the default) means **wait indefinitely** —\n * the runner stays up until the user stops it (Ctrl-C/SIGTERM). QR-scan\n * wait is a human-paced action; there is no sound default bound for it\n * (devtools#735). Pass an explicit value to opt into a bounded wait (CI/\n * headless callers — `--attach-timeout` on the CLI). (The per-file evaluate\n * timeout is separate, passed via the pool's `run` options.)\n */\n timeoutMs?: number;\n /** Disable browser auto-open of the QR dashboard (text QR only). */\n headless?: boolean;\n /**\n * Cell axes injected as `__AIT_CELL__` before the first test bundle runs, so\n * sdk-example's capture picks up the correct sdkLine/platform. Optional — when\n * omitted no cell is injected. The values are not secrets.\n */\n cell?: { sdkLine: string; platform: string };\n /**\n * Overrides the dashboard HTTP server's bind base port (devtools#752).\n * Threaded straight through to `startQrHttpServer`'s `dashboardPort`\n * option — see that option's doc for the increment-on-EADDRINUSE scan\n * behaviour and the `AIT_DEBUG_HTTP_PORT` env fallback. Omitted means\n * `startQrHttpServer` resolves its own default (env → fixed default).\n */\n dashboardPort?: number;\n /**\n * Receives the QR/attach render content (text chunks) from\n * `renderAndMaybeWait`, so the caller decides whether to print them — e.g.\n * suppress on non-interactive stdout.\n *\n * REQUIRED (not optional) on purpose: these chunks contain the QR payload\n * (which encodes the relay wss + TOTP `at=` code) and the attach JSON block.\n * Making the hook mandatory means the factory never falls back to printing\n * them itself — a downstream `vitest.config.ts` consumer that wires this via\n * the `@ait-co/devtools/test-runner` barrel is forced to make an explicit\n * stdout decision rather than silently leaking the secret-bearing block.\n *\n * SECRET-HANDLING: when a non-interactive caller is detected the hook MUST\n * suppress the WHOLE chunk (not just `attachUrl`), since `relayUrl` rides in\n * the same block.\n */\n onQrContent: (textChunks: string[]) => void;\n}\n\n/**\n * Builds a {@link RelayConnectionFactory} that opens a standalone env3 relay\n * connection.\n *\n * `open()` performs the full attach lifecycle and BLOCKS while a human scans\n * the rendered QR with their phone — there is no way around the manual scan\n * for env3. By default the wait is UNBOUNDED (`opts.timeoutMs` omitted): the\n * runner stays up until the user stops it (Ctrl-C/SIGTERM), since QR-scan is\n * a human-paced action with no sound default bound (devtools#735). Passing an\n * explicit `timeoutMs` opts into the old bounded behavior (CI/headless\n * callers). It resolves with the live `CdpConnection` once a matching page\n * attaches; `close()` tears the relay family down.\n *\n * The factory holds the booted relay family in a closure so `close()` can stop\n * it. A second `open()` on the same factory boots a fresh family (the previous\n * one should have been `close()`d first).\n */\nexport function createRelayConnectionFactory(\n opts: RelayConnectionFactoryOptions,\n): RelayConnectionFactory {\n const projectRoot = opts.projectRoot ?? process.cwd();\n // Undefined/absent → Infinity (wait forever). An explicit finite value opts\n // into the old bounded behavior. Number.isFinite guards downstream\n // (renderAndMaybeWait / waitForFirstTarget) treat Infinity as \"no timer\".\n const timeoutMs = opts.timeoutMs ?? Number.POSITIVE_INFINITY;\n const headless = opts.headless === true;\n\n // Captured so close() can stop the family that open() booted.\n let family: { connection: CdpConnection; stop(): void } | undefined;\n // QR HTTP server — started during open() when not headless, closed in close().\n let qrServer: QrHttpServer | undefined;\n // Session-phase state (#730) — drives the dashboard's `phase` field so the\n // CLI's run start/complete and final teardown push an immediate SSE update\n // instead of the dashboard just going dark when the process exits.\n let phase: DashboardState['phase'] = 'active';\n // Manual-blocking prompt state (devtools#741) — drives the dashboard's\n // `manualPrompt` field. `null` outside a manual step.\n let manualPrompt: DashboardState['manualPrompt'] = null;\n\n return {\n async open(): Promise<CdpConnection> {\n // Dynamic imports: keep the chii/cloudflared/debug-server graph OFF the\n // static import graph of this module (and the test-runner config barrel).\n const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import(\n '../mcp/attach-orchestrator.js'\n );\n const { injectDebugIndicator, injectGlobals } = await import('./cell.js');\n const { loadRelaySecretReadOnly } = await import('../mcp/relay-secret-store.js');\n const { bootRelayFamily, buildRelayVerifyAuth } = await import('../mcp/debug-server.js');\n\n // Load the project-local .ait_relay secret into AIT_DEBUG_TOTP_SECRET\n // BEFORE booting the relay so assertRelayAuthConfigured()/buildRelayVerifyAuth()\n // at the boot site see it. Read-only — never mints. SECRET-HANDLING: the\n // value is never logged here.\n await loadRelaySecretReadOnly({ projectRoot });\n\n // PRIMARY FIX (devtools#714): bootRelayFamily starts the cloudflared tunnel\n // as a background promise and returns immediately with tunnel.up === false.\n // We must NOT call prepareAttach until the tunnel is up — it fails fast when\n // tunnel.up is false (attach-orchestrator.ts tunnel-down guard).\n //\n // Wire onWssUrl (mirroring the MCP daemon path in debug-server.ts) to:\n // 1. resolve a caller-held tunnel-ready promise so open() can await it, and\n // 2. re-push dashboard SSE on late tunnel-up events (notifyStateChange).\n //\n // SECRET-HANDLING: onWssUrl receives the relay wss URL — never log it.\n let resolveTunnelUp!: () => void;\n const tunnelReady = new Promise<void>((resolve) => {\n resolveTunnelUp = resolve;\n });\n\n const booted = await bootRelayFamily({\n verifyAuth: buildRelayVerifyAuth(),\n onWssUrl: () => {\n // Resolve the tunnel-ready gate so the prepareAttach call below is\n // unblocked. qrServer may not be set yet at call time (it's started\n // after bootRelayFamily), so we use optional chaining — if the tunnel\n // happens to come up after qrServer is started, notifyStateChange pushes\n // the freshly minted attachUrl to any waiting dashboard SSE clients.\n // SECRET-HANDLING: wssUrl is NOT forwarded here — the value travels only\n // inside the closure via getTunnelStatus().wssUrl (used by mintAttachUrl).\n resolveTunnelUp();\n qrServer?.notifyStateChange();\n },\n // #730: parity with the MCP daemon path (debug-server.ts's onTunnelDown\n // wiring) — without this, a permanent tunnel drop during a standalone\n // CLI run left the dashboard showing a dead-but-scannable QR until the\n // watchdog (up to ~210s) finally fired.\n onTunnelDown: () => {\n qrServer?.notifyStateChange();\n },\n });\n family = booted;\n\n // If the tunnel is already up (extremely fast boot or test double), resolve\n // immediately so we don't stall on a promise that will never fire.\n if (booted.getTunnelStatus?.().up) {\n resolveTunnelUp();\n }\n\n // Track the last-captured attach parts so getDashboardState can mint a\n // fresh attach URL on every dashboard request/SSE push (fresh TOTP at=).\n // SECRET-HANDLING: parts contain the relay wss + scheme URL — never logged.\n let lastAttachParts: AttachUrlParts | undefined;\n\n // Assemble AttachDeps. We set qrHttpServer and onAttachUrlBuilt below\n // (before prepareAttach) after optionally starting the web-QR server.\n const attachDeps: AttachDeps = {\n getTunnelStatus: booted.getTunnelStatus ?? (() => ({ up: false, wssUrl: null })),\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n qrHttpServer: undefined, // filled in below if web-QR server started\n onAttachUrlBuilt: undefined, // filled in below\n canOpenBrowser: () => !headless,\n };\n\n // Web-QR server: reuse the same loopback HTTP dashboard that the MCP\n // start_attach path uses (src/mcp/qr-http-server.ts). This makes the QR\n // scannable even when stdout is non-interactive (Claude Code `!` / CI),\n // because the browser is opened by URL — not via captured stdout.\n //\n // Headless decision: we start the server even in --headless mode so the\n // printed stderr URL can be opened manually. The existing\n // canOpenBrowser: () => !headless gate inside renderAndMaybeWait prevents\n // the auto-open; headless users see only the stderr URL.\n //\n // On failure we fall back gracefully to the text-QR path — do NOT crash.\n // SECRET-HANDLING: only http://127.0.0.1:<port>/ (no secrets) goes to stderr.\n try {\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n\n const getDashboardState = (): DashboardState => ({\n tunnel: attachDeps.getTunnelStatus(),\n pages: null, // CLI/pool: no page-list introspection needed\n attachUrl: lastAttachParts ? mintAttachUrl(attachDeps, lastAttachParts) : null,\n mode: 'relay-dev' as const,\n phase, // #730 — CLI-only 'running'/'complete' transitions via onSessionPhase\n manualPrompt, // #741 — CLI-only --manual-blocking transitions via onManualPrompt\n });\n\n qrServer = await startQrHttpServer(getDashboardState, {\n dashboardPort: opts.dashboardPort,\n });\n\n // Wire the QR server into attachDeps BEFORE prepareAttach is called so\n // renderAndMaybeWait sees it and takes Path 2/3 (web-QR) instead of Path 4.\n attachDeps.qrHttpServer = qrServer;\n\n // Capture attach parts via onAttachUrlBuilt so getDashboardState can\n // mint a fresh URL (fresh TOTP at= code) on every dashboard render.\n attachDeps.onAttachUrlBuilt = (parts: AttachUrlParts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n };\n\n // Print the loopback dashboard URL to stderr — it carries no secrets\n // (TOTP codes and relay wss live only in the in-memory HTTP response).\n process.stderr.write(`devtools-test: QR dashboard: http://127.0.0.1:${qrServer.port}/\\n`);\n } catch {\n // startQrHttpServer failed (e.g. port conflict, import error). Fall back\n // to the existing text-QR path by leaving qrHttpServer: undefined.\n // qrServer remains undefined; close() handles that with optional chaining.\n }\n\n // PRIMARY FIX (devtools#714): await tunnel readiness before calling\n // prepareAttach. Race: a 15 s timeout is generous — cloudflared typically\n // comes up in < 5 s. On timeout we still call prepareAttach; it will hit\n // the tunnel-down guard and throw a secret-free error (same as before,\n // but now with a clear diagnostic instead of a silent WAITING freeze).\n //\n // Implementation: Promise.race against a 15 000 ms timeout signal. We do\n // NOT use a timer-based early-exit because the existing code already\n // surface-fails on tunnel-down inside prepareAttach with a secret-free\n // message. The timeout here is purely a \"give the tunnel a fair chance\"\n // gate — not a correctness boundary.\n const TUNNEL_BOOT_TIMEOUT_MS = 15_000;\n await Promise.race([\n tunnelReady,\n new Promise<void>((resolve) => setTimeout(resolve, TUNNEL_BOOT_TIMEOUT_MS)),\n ]);\n\n const prep = await prepareAttach(\n attachDeps,\n 'relay-dev',\n { scheme_url: opts.schemeUrl },\n booted.connection,\n );\n if (!prep.ok) {\n booted.stop();\n family = undefined;\n // SECONDARY FIX (devtools#714): close the QR server on the failure path\n // so the loopback port listener does not leak. The normal-exit path is\n // handled by close(); this mirrors that cleanup for the error path.\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING: do NOT surface `prep.error.content` in the thrown\n // message. Some prep error paths build their text from attach\n // components, and the CLI catch writes `e.message` to stderr — embedding\n // that text risks leaking the scheme/relay wss URL. The detailed\n // diagnostic is the daemon/dashboard's job; the factory throws a\n // secret-free message only.\n throw new Error(\n 'createRelayConnectionFactory: attach preparation failed — check the scheme_url and that the relay tunnel is up',\n );\n }\n\n // Render the QR + wait for the phone to attach. SECRET-HANDLING: this\n // function never logs scheme/wss/TOTP values.\n const waitResult = await renderAndMaybeWait(\n attachDeps,\n prep,\n true,\n timeoutMs,\n booted.connection,\n );\n if (waitResult.isError) {\n booted.stop();\n family = undefined;\n // SECONDARY FIX (devtools#714): close the QR server on the timeout path\n // (mirrors the prep.ok failure path above).\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING (BLOCKER fix): `waitResult.content` is the timeout\n // result built from `buildTimeoutError(baseText, ...)`, and `baseText`\n // is `JSON.stringify({ attachUrl, relayUrl, ... })` — `attachUrl` carries\n // the TOTP `at=` code and `relayUrl` is the relay `wss://` URL. The CLI\n // catch writes `e.message` to stderr, so extracting that text here would\n // leak the relay wss + TOTP code on every timeout. Throw a secret-free\n // message with only the timeout duration.\n const timeoutSec = Math.round(timeoutMs / 1000);\n throw new Error(\n `createRelayConnectionFactory: attach timed out after ${timeoutSec}s — phone did not scan the QR within the timeout`,\n );\n }\n\n // Surface the QR/attach render content to the caller, which owns the\n // stdout decision (suppress on non-interactive stdout). There is no\n // fallback that prints here itself: `onQrContent` is a required option so\n // the secret-bearing block (attachUrl TOTP + relayUrl wss) can never be\n // emitted without an explicit caller decision. SECRET-HANDLING.\n const qrChunks = waitResult.content\n .filter((c): c is { type: 'text'; text: string } => c.type === 'text')\n .map((c) => c.text);\n opts.onQrContent(qrChunks);\n\n // PAGE-READY GATE (devtools#720, fix (a)+(b)):\n //\n // FIX (a) — ORDER: enableDomains() MUST run before injectDebugIndicator()\n // and injectGlobals(). Both inject calls use Runtime.evaluate via\n // sendCommand(), which guards with `if (!this.ws) reject(\"Call\n // enableDomains() first\")`. With the old ordering (inject → inject →\n // enableDomains) injectDebugIndicator's throw was swallowed by cell.ts\n // try/catch but injectGlobals (no try/catch) propagated fatally — when\n // --cell was set open() hard-threw before enableDomains ever ran and the\n // CLI exited with 0 files executed.\n //\n // FIX (b) — BOUNDED RETRY: the window between waitForFirstTarget (non-\n // empty /targets) and enableDomains (page-level WS open) is where a\n // Cloudflare edge idle-drop can disconnect the phone. enableDomains()\n // calls refreshTargets() internally; if the target list is empty at that\n // point it throws \"No mini-app page attached\". We absorb up to\n // PAGE_READY_RETRIES transient failures by waiting briefly and retrying\n // the refreshTargets+enableDomains sequence. enableDomains() is\n // idempotent (concurrent callers share the in-flight promise), so retries\n // are safe. Only after all retries fail do we surface a secret-free\n // error. This is the CLI equivalent of the MCP daemon's soft-fail\n // cushion in relay-worker.ts.\n //\n // Neither fix touches chii-connection.ts or the MCP daemon path.\n const PAGE_READY_RETRIES = 3;\n const PAGE_READY_RETRY_DELAY_MS = 1_500;\n\n let lastEnableError: Error | undefined;\n for (let attempt = 1; attempt <= PAGE_READY_RETRIES; attempt++) {\n try {\n // FIX (a): enableDomains first — opens the page-level CDP websocket.\n await booted.connection.enableDomains();\n lastEnableError = undefined;\n break;\n } catch (err) {\n lastEnableError = err instanceof Error ? err : new Error(String(err));\n if (attempt < PAGE_READY_RETRIES) {\n // FIX (b): brief pause then re-poll /targets before retrying\n // enableDomains. Use a non-leaking approach: wait, then fall\n // through to the next loop iteration.\n await new Promise<void>((resolve) => setTimeout(resolve, PAGE_READY_RETRY_DELAY_MS));\n }\n }\n }\n\n if (lastEnableError !== undefined) {\n booted.stop();\n family = undefined;\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING: message contains only a duration + attempt count.\n // No relay wss URL, scheme URL, or TOTP code is included.\n throw new Error(\n `createRelayConnectionFactory: page did not become ready after ${PAGE_READY_RETRIES} attempts (${Math.round((PAGE_READY_RETRIES * PAGE_READY_RETRY_DELAY_MS) / 1000)}s) — the mini-app page may have disconnected before enableDomains() could open the CDP websocket`,\n );\n }\n\n // FIX (a): inject AFTER enableDomains so the page-level CDP websocket is\n // open and sendCommand() will not hit the ws===null guard.\n\n // Show the on-phone \"Debugger Connected\" badge. injectDebugIndicator\n // swallows its own errors (cell.ts try/catch), so a badge failure is\n // non-fatal — test execution proceeds regardless.\n await injectDebugIndicator(booted.connection);\n\n // Inject the cell globals before any test bundle runs (session-global).\n // injectGlobals() does NOT swallow errors — a genuine type/eval failure\n // here surfaces clearly instead of silently skipping cell injection.\n if (opts.cell !== undefined) {\n await injectGlobals(booted.connection, { __AIT_CELL__: opts.cell });\n }\n\n return booted.connection;\n },\n\n // #730: drives the dashboard's `phase` field so the CLI's run start/end\n // push an immediate SSE update instead of the dashboard just going dark.\n onSessionPhase(next: 'running' | 'complete'): void {\n phase = next;\n qrServer?.notifyStateChange();\n },\n\n // #741: drives the dashboard's `manualPrompt` field for --manual-blocking\n // — the CLI calls this immediately before injecting each manual file\n // (prompt set) and once more with `null` after the last one (prompt\n // cleared). Push is immediate (not waited on for ack) — v1 does not block\n // on dashboard acknowledgment, only on the file's own (long) evaluate.\n onManualPrompt(next: { file: string; index: number; total: number } | null): void {\n manualPrompt = next;\n qrServer?.notifyStateChange();\n },\n\n async close(connection: CdpConnection): Promise<void> {\n // #730: flip the on-phone badge to disconnected AND push the terminal\n // dashboard frame BEFORE tearing anything down, so neither surface goes\n // dark without explanation when the CLI exits (dog-food gaps #1 + #2).\n // Both are best-effort over the still-open channels — the ordering\n // (before family.stop()/qrServer.close()) is load-bearing:\n // - qrServer.notifyStateChange() writes synchronously to the still-open\n // SSE sockets, so the 'complete' frame is on the wire before the HTTP\n // server is closed below.\n // - injectDebugIndicator runs over the still-open CDP channel, so the\n // phone actually receives the disconnected-state update before the\n // relay/tunnel are torn down.\n if (phase !== 'complete') {\n phase = 'complete';\n qrServer?.notifyStateChange();\n }\n try {\n const { injectDebugIndicator } = await import('./cell.js');\n await injectDebugIndicator(connection, { state: 'disconnected' });\n } catch {\n // Channel may already be down (e.g. attach never completed) — non-fatal,\n // the badge is informational UI only.\n }\n // family.stop() is synchronous best-effort: closes the CDP connection and\n // shuts down the relay + cloudflared child.\n family?.stop();\n family = undefined;\n // Close the web-QR HTTP server if one was started during open().\n // close() is idempotent via optional chaining + reassignment to undefined.\n await qrServer?.close();\n qrServer = undefined;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as RelayRunReport, i as RelayRunOptions, n as FileResult, o as flattenResults, r as MANUAL_FILE_TIMEOUT_MS, s as runTestFilesOverRelay, t as EVALUATE_TIMEOUT_MARKER } from "../relay-worker-
|
|
1
|
+
import { a as RelayRunReport, i as RelayRunOptions, n as FileResult, o as flattenResults, r as MANUAL_FILE_TIMEOUT_MS, s as runTestFilesOverRelay, t as EVALUATE_TIMEOUT_MARKER } from "../relay-worker-Dw3aIzIK.js";
|
|
2
2
|
export { EVALUATE_TIMEOUT_MARKER, FileResult, MANUAL_FILE_TIMEOUT_MS, RelayRunOptions, RelayRunReport, flattenResults, runTestFilesOverRelay };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as runTestFilesOverRelay, n as MANUAL_FILE_TIMEOUT_MS, r as flattenResults, t as EVALUATE_TIMEOUT_MARKER } from "../relay-worker-
|
|
1
|
+
import { i as runTestFilesOverRelay, n as MANUAL_FILE_TIMEOUT_MS, r as flattenResults, t as EVALUATE_TIMEOUT_MARKER } from "../relay-worker-UK0nBfDX.js";
|
|
2
2
|
export { EVALUATE_TIMEOUT_MARKER, MANUAL_FILE_TIMEOUT_MS, flattenResults, runTestFilesOverRelay };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as AitCaptureLine } from "../capture-ltuV0gZa.js";
|
|
2
|
-
import {
|
|
3
|
-
import { a as RelayRunReport } from "../relay-worker-
|
|
2
|
+
import { r as TestResult } from "../runtime-DfHHZms2.js";
|
|
3
|
+
import { a as RelayRunReport } from "../relay-worker-Dw3aIzIK.js";
|
|
4
4
|
|
|
5
5
|
//#region src/test-runner/report.d.ts
|
|
6
6
|
/** The cell axes a report is stamped with — the test-matrix coordinates. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as CdpConnection } from "../cdp-connection-
|
|
2
|
-
import { t as RunReport } from "../runtime-
|
|
1
|
+
import { t as CdpConnection } from "../cdp-connection-BYE9meXe.js";
|
|
2
|
+
import { t as RunReport } from "../runtime-DfHHZms2.js";
|
|
3
3
|
|
|
4
4
|
//#region src/test-runner/rpc.d.ts
|
|
5
5
|
/**
|
|
@@ -38,12 +38,15 @@ declare function parseRunTestsResult(rawValue: unknown): RpcRunResult;
|
|
|
38
38
|
* Injects `bundleCode` into the attached page and awaits test execution.
|
|
39
39
|
*
|
|
40
40
|
* Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the
|
|
41
|
-
* async IIFE to settle.
|
|
42
|
-
*
|
|
41
|
+
* async IIFE to settle. `timeoutMs` (default 60s) is the authoritative file
|
|
42
|
+
* timeout — this function races it against the evaluate call AND (devtools#747)
|
|
43
|
+
* threads it down to the underlying `CdpConnection`'s own per-command watchdog
|
|
44
|
+
* (plus a small margin) so that watchdog can never undercut this race. Split
|
|
45
|
+
* into smaller files if you hit the timeout.
|
|
43
46
|
*
|
|
44
47
|
* @param connection - Active CDP connection (relay or local).
|
|
45
48
|
* @param bundleCode - IIFE bundle string from `bundleTestFile`.
|
|
46
|
-
* @param timeoutMs - Override the default
|
|
49
|
+
* @param timeoutMs - Override the default 60 s timeout.
|
|
47
50
|
*
|
|
48
51
|
* SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.
|
|
49
52
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc.d.ts","names":[],"sources":["../../src/test-runner/rpc.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","names":[],"sources":["../../src/test-runner/rpc.ts"],"mappings":";;;;;;;;;;;;;AA4EA;;;iBAvCgB,uBAAA,CAAwB,UAAA;;AAoFxC;;KAvDY,YAAA;EAAiB,EAAA;EAAU,MAAA,EAAQ,SAAA;AAAA;EAAgB,EAAA;EAAW,KAAA;AAAA;;;;;;;;;iBAU1D,mBAAA,CAAoB,QAAA,YAAoB,YAAA;;;;;;;;;;;;;;;;;iBA6ClC,kBAAA,CACpB,UAAA,EAAY,aAAA,EACZ,UAAA,UACA,SAAA,YACC,OAAA,CAAQ,YAAA"}
|
package/dist/test-runner/rpc.js
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
/** Maximum milliseconds to wait for a single evaluate round-trip. */
|
|
3
3
|
const DEFAULT_TIMEOUT_MS = 6e4;
|
|
4
4
|
/**
|
|
5
|
+
* Extra headroom (ms) added on top of the rpc-level `timeoutMs` when telling
|
|
6
|
+
* the underlying `CdpConnection` its per-command watchdog budget (devtools#747).
|
|
7
|
+
* The rpc-level race above is the authoritative file timeout — this margin
|
|
8
|
+
* only ensures the connection's own (shorter-by-default) watchdog never fires
|
|
9
|
+
* first and masks the rpc-level timeout error with a connection-level one.
|
|
10
|
+
*/
|
|
11
|
+
const CONNECTION_TIMEOUT_MARGIN_MS = 5e3;
|
|
12
|
+
/**
|
|
5
13
|
* Wraps bundle code in a self-executing IIFE that:
|
|
6
14
|
* 1. Evaluates the bundle (registering describe/it/test).
|
|
7
15
|
* 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.
|
|
@@ -48,12 +56,15 @@ function parseRunTestsResult(rawValue) {
|
|
|
48
56
|
* Injects `bundleCode` into the attached page and awaits test execution.
|
|
49
57
|
*
|
|
50
58
|
* Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the
|
|
51
|
-
* async IIFE to settle.
|
|
52
|
-
*
|
|
59
|
+
* async IIFE to settle. `timeoutMs` (default 60s) is the authoritative file
|
|
60
|
+
* timeout — this function races it against the evaluate call AND (devtools#747)
|
|
61
|
+
* threads it down to the underlying `CdpConnection`'s own per-command watchdog
|
|
62
|
+
* (plus a small margin) so that watchdog can never undercut this race. Split
|
|
63
|
+
* into smaller files if you hit the timeout.
|
|
53
64
|
*
|
|
54
65
|
* @param connection - Active CDP connection (relay or local).
|
|
55
66
|
* @param bundleCode - IIFE bundle string from `bundleTestFile`.
|
|
56
|
-
* @param timeoutMs - Override the default
|
|
67
|
+
* @param timeoutMs - Override the default 60 s timeout.
|
|
57
68
|
*
|
|
58
69
|
* SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.
|
|
59
70
|
*/
|
|
@@ -65,7 +76,7 @@ async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TI
|
|
|
65
76
|
expression,
|
|
66
77
|
returnByValue: true,
|
|
67
78
|
awaitPromise: true
|
|
68
|
-
});
|
|
79
|
+
}, { timeoutMs: timeoutMs + CONNECTION_TIMEOUT_MARGIN_MS });
|
|
69
80
|
const raceResult = await Promise.race([evalPromise.then((v) => ({
|
|
70
81
|
tag: "eval",
|
|
71
82
|
v
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc.js","names":[],"sources":["../../src/test-runner/rpc.ts"],"sourcesContent":["/**\n * Node-side RPC helpers for injecting and collecting test execution over CDP.\n *\n * Uses the same IIFE + JSON.stringify envelope pattern as `buildCallSdkExpression`\n * in `src/mcp/tools.ts` to reliably shuttle structured results through\n * `Runtime.evaluate`'s `returnByValue: true` boundary.\n *\n * SECRET-HANDLING: bundle code, relay URLs, and result values are NOT logged.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { RunReport } from './runtime.js';\n\n/** Maximum milliseconds to wait for a single evaluate round-trip. */\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\n/**\n * Wraps bundle code in a self-executing IIFE that:\n * 1. Evaluates the bundle (registering describe/it/test).\n * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.\n * 3. Returns a JSON-serialised `RunReport` string.\n *\n * The double-serialisation (RunReport → JSON string → returnByValue string)\n * is intentional: CDP `returnByValue` reliably transports strings; deeply\n * nested objects can lose fidelity across the Chii relay.\n *\n * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.\n */\nexport function buildRunTestsExpression(bundleCode: string): string {\n // We trust bundleCode is already a self-contained IIFE that installs\n // `window.__testBundle` (or `globalThis.__testBundle`).\n // We then call `__testBundle.runTestModule()` and return a JSON string.\n return (\n `(async () => {` +\n // Step 1: evaluate the bundle to register tests\n ` try { ${bundleCode} } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)});` +\n ` }` +\n // Step 2: check that the expected exports are present\n ` if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'});` +\n ` }` +\n // Step 3: run tests — pass the factory so runTestModule installs globals\n // first, then invokes the factory to register describe/it/test blocks.\n ` try {` +\n ` const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory);` +\n ` return JSON.stringify({ok:true,value:report});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Result of `injectAndRunBundle`.\n */\nexport type RpcRunResult = { ok: true; report: RunReport } | { ok: false; error: string };\n\n/**\n * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`\n * evaluate call into a typed `RpcRunResult`.\n *\n * Throws only on parse failure — an `ok:false` envelope is a normal result.\n *\n * SECRET-HANDLING: `rawValue` is not included in error messages.\n */\nexport function parseRunTestsResult(rawValue: unknown): RpcRunResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `rpc.parseRunTestsResult: unexpected return type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue — could contain secrets.\n throw new Error('rpc.parseRunTestsResult: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('rpc.parseRunTestsResult: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, report: obj.value as RunReport };\n }\n if (obj.ok === false) {\n return {\n ok: false,\n error: typeof obj.error === 'string' ? obj.error : String(obj.error),\n };\n }\n throw new Error('rpc.parseRunTestsResult: result missing \"ok\" field');\n}\n\n/**\n * Injects `bundleCode` into the attached page and awaits test execution.\n *\n * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the\n * async IIFE to settle.
|
|
1
|
+
{"version":3,"file":"rpc.js","names":[],"sources":["../../src/test-runner/rpc.ts"],"sourcesContent":["/**\n * Node-side RPC helpers for injecting and collecting test execution over CDP.\n *\n * Uses the same IIFE + JSON.stringify envelope pattern as `buildCallSdkExpression`\n * in `src/mcp/tools.ts` to reliably shuttle structured results through\n * `Runtime.evaluate`'s `returnByValue: true` boundary.\n *\n * SECRET-HANDLING: bundle code, relay URLs, and result values are NOT logged.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { RunReport } from './runtime.js';\n\n/** Maximum milliseconds to wait for a single evaluate round-trip. */\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\n/**\n * Extra headroom (ms) added on top of the rpc-level `timeoutMs` when telling\n * the underlying `CdpConnection` its per-command watchdog budget (devtools#747).\n * The rpc-level race above is the authoritative file timeout — this margin\n * only ensures the connection's own (shorter-by-default) watchdog never fires\n * first and masks the rpc-level timeout error with a connection-level one.\n */\nconst CONNECTION_TIMEOUT_MARGIN_MS = 5_000;\n\n/**\n * Wraps bundle code in a self-executing IIFE that:\n * 1. Evaluates the bundle (registering describe/it/test).\n * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.\n * 3. Returns a JSON-serialised `RunReport` string.\n *\n * The double-serialisation (RunReport → JSON string → returnByValue string)\n * is intentional: CDP `returnByValue` reliably transports strings; deeply\n * nested objects can lose fidelity across the Chii relay.\n *\n * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.\n */\nexport function buildRunTestsExpression(bundleCode: string): string {\n // We trust bundleCode is already a self-contained IIFE that installs\n // `window.__testBundle` (or `globalThis.__testBundle`).\n // We then call `__testBundle.runTestModule()` and return a JSON string.\n return (\n `(async () => {` +\n // Step 1: evaluate the bundle to register tests\n ` try { ${bundleCode} } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)});` +\n ` }` +\n // Step 2: check that the expected exports are present\n ` if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'});` +\n ` }` +\n // Step 3: run tests — pass the factory so runTestModule installs globals\n // first, then invokes the factory to register describe/it/test blocks.\n ` try {` +\n ` const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory);` +\n ` return JSON.stringify({ok:true,value:report});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Result of `injectAndRunBundle`.\n */\nexport type RpcRunResult = { ok: true; report: RunReport } | { ok: false; error: string };\n\n/**\n * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`\n * evaluate call into a typed `RpcRunResult`.\n *\n * Throws only on parse failure — an `ok:false` envelope is a normal result.\n *\n * SECRET-HANDLING: `rawValue` is not included in error messages.\n */\nexport function parseRunTestsResult(rawValue: unknown): RpcRunResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `rpc.parseRunTestsResult: unexpected return type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue — could contain secrets.\n throw new Error('rpc.parseRunTestsResult: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('rpc.parseRunTestsResult: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, report: obj.value as RunReport };\n }\n if (obj.ok === false) {\n return {\n ok: false,\n error: typeof obj.error === 'string' ? obj.error : String(obj.error),\n };\n }\n throw new Error('rpc.parseRunTestsResult: result missing \"ok\" field');\n}\n\n/**\n * Injects `bundleCode` into the attached page and awaits test execution.\n *\n * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the\n * async IIFE to settle. `timeoutMs` (default 60s) is the authoritative file\n * timeout — this function races it against the evaluate call AND (devtools#747)\n * threads it down to the underlying `CdpConnection`'s own per-command watchdog\n * (plus a small margin) so that watchdog can never undercut this race. Split\n * into smaller files if you hit the timeout.\n *\n * @param connection - Active CDP connection (relay or local).\n * @param bundleCode - IIFE bundle string from `bundleTestFile`.\n * @param timeoutMs - Override the default 60 s timeout.\n *\n * SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.\n */\nexport async function injectAndRunBundle(\n connection: CdpConnection,\n bundleCode: string,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n): Promise<RpcRunResult> {\n const expression = buildRunTestsExpression(bundleCode);\n\n // Use a tagged race so we can distinguish \"timeout won\" from \"evaluate won\"\n // without relying on exception identity. The timeout arm RETURNS a typed\n // RpcRunResult (ok:false + EVALUATE_TIMEOUT_MARKER) so the caller (relay-\n // worker's attempt()) can detect it via `.error.includes(EVALUATE_TIMEOUT_MARKER)`\n // and trigger the one-retry path.\n //\n // Genuine CDP `exceptionDetails` (engine exceptions thrown inside the page)\n // still THROW — those are deterministic failures that a retry cannot fix and\n // relay-worker's catch block finalises them correctly.\n //\n // DO NOT change the literal string below — relay-worker.ts imports\n // EVALUATE_TIMEOUT_MARKER = 'rpc: evaluate timed out after' and uses\n // `.includes()` to recognise this exact prefix.\n const TIMEOUT_SENTINEL = Symbol('timeout');\n const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) =>\n setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs),\n );\n\n // devtools#747: thread our own timeout budget down to the connection's\n // per-command watchdog (ChiiCdpConnection's default is 30s and used to fire\n // before this rpc-level race no matter how large `timeoutMs` was). A small\n // margin keeps the connection watchdog from firing a beat before our own\n // race settles — the rpc-level race above remains the authoritative file\n // timeout; this override only ensures the connection layer never undercuts\n // it. `LocalCdpConnection` ignores the option (no watchdog of its own).\n const evalPromise = connection.send(\n 'Runtime.evaluate',\n {\n expression,\n returnByValue: true,\n awaitPromise: true,\n },\n { timeoutMs: timeoutMs + CONNECTION_TIMEOUT_MARGIN_MS },\n );\n\n const raceResult = await Promise.race([\n evalPromise.then((v) => ({ tag: 'eval' as const, v })),\n timeoutPromise.then(() => ({ tag: 'timeout' as const })),\n ]);\n\n if (raceResult.tag === 'timeout') {\n return { ok: false, error: `rpc: evaluate timed out after ${timeoutMs}ms` };\n }\n\n const cdpResult = raceResult.v;\n\n if (cdpResult.exceptionDetails) {\n // Surface only the engine error string — not the expression or value.\n const msg =\n cdpResult.exceptionDetails.exception?.description ??\n cdpResult.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`rpc.injectAndRunBundle: ${msg}`);\n }\n\n return parseRunTestsResult(cdpResult.result.value);\n}\n"],"mappings":";;AAcA,MAAM,qBAAqB;;;;;;;;AAS3B,MAAM,+BAA+B;;;;;;;;;;;;;AAcrC,SAAgB,wBAAwB,YAA4B;AAIlE,QACE,yBAEW,WAAW;;;;;;;;;;AAgC1B,SAAgB,oBAAoB,UAAiC;AACnE,KAAI,OAAO,aAAa,SACtB,OAAM,IAAI,MACR,oDAAoD,OAAO,SAAS,0BACrE;CAEH,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,SAAS;SACvB;AAEN,QAAM,IAAI,MAAM,2DAA2D;;AAE7E,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MAAM,0DAA0D;CAE5E,MAAM,MAAM;AACZ,KAAI,IAAI,OAAO,KACb,QAAO;EAAE,IAAI;EAAM,QAAQ,IAAI;EAAoB;AAErD,KAAI,IAAI,OAAO,MACb,QAAO;EACL,IAAI;EACJ,OAAO,OAAO,IAAI,UAAU,WAAW,IAAI,QAAQ,OAAO,IAAI,MAAM;EACrE;AAEH,OAAM,IAAI,MAAM,uDAAqD;;;;;;;;;;;;;;;;;;AAmBvE,eAAsB,mBACpB,YACA,YACA,YAAY,oBACW;CACvB,MAAM,aAAa,wBAAwB,WAAW;CAetD,MAAM,mBAAmB,OAAO,UAAU;CAC1C,MAAM,iBAAiB,IAAI,SAAkC,YAC3D,iBAAiB,QAAQ,iBAAiB,EAAE,UAAU,CACvD;CASD,MAAM,cAAc,WAAW,KAC7B,oBACA;EACE;EACA,eAAe;EACf,cAAc;EACf,EACD,EAAE,WAAW,YAAY,8BAA8B,CACxD;CAED,MAAM,aAAa,MAAM,QAAQ,KAAK,CACpC,YAAY,MAAM,OAAO;EAAE,KAAK;EAAiB;EAAG,EAAE,EACtD,eAAe,YAAY,EAAE,KAAK,WAAoB,EAAE,CACzD,CAAC;AAEF,KAAI,WAAW,QAAQ,UACrB,QAAO;EAAE,IAAI;EAAO,OAAO,iCAAiC,UAAU;EAAK;CAG7E,MAAM,YAAY,WAAW;AAE7B,KAAI,UAAU,kBAAkB;EAE9B,MAAM,MACJ,UAAU,iBAAiB,WAAW,eACtC,UAAU,iBAAiB,QAC3B;AACF,QAAM,IAAI,MAAM,2BAA2B,MAAM;;AAGnD,QAAO,oBAAoB,UAAU,OAAO,MAAM"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { RunReport, TestResult, runTestModule, runtimeGlobals };
|
|
1
|
+
import { a as runtimeGlobals, i as runTestModule, n as TestContext, r as TestResult, t as RunReport } from "../runtime-DfHHZms2.js";
|
|
2
|
+
export { RunReport, TestContext, TestResult, runTestModule, runtimeGlobals };
|
|
@@ -259,6 +259,24 @@ expect.objectContaining = makeObjectContaining;
|
|
|
259
259
|
expect.arrayContaining = makeArrayContaining;
|
|
260
260
|
expect.stringContaining = makeStringContaining;
|
|
261
261
|
expect.stringMatching = makeStringMatching;
|
|
262
|
+
/**
|
|
263
|
+
* Internal sentinel thrown by `ctx.skip()` to unwind the test body early.
|
|
264
|
+
* Caught by the run loop (never surfaced as a failure) — see `runTestModule`'s
|
|
265
|
+
* test-execution `catch` branch.
|
|
266
|
+
*/
|
|
267
|
+
var InPageSkipSentinel = class {
|
|
268
|
+
constructor(note) {
|
|
269
|
+
this.note = note;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
function _makeTestContext(name) {
|
|
273
|
+
return {
|
|
274
|
+
skip(condition, note) {
|
|
275
|
+
if (condition === void 0 || condition) throw new InPageSkipSentinel(note);
|
|
276
|
+
},
|
|
277
|
+
task: { name }
|
|
278
|
+
};
|
|
279
|
+
}
|
|
262
280
|
const _pendingTests = [];
|
|
263
281
|
const _suiteStack = [];
|
|
264
282
|
/** Registers a suite scope; calls `fn` synchronously to collect inner tests. */
|
|
@@ -512,15 +530,23 @@ async function runTestModule(moduleFactory) {
|
|
|
512
530
|
const beErr = await _runHooks(_hooksFor("beforeEach", pending.suitePath));
|
|
513
531
|
const tStart = Date.now();
|
|
514
532
|
let testErr;
|
|
533
|
+
let inPageSkip;
|
|
515
534
|
if (beErr) testErr = `beforeEach failed: ${beErr.message}`;
|
|
516
535
|
else try {
|
|
517
|
-
await pending.fn();
|
|
536
|
+
await pending.fn(_makeTestContext(fullName));
|
|
518
537
|
} catch (e) {
|
|
519
|
-
|
|
538
|
+
if (e instanceof InPageSkipSentinel) inPageSkip = e;
|
|
539
|
+
else testErr = e instanceof Error ? e.message : String(e);
|
|
520
540
|
}
|
|
521
541
|
const aeErr = await _runHooks(_hooksFor("afterEach", pending.suitePath));
|
|
522
|
-
if (aeErr && !testErr) testErr = `afterEach failed: ${aeErr.message}`;
|
|
523
|
-
if (
|
|
542
|
+
if (aeErr && !testErr && !inPageSkip) testErr = `afterEach failed: ${aeErr.message}`;
|
|
543
|
+
if (inPageSkip) results.push({
|
|
544
|
+
name: fullName,
|
|
545
|
+
status: "skip",
|
|
546
|
+
duration: Date.now() - tStart,
|
|
547
|
+
...inPageSkip.note !== void 0 ? { note: inPageSkip.note } : {}
|
|
548
|
+
});
|
|
549
|
+
else if (testErr !== void 0) results.push({
|
|
524
550
|
name: fullName,
|
|
525
551
|
status: "fail",
|
|
526
552
|
duration: Date.now() - tStart,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","names":["#received","#negated","#assert"],"sources":["../../src/test-runner/runtime.ts"],"sourcesContent":["/**\n * Thin test runtime for WebView execution.\n *\n * This file is bundled by `bundle.ts` together with the user's test file\n * into a single IIFE injected into the WebView via `Runtime.evaluate`.\n * It MUST stay browser-compatible — no Node.js APIs allowed.\n *\n * Design: rather than shipping @vitest/runner verbatim into a WebView (where\n * its Node-side internals cause issues), this runtime provides a minimal\n * compatible API surface — describe/it/test/expect globals — collects results\n * into a plain JSON-safe object, and exports `runTestModule` as the entry point.\n *\n * @vitest/runner and @vitest/expect are listed as dependencies so that the\n * package's type contracts are available and so the browser-compatible subsets\n * can be referenced. The Vitest custom pool that drives this runtime through\n * Vitest's `PoolRunnerInitializer` lives in `pool.ts`.\n *\n * NOTE: this file is imported by type from Node-side code (rpc.ts / relay-worker.ts)\n * for the RunReport / TestResult type shapes. The runtime ITSELF is not imported\n * at runtime on the Node side — only the types are used.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Public result types (used by rpc.ts and relay-worker.ts) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result of a single test case.\n * All fields are JSON-serialisable.\n */\nexport interface TestResult {\n /** Full dot-joined test name including nested suite names. */\n name: string;\n /** `'pass'` or `'fail'`. `'skip'` for skipped tests. */\n status: 'pass' | 'fail' | 'skip';\n /** Duration in milliseconds. */\n duration: number;\n /** Error message (fail only). Does NOT include the expression/secret. */\n error?: string;\n}\n\n/** Aggregate report returned by `runTestModule`. */\nexport interface RunReport {\n /** ISO timestamp of when `runTestModule` was called. */\n startedAt: string;\n /** Total elapsed milliseconds (wall-clock). */\n duration: number;\n passed: number;\n failed: number;\n skipped: number;\n tests: TestResult[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Asymmetric matchers (devtools#692) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Brand property that tags asymmetric matcher markers (`expect.any(String)`,\n * `expect.objectContaining(...)`, etc). A reserved string key is used as the\n * brand so the guard stays a plain own-property check — markers are only ever\n * compared in-process by reference, so no cross-realm Symbol coordination is\n * needed.\n */\nconst ASYMMETRIC_BRAND = '$$asymmetricMatch' as const;\n\n/**\n * An asymmetric matcher: a placeholder that, when found on the `expected` side\n * of a deep comparison, runs its own `asymmetricMatch(actual)` predicate\n * instead of being structurally compared as a plain object. This is what makes\n * `toMatchObject({ hash: expect.any(String) })` match any string `hash`.\n */\ninterface AsymmetricMatcher {\n /** Brand discriminator — present only on asymmetric markers. */\n [ASYMMETRIC_BRAND]: true;\n /** Human-readable label used in failure messages. */\n toString(): string;\n /** Returns true when `actual` satisfies this matcher. */\n asymmetricMatch(actual: unknown): boolean;\n}\n\n/** Narrowing guard: is `v` an asymmetric matcher marker? */\nfunction isAsymmetric(v: unknown): v is AsymmetricMatcher {\n return (\n typeof v === 'object' &&\n v !== null &&\n (v as Record<string, unknown>)[ASYMMETRIC_BRAND] === true &&\n typeof (v as { asymmetricMatch?: unknown }).asymmetricMatch === 'function'\n );\n}\n\n/** The widest constructor type TypeScript allows without `any` (see toBeInstanceOf). */\ntype AnyCtor = abstract new (...args: never) => unknown;\n\n/**\n * `expect.any(Ctor)` — matches a value by its constructor. Primitive wrappers\n * (`String`/`Number`/`Boolean`) are matched by `typeof` (so the unboxed\n * primitive matches) AND by `instanceof` (so a boxed wrapper instance also\n * matches). `BigInt`/`Symbol` match by `typeof`. `Object` matches any non-null\n * object (NOT functions, matching vitest), `Array` uses `Array.isArray`,\n * `Function` uses `typeof === 'function'`, and any other constructor (user\n * classes) falls back to `instanceof`.\n */\nfunction makeAny(ctor: AnyCtor): AsymmetricMatcher {\n const name = (ctor as { name?: string }).name ?? String(ctor);\n const ctorAsFn = ctor as unknown as (new (...args: unknown[]) => unknown) | undefined;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `Any<${name}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (actual === null || actual === undefined) return false;\n switch (ctor as unknown) {\n case String:\n return typeof actual === 'string' || actual instanceof String;\n case Number:\n return typeof actual === 'number' || actual instanceof Number;\n case Boolean:\n return typeof actual === 'boolean' || actual instanceof Boolean;\n case BigInt:\n return typeof actual === 'bigint';\n case Symbol:\n return typeof actual === 'symbol';\n case Function:\n return typeof actual === 'function';\n case Object:\n // Match vitest: `expect.any(Object)` accepts objects only, NOT\n // functions (vitest checks `typeof other === 'object'`). `null` is\n // already excluded by the guard above.\n return typeof actual === 'object';\n case Array:\n return Array.isArray(actual);\n default:\n return ctorAsFn ? actual instanceof ctorAsFn : false;\n }\n },\n };\n}\n\n/** `expect.anything()` — matches any value except `null`/`undefined`. */\nfunction makeAnything(): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => 'Anything',\n asymmetricMatch: (actual: unknown): boolean => actual !== null && actual !== undefined,\n };\n}\n\n/** `expect.objectContaining(obj)` — partial-match every key of `obj` on `actual`. */\nfunction makeObjectContaining(expected: Record<string, unknown>): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ObjectContaining<${safeStringify(expected)}>`,\n asymmetricMatch: (actual: unknown): boolean => deepMatchObject(actual, expected),\n };\n}\n\n/** `expect.arrayContaining(arr)` — `actual` is an array containing each element of `arr`. */\nfunction makeArrayContaining(expected: unknown[]): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ArrayContaining<${safeStringify(expected)}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (!Array.isArray(actual)) return false;\n return expected.every((exp) =>\n actual.some((act) => (isAsymmetric(exp) ? exp.asymmetricMatch(act) : deepEqual(act, exp))),\n );\n },\n };\n}\n\n/** `expect.stringContaining(sub)` — `actual` is a string containing `sub`. */\nfunction makeStringContaining(sub: string): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringContaining<${sub}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && actual.includes(sub),\n };\n}\n\n/** `expect.stringMatching(re)` — `actual` is a string matching `re` (string or RegExp). */\nfunction makeStringMatching(re: string | RegExp): AsymmetricMatcher {\n const pattern = typeof re === 'string' ? new RegExp(re) : re;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringMatching<${String(pattern)}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && pattern.test(actual),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Deep equality helpers */\n/* -------------------------------------------------------------------------- */\n\n/** JSON.stringify that never throws (cycles/BigInt) — for failure messages. */\nfunction safeStringify(v: unknown): string {\n try {\n return (\n JSON.stringify(v, (_k, val) =>\n isAsymmetric(val) ? val.toString() : typeof val === 'bigint' ? `${val}n` : val,\n ) ?? String(v)\n );\n } catch {\n return String(v);\n }\n}\n\n/**\n * Recursive structural equality — replaces JSON.stringify comparison to\n * handle key-order differences and undefined values correctly.\n *\n * When the `b` (expected) side is an asymmetric marker, its `asymmetricMatch`\n * predicate decides the result instead of structural comparison — markers are\n * placeholders, not plain objects, so deep-comparing them never matches.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (isAsymmetric(b)) return b.asymmetricMatch(a);\n if (Object.is(a, b)) return true;\n if (a === null || b === null) return false;\n if (typeof a !== 'object' || typeof b !== 'object') return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n if (Array.isArray(a) || Array.isArray(b)) return false;\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(bObj, key)) return false;\n if (!deepEqual(aObj[key], bObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Partial-match: every key in `expected` must exist in `received` with a\n * recursively matching value. Extra keys in `received` are ignored.\n *\n * As in `deepEqual`, an asymmetric marker on the `expected` side delegates to\n * its `asymmetricMatch` predicate rather than being compared structurally.\n */\nfunction deepMatchObject(received: unknown, expected: unknown): boolean {\n if (isAsymmetric(expected)) return expected.asymmetricMatch(received);\n if (Object.is(received, expected)) return true;\n if (expected === null || received === null) return Object.is(received, expected);\n if (typeof expected !== 'object' || typeof received !== 'object')\n return Object.is(received, expected);\n\n if (Array.isArray(expected) && Array.isArray(received)) {\n if (expected.length !== received.length) return false;\n for (let i = 0; i < expected.length; i++) {\n if (!deepMatchObject(received[i], expected[i])) return false;\n }\n return true;\n }\n if (Array.isArray(expected) || Array.isArray(received)) return false;\n\n const expObj = expected as Record<string, unknown>;\n const recObj = received as Record<string, unknown>;\n for (const key of Object.keys(expObj)) {\n if (!Object.hasOwn(recObj, key)) return false;\n if (!deepMatchObject(recObj[key], expObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Resolves a dot-separated property path on an object.\n * Returns `{ found: true, value }` or `{ found: false }`.\n */\nfunction resolvePath(obj: unknown, dotPath: string): { found: boolean; value?: unknown } {\n const parts = dotPath.split('.');\n let cur: unknown = obj;\n for (const part of parts) {\n if (cur === null || cur === undefined || typeof cur !== 'object') return { found: false };\n const record = cur as Record<string, unknown>;\n if (!Object.hasOwn(record, part)) return { found: false };\n cur = record[part];\n }\n return { found: true, value: cur };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Lightweight expect implementation */\n/* -------------------------------------------------------------------------- */\n\n/** Thrown by expect matchers on failure. */\nclass AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AssertionError';\n }\n}\n\n/** Minimal expect builder compatible with Vitest's jest-style API. */\nclass Expectation {\n #received: unknown;\n #negated = false;\n\n constructor(received: unknown) {\n this.#received = received;\n }\n\n get not(): this {\n const neg = new Expectation(this.#received) as this;\n neg.#negated = true;\n return neg;\n }\n\n #assert(pass: boolean, msg: string): void {\n const actual = this.#negated ? !pass : pass;\n if (!actual) {\n throw new AssertionError(this.#negated ? `Expected NOT: ${msg}` : msg);\n }\n }\n\n toBe(expected: unknown): void {\n this.#assert(\n Object.is(this.#received, expected),\n `Expected ${String(expected)}, received ${String(this.#received)}`,\n );\n }\n\n toEqual(expected: unknown): void {\n this.#assert(\n deepEqual(this.#received, expected),\n `Expected ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toBeTruthy(): void {\n this.#assert(Boolean(this.#received), `Expected truthy, received ${String(this.#received)}`);\n }\n\n toBeFalsy(): void {\n this.#assert(!this.#received, `Expected falsy, received ${String(this.#received)}`);\n }\n\n toBeNull(): void {\n this.#assert(this.#received === null, `Expected null, received ${String(this.#received)}`);\n }\n\n toBeUndefined(): void {\n this.#assert(\n this.#received === undefined,\n `Expected undefined, received ${String(this.#received)}`,\n );\n }\n\n toBeGreaterThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received > n,\n `Expected > ${n}, received ${String(this.#received)}`,\n );\n }\n\n toBeLessThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received < n,\n `Expected < ${n}, received ${String(this.#received)}`,\n );\n }\n\n toContain(sub: unknown): void {\n if (Array.isArray(this.#received)) {\n this.#assert(\n this.#received.includes(sub),\n `Expected array to contain ${JSON.stringify(sub)}, received ${JSON.stringify(this.#received)}`,\n );\n return;\n }\n this.#assert(\n typeof this.#received === 'string' && typeof sub === 'string' && this.#received.includes(sub),\n `Expected to contain \"${String(sub)}\", received \"${String(this.#received)}\"`,\n );\n }\n\n toThrow(msgFragment?: string): void {\n if (typeof this.#received !== 'function') {\n throw new AssertionError('toThrow: expected a function');\n }\n let threw = false;\n let errorMsg = '';\n try {\n (this.#received as () => unknown)();\n } catch (e) {\n threw = true;\n errorMsg = e instanceof Error ? e.message : String(e);\n }\n if (msgFragment !== undefined) {\n this.#assert(\n threw && errorMsg.includes(msgFragment),\n `Expected to throw containing \"${msgFragment}\", got \"${errorMsg}\"`,\n );\n } else {\n this.#assert(threw, 'Expected function to throw');\n }\n }\n\n // ---- New matchers (devtools#683) -----------------------------------------\n\n toMatchObject(expected: Record<string, unknown>): void {\n this.#assert(\n deepMatchObject(this.#received, expected),\n `Expected object to match ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toHaveProperty(dotPath: string, ...rest: unknown[]): void {\n const { found, value: actual } = resolvePath(this.#received, dotPath);\n if (rest.length > 0) {\n const value = rest[0];\n this.#assert(\n found && deepEqual(actual, value),\n `Expected property \"${dotPath}\" to equal ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n );\n } else {\n this.#assert(found, `Expected property \"${dotPath}\" to exist`);\n }\n }\n\n // The `abstract new (...args: never) => unknown` signature is the widest\n // constructor type that TypeScript allows without explicit `any`. Real-world\n // constructors like `ErrorConstructor` are assignable to it because `never`\n // in parameter position is contravariant (any arg list satisfies `never[]`).\n toBeInstanceOf(ctor: abstract new (...args: never) => unknown): void {\n this.#assert(\n this.#received instanceof (ctor as new (...args: unknown[]) => unknown),\n `Expected instance of ${(ctor as { name?: string }).name ?? String(ctor)}, received ${String(this.#received)}`,\n );\n }\n\n toBeTypeOf(typeStr: string): void {\n this.#assert(\n typeof this.#received === typeStr,\n `Expected typeof \"${typeStr}\", received \"${typeof this.#received}\"`,\n );\n }\n}\n\n/**\n * The `expect` callable plus its asymmetric-matcher static surface\n * (`expect.any`, `expect.objectContaining`, …). The user file accesses these as\n * `expect.any(String)`; the vitest redirect (`bundle.ts`) re-exports `expect` by\n * reading `globalThis.expect`, so attaching the statics to this function object\n * makes them reachable through the getter (a function's own properties survive\n * the getter that returns the function itself).\n */\ninterface ExpectStatic {\n (received: unknown): Expectation;\n any(ctor: AnyCtor): AsymmetricMatcher;\n anything(): AsymmetricMatcher;\n objectContaining(expected: Record<string, unknown>): AsymmetricMatcher;\n arrayContaining(expected: unknown[]): AsymmetricMatcher;\n stringContaining(sub: string): AsymmetricMatcher;\n stringMatching(re: string | RegExp): AsymmetricMatcher;\n}\n\n/** The `expect` function installed as a global. */\nconst expect: ExpectStatic = ((received: unknown): Expectation =>\n new Expectation(received)) as ExpectStatic;\n\n// Asymmetric matcher statics — vitest-compatible surface (devtools#692).\nexpect.any = makeAny;\nexpect.anything = makeAnything;\nexpect.objectContaining = makeObjectContaining;\nexpect.arrayContaining = makeArrayContaining;\nexpect.stringContaining = makeStringContaining;\nexpect.stringMatching = makeStringMatching;\n\n/* -------------------------------------------------------------------------- */\n/* describe / it / test registry */\n/* -------------------------------------------------------------------------- */\n\ninterface PendingTest {\n suitePath: string[];\n name: string;\n fn: () => void | Promise<void>;\n skip: boolean;\n}\n\nconst _pendingTests: PendingTest[] = [];\nconst _suiteStack: string[] = [];\n\n/** Registers a suite scope; calls `fn` synchronously to collect inner tests. */\nfunction describe(name: string, fn: () => void): void {\n _suiteStack.push(name);\n fn();\n _suiteStack.pop();\n}\n\n/** Registers a test. */\nfunction it(name: string, fn: () => void | Promise<void>): void {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip: false });\n}\n\n/** Alias for `it`. */\nconst test = it;\n\n/** Skipped test — registered but not executed. */\ndescribe.skip = (name: string, _fn: () => void): void => {\n void name;\n};\nit.skip = (name: string, _fn?: () => void | Promise<void>): void => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn: () => {}, skip: true });\n};\ntest.skip = it.skip;\n\n/**\n * `it.skipIf(cond)(name, fn)` / `it.runIf(cond)(name, fn)` — conditional test\n * registration. `skipIf` skips when `cond` is truthy; `runIf` runs only when\n * `cond` is truthy (skips otherwise). sdk-example uses\n * `it.skipIf(cell.platform === 'mock')(...)` to skip real-SDK-only cases in env1.\n */\ntype ItRegistrar = (name: string, fn: () => void | Promise<void>) => void;\nfunction _conditionalIt(skip: boolean): ItRegistrar {\n return (name, fn) => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip });\n };\n}\nit.skipIf = (cond: unknown): ItRegistrar => _conditionalIt(Boolean(cond));\nit.runIf = (cond: unknown): ItRegistrar => _conditionalIt(!cond);\ntest.skipIf = it.skipIf;\ntest.runIf = it.runIf;\n\n/**\n * `describe.skipIf(cond)(name, fn)` / `describe.runIf(cond)(name, fn)`.\n * When skipped, the suite body still runs to register its tests but every test\n * inside is marked skipped (collected via a temporary skip flag is overkill —\n * a skipped describe simply does not invoke its body, mirroring `describe.skip`).\n */\ntype DescribeRegistrar = (name: string, fn: () => void) => void;\ndescribe.skipIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, _fn) => void name : (name, fn) => describe(name, fn);\ndescribe.runIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, fn) => describe(name, fn) : (name, _fn) => void name;\n\n/* -------------------------------------------------------------------------- */\n/* Lifecycle hooks (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ntype HookType = 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach';\n\ninterface HookEntry {\n /** Suite path at the time of registration ([] = module scope). */\n suitePath: string[];\n type: HookType;\n fn: () => void | Promise<void>;\n}\n\nconst _hooks: HookEntry[] = [];\n\nfunction _registerHook(type: HookType, fn: () => void | Promise<void>): void {\n _hooks.push({ suitePath: [..._suiteStack], type, fn });\n}\n\n/**\n * Returns hooks whose suitePath is a prefix of `testSuitePath`\n * (i.e. the hook's scope contains the test).\n */\nfunction _hooksFor(type: HookType, testSuitePath: string[]): Array<() => void | Promise<void>> {\n return _hooks\n .filter((h) => {\n if (h.type !== type) return false;\n // Hook scope must be a prefix of the test's suite path\n if (h.suitePath.length > testSuitePath.length) return false;\n for (let i = 0; i < h.suitePath.length; i++) {\n if (h.suitePath[i] !== testSuitePath[i]) return false;\n }\n return true;\n })\n .map((h) => h.fn);\n}\n\n/** Runs an array of hook functions in order, awaiting each. */\nasync function _runHooks(fns: Array<() => void | Promise<void>>): Promise<Error | null> {\n for (const fn of fns) {\n try {\n await fn();\n } catch (e) {\n return e instanceof Error ? e : new Error(String(e));\n }\n }\n return null;\n}\n\nfunction beforeAll(fn: () => void | Promise<void>): void {\n _registerHook('beforeAll', fn);\n}\nfunction afterAll(fn: () => void | Promise<void>): void {\n _registerHook('afterAll', fn);\n}\nfunction beforeEach(fn: () => void | Promise<void>): void {\n _registerHook('beforeEach', fn);\n}\nfunction afterEach(fn: () => void | Promise<void>): void {\n _registerHook('afterEach', fn);\n}\n\n/* -------------------------------------------------------------------------- */\n/* vi shim (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ninterface SpyCall {\n args: unknown[];\n returnValue: unknown;\n}\n\ninterface MockFn<T extends unknown[], R> {\n (...args: T): R;\n mock: { calls: SpyCall[] };\n mockImplementation(fn: (...args: T) => R): this;\n mockReturnValue(value: R): this;\n mockRestore(): void;\n}\n\ninterface SpyRecord {\n obj: Record<string, unknown>;\n method: string;\n original: unknown;\n}\n\nconst _spyRegistry: SpyRecord[] = [];\n\nfunction _createMockFn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n let currentImpl: ((...args: T) => R) | undefined = impl;\n const calls: SpyCall[] = [];\n\n const mockFn = ((...args: T): R => {\n const returnValue = currentImpl ? currentImpl(...args) : (undefined as unknown as R);\n calls.push({ args, returnValue });\n return returnValue;\n }) as MockFn<T, R>;\n\n mockFn.mock = { calls };\n\n mockFn.mockImplementation = function (fn: (...args: T) => R): typeof mockFn {\n currentImpl = fn;\n return this;\n };\n\n mockFn.mockReturnValue = function (value: R): typeof mockFn {\n currentImpl = () => value;\n return this;\n };\n\n // No-op restore for standalone fn (only spyOn-created fns have a real restore)\n mockFn.mockRestore = (): void => {};\n\n return mockFn;\n}\n\nconst vi = {\n /** Creates a spy on `obj[method]`, replacing it with a mock function. */\n spyOn<T extends Record<string, unknown>, K extends keyof T>(\n obj: T,\n method: K,\n ): MockFn<unknown[], unknown> {\n const original = obj[method];\n const spy = _createMockFn<unknown[], unknown>(\n typeof original === 'function' ? (original as (...args: unknown[]) => unknown) : undefined,\n );\n\n spy.mockRestore = (): void => {\n obj[method] = original as T[K];\n };\n\n _spyRegistry.push({ obj: obj as Record<string, unknown>, method: method as string, original });\n obj[method] = spy as unknown as T[K];\n return spy;\n },\n\n /** Creates a standalone mock function, optionally wrapping `impl`. */\n fn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n return _createMockFn(impl);\n },\n\n /** Restores all spies created via `vi.spyOn` to their original values. */\n restoreAllMocks(): void {\n for (const { obj, method, original } of _spyRegistry) {\n obj[method] = original;\n }\n _spyRegistry.length = 0;\n },\n};\n\n/* -------------------------------------------------------------------------- */\n/* Runtime entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Runtime globals object — exported for direct use in unit tests so that\n * test factories can reference runtime's own `it`/`expect`/`beforeAll`/etc.\n * without depending on `globalThis` injection.\n *\n * In a real WebView bundle these are accessed via globals installed by\n * `runTestModule`; in Node tests, import from here directly.\n */\nexport const runtimeGlobals = {\n describe,\n it,\n test,\n expect,\n beforeAll,\n afterAll,\n beforeEach,\n afterEach,\n vi,\n} as const;\n\n/**\n * Installs describe/it/test/expect/afterAll/afterEach/beforeAll/beforeEach/vi\n * as globals, invokes `moduleFactory` to register the user's tests, then\n * executes them and returns a RunReport.\n *\n * This function is exported as `__testBundle.runTestModule` by the IIFE wrapper\n * that bundle.ts generates. The Node-side rpc.ts calls it via `Runtime.evaluate`.\n *\n * @param moduleFactory - A zero-argument function that contains the user's\n * top-level test code (describe/it/test calls). The bundler wraps the entire\n * test module so that its top-level statements become the body of this factory.\n */\nexport async function runTestModule(\n moduleFactory?: () => void | Promise<void>,\n): Promise<RunReport> {\n // Reset state for re-entrant calls within the same page context.\n _pendingTests.length = 0;\n _suiteStack.length = 0;\n _hooks.length = 0;\n _spyRegistry.length = 0;\n\n // Install globals that user test code expects.\n type G = typeof globalThis & {\n describe: typeof describe;\n it: typeof it;\n test: typeof test;\n expect: typeof expect;\n beforeAll: typeof beforeAll;\n afterAll: typeof afterAll;\n beforeEach: typeof beforeEach;\n afterEach: typeof afterEach;\n vi: typeof vi;\n };\n const g = globalThis as G;\n g.describe = describe;\n g.it = it;\n g.test = test;\n g.expect = expect;\n g.beforeAll = beforeAll;\n g.afterAll = afterAll;\n g.beforeEach = beforeEach;\n g.afterEach = afterEach;\n g.vi = vi;\n\n // Run the factory (which registers describe/it/test blocks via globals).\n if (moduleFactory) {\n await moduleFactory();\n }\n\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const results: TestResult[] = [];\n\n // Determine unique suite scopes from registered tests, in discovery order.\n // We need to fire beforeAll/afterAll once per scope (grouped by suitePath).\n //\n // Simplified model: we run beforeAll hooks before the first test in a scope\n // and afterAll hooks after the last test in a scope. Scope identity is the\n // entire suitePath string (e.g. \"Suite A > Suite B\").\n //\n // For module-scope hooks (suitePath=[]), they wrap the entire test run.\n\n // Group tests by their suite key to track beforeAll/afterAll per scope.\n // We keep a Set of scopes for which beforeAll has been fired.\n const firedBeforeAll = new Set<string>();\n // Map from scope key → last index in _pendingTests that belongs to that scope.\n const lastIndexForScope = new Map<string, number>();\n for (let i = 0; i < _pendingTests.length; i++) {\n const key = _pendingTests[i].suitePath.join('\\0');\n lastIndexForScope.set(key, i);\n // Also compute for parent scopes (module scope = '')\n const path = _pendingTests[i].suitePath;\n for (let depth = 0; depth < path.length; depth++) {\n const parentKey = path.slice(0, depth).join('\\0');\n const cur = lastIndexForScope.get(parentKey) ?? -1;\n if (i > cur) lastIndexForScope.set(parentKey, i);\n }\n }\n // Module scope key\n const MODULE_KEY = '';\n if (!lastIndexForScope.has(MODULE_KEY) && _pendingTests.length > 0) {\n lastIndexForScope.set(MODULE_KEY, _pendingTests.length - 1);\n }\n\n // Fire module-scope beforeAll before any tests\n if (_pendingTests.length > 0) {\n const baFns = _hooksFor('beforeAll', []);\n const baErr = await _runHooks(baFns);\n firedBeforeAll.add(MODULE_KEY);\n if (baErr) {\n // If module beforeAll fails, mark all tests as failed.\n for (const pending of _pendingTests) {\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n // Still run module afterAll for cleanup (e.g. flushCapture)\n const aaFns = _hooksFor('afterAll', []);\n await _runHooks(aaFns);\n return { startedAt, duration, passed, failed, skipped, tests: results };\n }\n }\n\n for (let i = 0; i < _pendingTests.length; i++) {\n const pending = _pendingTests[i];\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n\n if (pending.skip) {\n results.push({ name: fullName, status: 'skip', duration: 0 });\n continue;\n }\n\n // Fire suite-scoped beforeAll for any new scopes entered by this test\n for (let depth = 1; depth <= pending.suitePath.length; depth++) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n if (!firedBeforeAll.has(scopeKey)) {\n // Only hooks registered exactly at this scope (not broader/narrower)\n const scopedFns = _hooks\n .filter((h) => h.type === 'beforeAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n const baErr = await _runHooks(scopedFns);\n firedBeforeAll.add(scopeKey);\n if (baErr) {\n // Mark remaining tests in this scope as failed\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n }\n }\n\n // beforeEach\n const beFns = _hooksFor('beforeEach', pending.suitePath);\n const beErr = await _runHooks(beFns);\n\n const tStart = Date.now();\n let testErr: string | undefined;\n\n if (beErr) {\n testErr = `beforeEach failed: ${beErr.message}`;\n } else {\n try {\n await pending.fn();\n } catch (e) {\n testErr = e instanceof Error ? e.message : String(e);\n }\n }\n\n // afterEach — always run even if test failed\n const aeFns = _hooksFor('afterEach', pending.suitePath);\n const aeErr = await _runHooks(aeFns);\n if (aeErr && !testErr) testErr = `afterEach failed: ${aeErr.message}`;\n\n if (testErr !== undefined) {\n results.push({\n name: fullName,\n status: 'fail',\n duration: Date.now() - tStart,\n error: testErr,\n });\n } else {\n results.push({ name: fullName, status: 'pass', duration: Date.now() - tStart });\n }\n\n // Fire suite-scoped afterAll when this is the last test in each scope\n for (let depth = pending.suitePath.length; depth >= 1; depth--) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n const lastIdx = lastIndexForScope.get(scopeKey) ?? -1;\n if (lastIdx === i) {\n const scopedFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n await _runHooks(scopedFns);\n }\n }\n }\n\n // Fire module-scope afterAll after all tests — critical for flushCapture\n const moduleAfterAllFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.length === 0)\n .map((h) => h.fn);\n await _runHooks(moduleAfterAllFns);\n\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n\n return { startedAt, duration, passed, failed, skipped, tests: results };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runtime.js","names":["#received","#negated","#assert"],"sources":["../../src/test-runner/runtime.ts"],"sourcesContent":["/**\n * Thin test runtime for WebView execution.\n *\n * This file is bundled by `bundle.ts` together with the user's test file\n * into a single IIFE injected into the WebView via `Runtime.evaluate`.\n * It MUST stay browser-compatible — no Node.js APIs allowed.\n *\n * Design: rather than shipping @vitest/runner verbatim into a WebView (where\n * its Node-side internals cause issues), this runtime provides a minimal\n * compatible API surface — describe/it/test/expect globals — collects results\n * into a plain JSON-safe object, and exports `runTestModule` as the entry point.\n *\n * @vitest/runner and @vitest/expect are listed as dependencies so that the\n * package's type contracts are available and so the browser-compatible subsets\n * can be referenced. The Vitest custom pool that drives this runtime through\n * Vitest's `PoolRunnerInitializer` lives in `pool.ts`.\n *\n * NOTE: this file is imported by type from Node-side code (rpc.ts / relay-worker.ts)\n * for the RunReport / TestResult type shapes. The runtime ITSELF is not imported\n * at runtime on the Node side — only the types are used.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Public result types (used by rpc.ts and relay-worker.ts) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result of a single test case.\n * All fields are JSON-serialisable.\n */\nexport interface TestResult {\n /** Full dot-joined test name including nested suite names. */\n name: string;\n /** `'pass'` or `'fail'`. `'skip'` for skipped tests. */\n status: 'pass' | 'fail' | 'skip';\n /** Duration in milliseconds. */\n duration: number;\n /** Error message (fail only). Does NOT include the expression/secret. */\n error?: string;\n /**\n * Optional note attached to a skip — populated when the test body itself\n * calls `ctx.skip(cond, note)` (devtools#746), mirroring the static\n * `it.skip`/`it.skipIf` path which has no note today. `'skip'` status only.\n */\n note?: string;\n}\n\n/** Aggregate report returned by `runTestModule`. */\nexport interface RunReport {\n /** ISO timestamp of when `runTestModule` was called. */\n startedAt: string;\n /** Total elapsed milliseconds (wall-clock). */\n duration: number;\n passed: number;\n failed: number;\n skipped: number;\n tests: TestResult[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Asymmetric matchers (devtools#692) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Brand property that tags asymmetric matcher markers (`expect.any(String)`,\n * `expect.objectContaining(...)`, etc). A reserved string key is used as the\n * brand so the guard stays a plain own-property check — markers are only ever\n * compared in-process by reference, so no cross-realm Symbol coordination is\n * needed.\n */\nconst ASYMMETRIC_BRAND = '$$asymmetricMatch' as const;\n\n/**\n * An asymmetric matcher: a placeholder that, when found on the `expected` side\n * of a deep comparison, runs its own `asymmetricMatch(actual)` predicate\n * instead of being structurally compared as a plain object. This is what makes\n * `toMatchObject({ hash: expect.any(String) })` match any string `hash`.\n */\ninterface AsymmetricMatcher {\n /** Brand discriminator — present only on asymmetric markers. */\n [ASYMMETRIC_BRAND]: true;\n /** Human-readable label used in failure messages. */\n toString(): string;\n /** Returns true when `actual` satisfies this matcher. */\n asymmetricMatch(actual: unknown): boolean;\n}\n\n/** Narrowing guard: is `v` an asymmetric matcher marker? */\nfunction isAsymmetric(v: unknown): v is AsymmetricMatcher {\n return (\n typeof v === 'object' &&\n v !== null &&\n (v as Record<string, unknown>)[ASYMMETRIC_BRAND] === true &&\n typeof (v as { asymmetricMatch?: unknown }).asymmetricMatch === 'function'\n );\n}\n\n/** The widest constructor type TypeScript allows without `any` (see toBeInstanceOf). */\ntype AnyCtor = abstract new (...args: never) => unknown;\n\n/**\n * `expect.any(Ctor)` — matches a value by its constructor. Primitive wrappers\n * (`String`/`Number`/`Boolean`) are matched by `typeof` (so the unboxed\n * primitive matches) AND by `instanceof` (so a boxed wrapper instance also\n * matches). `BigInt`/`Symbol` match by `typeof`. `Object` matches any non-null\n * object (NOT functions, matching vitest), `Array` uses `Array.isArray`,\n * `Function` uses `typeof === 'function'`, and any other constructor (user\n * classes) falls back to `instanceof`.\n */\nfunction makeAny(ctor: AnyCtor): AsymmetricMatcher {\n const name = (ctor as { name?: string }).name ?? String(ctor);\n const ctorAsFn = ctor as unknown as (new (...args: unknown[]) => unknown) | undefined;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `Any<${name}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (actual === null || actual === undefined) return false;\n switch (ctor as unknown) {\n case String:\n return typeof actual === 'string' || actual instanceof String;\n case Number:\n return typeof actual === 'number' || actual instanceof Number;\n case Boolean:\n return typeof actual === 'boolean' || actual instanceof Boolean;\n case BigInt:\n return typeof actual === 'bigint';\n case Symbol:\n return typeof actual === 'symbol';\n case Function:\n return typeof actual === 'function';\n case Object:\n // Match vitest: `expect.any(Object)` accepts objects only, NOT\n // functions (vitest checks `typeof other === 'object'`). `null` is\n // already excluded by the guard above.\n return typeof actual === 'object';\n case Array:\n return Array.isArray(actual);\n default:\n return ctorAsFn ? actual instanceof ctorAsFn : false;\n }\n },\n };\n}\n\n/** `expect.anything()` — matches any value except `null`/`undefined`. */\nfunction makeAnything(): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => 'Anything',\n asymmetricMatch: (actual: unknown): boolean => actual !== null && actual !== undefined,\n };\n}\n\n/** `expect.objectContaining(obj)` — partial-match every key of `obj` on `actual`. */\nfunction makeObjectContaining(expected: Record<string, unknown>): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ObjectContaining<${safeStringify(expected)}>`,\n asymmetricMatch: (actual: unknown): boolean => deepMatchObject(actual, expected),\n };\n}\n\n/** `expect.arrayContaining(arr)` — `actual` is an array containing each element of `arr`. */\nfunction makeArrayContaining(expected: unknown[]): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `ArrayContaining<${safeStringify(expected)}>`,\n asymmetricMatch(actual: unknown): boolean {\n if (!Array.isArray(actual)) return false;\n return expected.every((exp) =>\n actual.some((act) => (isAsymmetric(exp) ? exp.asymmetricMatch(act) : deepEqual(act, exp))),\n );\n },\n };\n}\n\n/** `expect.stringContaining(sub)` — `actual` is a string containing `sub`. */\nfunction makeStringContaining(sub: string): AsymmetricMatcher {\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringContaining<${sub}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && actual.includes(sub),\n };\n}\n\n/** `expect.stringMatching(re)` — `actual` is a string matching `re` (string or RegExp). */\nfunction makeStringMatching(re: string | RegExp): AsymmetricMatcher {\n const pattern = typeof re === 'string' ? new RegExp(re) : re;\n return {\n [ASYMMETRIC_BRAND]: true,\n toString: () => `StringMatching<${String(pattern)}>`,\n asymmetricMatch: (actual: unknown): boolean =>\n typeof actual === 'string' && pattern.test(actual),\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Deep equality helpers */\n/* -------------------------------------------------------------------------- */\n\n/** JSON.stringify that never throws (cycles/BigInt) — for failure messages. */\nfunction safeStringify(v: unknown): string {\n try {\n return (\n JSON.stringify(v, (_k, val) =>\n isAsymmetric(val) ? val.toString() : typeof val === 'bigint' ? `${val}n` : val,\n ) ?? String(v)\n );\n } catch {\n return String(v);\n }\n}\n\n/**\n * Recursive structural equality — replaces JSON.stringify comparison to\n * handle key-order differences and undefined values correctly.\n *\n * When the `b` (expected) side is an asymmetric marker, its `asymmetricMatch`\n * predicate decides the result instead of structural comparison — markers are\n * placeholders, not plain objects, so deep-comparing them never matches.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (isAsymmetric(b)) return b.asymmetricMatch(a);\n if (Object.is(a, b)) return true;\n if (a === null || b === null) return false;\n if (typeof a !== 'object' || typeof b !== 'object') return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n if (Array.isArray(a) || Array.isArray(b)) return false;\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(bObj, key)) return false;\n if (!deepEqual(aObj[key], bObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Partial-match: every key in `expected` must exist in `received` with a\n * recursively matching value. Extra keys in `received` are ignored.\n *\n * As in `deepEqual`, an asymmetric marker on the `expected` side delegates to\n * its `asymmetricMatch` predicate rather than being compared structurally.\n */\nfunction deepMatchObject(received: unknown, expected: unknown): boolean {\n if (isAsymmetric(expected)) return expected.asymmetricMatch(received);\n if (Object.is(received, expected)) return true;\n if (expected === null || received === null) return Object.is(received, expected);\n if (typeof expected !== 'object' || typeof received !== 'object')\n return Object.is(received, expected);\n\n if (Array.isArray(expected) && Array.isArray(received)) {\n if (expected.length !== received.length) return false;\n for (let i = 0; i < expected.length; i++) {\n if (!deepMatchObject(received[i], expected[i])) return false;\n }\n return true;\n }\n if (Array.isArray(expected) || Array.isArray(received)) return false;\n\n const expObj = expected as Record<string, unknown>;\n const recObj = received as Record<string, unknown>;\n for (const key of Object.keys(expObj)) {\n if (!Object.hasOwn(recObj, key)) return false;\n if (!deepMatchObject(recObj[key], expObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Resolves a dot-separated property path on an object.\n * Returns `{ found: true, value }` or `{ found: false }`.\n */\nfunction resolvePath(obj: unknown, dotPath: string): { found: boolean; value?: unknown } {\n const parts = dotPath.split('.');\n let cur: unknown = obj;\n for (const part of parts) {\n if (cur === null || cur === undefined || typeof cur !== 'object') return { found: false };\n const record = cur as Record<string, unknown>;\n if (!Object.hasOwn(record, part)) return { found: false };\n cur = record[part];\n }\n return { found: true, value: cur };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Lightweight expect implementation */\n/* -------------------------------------------------------------------------- */\n\n/** Thrown by expect matchers on failure. */\nclass AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AssertionError';\n }\n}\n\n/** Minimal expect builder compatible with Vitest's jest-style API. */\nclass Expectation {\n #received: unknown;\n #negated = false;\n\n constructor(received: unknown) {\n this.#received = received;\n }\n\n get not(): this {\n const neg = new Expectation(this.#received) as this;\n neg.#negated = true;\n return neg;\n }\n\n #assert(pass: boolean, msg: string): void {\n const actual = this.#negated ? !pass : pass;\n if (!actual) {\n throw new AssertionError(this.#negated ? `Expected NOT: ${msg}` : msg);\n }\n }\n\n toBe(expected: unknown): void {\n this.#assert(\n Object.is(this.#received, expected),\n `Expected ${String(expected)}, received ${String(this.#received)}`,\n );\n }\n\n toEqual(expected: unknown): void {\n this.#assert(\n deepEqual(this.#received, expected),\n `Expected ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toBeTruthy(): void {\n this.#assert(Boolean(this.#received), `Expected truthy, received ${String(this.#received)}`);\n }\n\n toBeFalsy(): void {\n this.#assert(!this.#received, `Expected falsy, received ${String(this.#received)}`);\n }\n\n toBeNull(): void {\n this.#assert(this.#received === null, `Expected null, received ${String(this.#received)}`);\n }\n\n toBeUndefined(): void {\n this.#assert(\n this.#received === undefined,\n `Expected undefined, received ${String(this.#received)}`,\n );\n }\n\n toBeGreaterThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received > n,\n `Expected > ${n}, received ${String(this.#received)}`,\n );\n }\n\n toBeLessThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received < n,\n `Expected < ${n}, received ${String(this.#received)}`,\n );\n }\n\n toContain(sub: unknown): void {\n if (Array.isArray(this.#received)) {\n this.#assert(\n this.#received.includes(sub),\n `Expected array to contain ${JSON.stringify(sub)}, received ${JSON.stringify(this.#received)}`,\n );\n return;\n }\n this.#assert(\n typeof this.#received === 'string' && typeof sub === 'string' && this.#received.includes(sub),\n `Expected to contain \"${String(sub)}\", received \"${String(this.#received)}\"`,\n );\n }\n\n toThrow(msgFragment?: string): void {\n if (typeof this.#received !== 'function') {\n throw new AssertionError('toThrow: expected a function');\n }\n let threw = false;\n let errorMsg = '';\n try {\n (this.#received as () => unknown)();\n } catch (e) {\n threw = true;\n errorMsg = e instanceof Error ? e.message : String(e);\n }\n if (msgFragment !== undefined) {\n this.#assert(\n threw && errorMsg.includes(msgFragment),\n `Expected to throw containing \"${msgFragment}\", got \"${errorMsg}\"`,\n );\n } else {\n this.#assert(threw, 'Expected function to throw');\n }\n }\n\n // ---- New matchers (devtools#683) -----------------------------------------\n\n toMatchObject(expected: Record<string, unknown>): void {\n this.#assert(\n deepMatchObject(this.#received, expected),\n `Expected object to match ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toHaveProperty(dotPath: string, ...rest: unknown[]): void {\n const { found, value: actual } = resolvePath(this.#received, dotPath);\n if (rest.length > 0) {\n const value = rest[0];\n this.#assert(\n found && deepEqual(actual, value),\n `Expected property \"${dotPath}\" to equal ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n );\n } else {\n this.#assert(found, `Expected property \"${dotPath}\" to exist`);\n }\n }\n\n // The `abstract new (...args: never) => unknown` signature is the widest\n // constructor type that TypeScript allows without explicit `any`. Real-world\n // constructors like `ErrorConstructor` are assignable to it because `never`\n // in parameter position is contravariant (any arg list satisfies `never[]`).\n toBeInstanceOf(ctor: abstract new (...args: never) => unknown): void {\n this.#assert(\n this.#received instanceof (ctor as new (...args: unknown[]) => unknown),\n `Expected instance of ${(ctor as { name?: string }).name ?? String(ctor)}, received ${String(this.#received)}`,\n );\n }\n\n toBeTypeOf(typeStr: string): void {\n this.#assert(\n typeof this.#received === typeStr,\n `Expected typeof \"${typeStr}\", received \"${typeof this.#received}\"`,\n );\n }\n}\n\n/**\n * The `expect` callable plus its asymmetric-matcher static surface\n * (`expect.any`, `expect.objectContaining`, …). The user file accesses these as\n * `expect.any(String)`; the vitest redirect (`bundle.ts`) re-exports `expect` by\n * reading `globalThis.expect`, so attaching the statics to this function object\n * makes them reachable through the getter (a function's own properties survive\n * the getter that returns the function itself).\n */\ninterface ExpectStatic {\n (received: unknown): Expectation;\n any(ctor: AnyCtor): AsymmetricMatcher;\n anything(): AsymmetricMatcher;\n objectContaining(expected: Record<string, unknown>): AsymmetricMatcher;\n arrayContaining(expected: unknown[]): AsymmetricMatcher;\n stringContaining(sub: string): AsymmetricMatcher;\n stringMatching(re: string | RegExp): AsymmetricMatcher;\n}\n\n/** The `expect` function installed as a global. */\nconst expect: ExpectStatic = ((received: unknown): Expectation =>\n new Expectation(received)) as ExpectStatic;\n\n// Asymmetric matcher statics — vitest-compatible surface (devtools#692).\nexpect.any = makeAny;\nexpect.anything = makeAnything;\nexpect.objectContaining = makeObjectContaining;\nexpect.arrayContaining = makeArrayContaining;\nexpect.stringContaining = makeStringContaining;\nexpect.stringMatching = makeStringMatching;\n\n/* -------------------------------------------------------------------------- */\n/* Vitest-4-compatible task context (devtools#746) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Internal sentinel thrown by `ctx.skip()` to unwind the test body early.\n * Caught by the run loop (never surfaced as a failure) — see `runTestModule`'s\n * test-execution `catch` branch.\n */\nclass InPageSkipSentinel {\n constructor(readonly note: string | undefined) {}\n}\n\n/**\n * Minimal vitest-4-compatible task context passed as the first argument to a\n * test function, e.g. `it('...', async (ctx) => { ctx.skip(cond, note); })`.\n *\n * Real vitest's `TestContext` is much larger (`expect`, `onTestFailed`, …) —\n * this shim only implements what sdk-example's device-fixture suites actually\n * use: `ctx.skip` (vitest 4 semantics below) and a trivial `ctx.task.name`.\n */\nexport interface TestContext {\n /**\n * `ctx.skip()` (no args) unconditionally skips the remainder of the test\n * body. `ctx.skip(cond, note)` skips only when `cond` is truthy; when falsy,\n * this is a no-op and the body continues normally. Skipping throws an\n * internal sentinel — do not catch `InPageSkipSentinel` in user code.\n */\n skip(condition?: boolean, note?: string): void;\n /** Trivial task descriptor — nice-to-have, mirrors vitest's `ctx.task.name`. */\n task: { name: string };\n}\n\nfunction _makeTestContext(name: string): TestContext {\n return {\n skip(condition?: boolean, note?: string): void {\n if (condition === undefined || condition) {\n throw new InPageSkipSentinel(note);\n }\n },\n task: { name },\n };\n}\n\n/* -------------------------------------------------------------------------- */\n/* describe / it / test registry */\n/* -------------------------------------------------------------------------- */\n\n/** A test function as the user writes it — 0-arg or 1-arg (vitest task context). */\ntype TestFn = (ctx: TestContext) => void | Promise<void>;\n\ninterface PendingTest {\n suitePath: string[];\n name: string;\n fn: TestFn;\n skip: boolean;\n}\n\nconst _pendingTests: PendingTest[] = [];\nconst _suiteStack: string[] = [];\n\n/** Registers a suite scope; calls `fn` synchronously to collect inner tests. */\nfunction describe(name: string, fn: () => void): void {\n _suiteStack.push(name);\n fn();\n _suiteStack.pop();\n}\n\n/** Registers a test. */\nfunction it(name: string, fn: TestFn): void {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip: false });\n}\n\n/** Alias for `it`. */\nconst test = it;\n\n/** Skipped test — registered but not executed. */\ndescribe.skip = (name: string, _fn: () => void): void => {\n void name;\n};\nit.skip = (name: string, _fn?: TestFn): void => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn: () => {}, skip: true });\n};\ntest.skip = it.skip;\n\n/**\n * `it.skipIf(cond)(name, fn)` / `it.runIf(cond)(name, fn)` — conditional test\n * registration. `skipIf` skips when `cond` is truthy; `runIf` runs only when\n * `cond` is truthy (skips otherwise). sdk-example uses\n * `it.skipIf(cell.platform === 'mock')(...)` to skip real-SDK-only cases in env1.\n */\ntype ItRegistrar = (name: string, fn: TestFn) => void;\nfunction _conditionalIt(skip: boolean): ItRegistrar {\n return (name, fn) => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip });\n };\n}\nit.skipIf = (cond: unknown): ItRegistrar => _conditionalIt(Boolean(cond));\nit.runIf = (cond: unknown): ItRegistrar => _conditionalIt(!cond);\ntest.skipIf = it.skipIf;\ntest.runIf = it.runIf;\n\n/**\n * `describe.skipIf(cond)(name, fn)` / `describe.runIf(cond)(name, fn)`.\n * When skipped, the suite body still runs to register its tests but every test\n * inside is marked skipped (collected via a temporary skip flag is overkill —\n * a skipped describe simply does not invoke its body, mirroring `describe.skip`).\n */\ntype DescribeRegistrar = (name: string, fn: () => void) => void;\ndescribe.skipIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, _fn) => void name : (name, fn) => describe(name, fn);\ndescribe.runIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, fn) => describe(name, fn) : (name, _fn) => void name;\n\n/* -------------------------------------------------------------------------- */\n/* Lifecycle hooks (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ntype HookType = 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach';\n\ninterface HookEntry {\n /** Suite path at the time of registration ([] = module scope). */\n suitePath: string[];\n type: HookType;\n fn: () => void | Promise<void>;\n}\n\nconst _hooks: HookEntry[] = [];\n\nfunction _registerHook(type: HookType, fn: () => void | Promise<void>): void {\n _hooks.push({ suitePath: [..._suiteStack], type, fn });\n}\n\n/**\n * Returns hooks whose suitePath is a prefix of `testSuitePath`\n * (i.e. the hook's scope contains the test).\n */\nfunction _hooksFor(type: HookType, testSuitePath: string[]): Array<() => void | Promise<void>> {\n return _hooks\n .filter((h) => {\n if (h.type !== type) return false;\n // Hook scope must be a prefix of the test's suite path\n if (h.suitePath.length > testSuitePath.length) return false;\n for (let i = 0; i < h.suitePath.length; i++) {\n if (h.suitePath[i] !== testSuitePath[i]) return false;\n }\n return true;\n })\n .map((h) => h.fn);\n}\n\n/** Runs an array of hook functions in order, awaiting each. */\nasync function _runHooks(fns: Array<() => void | Promise<void>>): Promise<Error | null> {\n for (const fn of fns) {\n try {\n await fn();\n } catch (e) {\n return e instanceof Error ? e : new Error(String(e));\n }\n }\n return null;\n}\n\nfunction beforeAll(fn: () => void | Promise<void>): void {\n _registerHook('beforeAll', fn);\n}\nfunction afterAll(fn: () => void | Promise<void>): void {\n _registerHook('afterAll', fn);\n}\nfunction beforeEach(fn: () => void | Promise<void>): void {\n _registerHook('beforeEach', fn);\n}\nfunction afterEach(fn: () => void | Promise<void>): void {\n _registerHook('afterEach', fn);\n}\n\n/* -------------------------------------------------------------------------- */\n/* vi shim (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ninterface SpyCall {\n args: unknown[];\n returnValue: unknown;\n}\n\ninterface MockFn<T extends unknown[], R> {\n (...args: T): R;\n mock: { calls: SpyCall[] };\n mockImplementation(fn: (...args: T) => R): this;\n mockReturnValue(value: R): this;\n mockRestore(): void;\n}\n\ninterface SpyRecord {\n obj: Record<string, unknown>;\n method: string;\n original: unknown;\n}\n\nconst _spyRegistry: SpyRecord[] = [];\n\nfunction _createMockFn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n let currentImpl: ((...args: T) => R) | undefined = impl;\n const calls: SpyCall[] = [];\n\n const mockFn = ((...args: T): R => {\n const returnValue = currentImpl ? currentImpl(...args) : (undefined as unknown as R);\n calls.push({ args, returnValue });\n return returnValue;\n }) as MockFn<T, R>;\n\n mockFn.mock = { calls };\n\n mockFn.mockImplementation = function (fn: (...args: T) => R): typeof mockFn {\n currentImpl = fn;\n return this;\n };\n\n mockFn.mockReturnValue = function (value: R): typeof mockFn {\n currentImpl = () => value;\n return this;\n };\n\n // No-op restore for standalone fn (only spyOn-created fns have a real restore)\n mockFn.mockRestore = (): void => {};\n\n return mockFn;\n}\n\nconst vi = {\n /** Creates a spy on `obj[method]`, replacing it with a mock function. */\n spyOn<T extends Record<string, unknown>, K extends keyof T>(\n obj: T,\n method: K,\n ): MockFn<unknown[], unknown> {\n const original = obj[method];\n const spy = _createMockFn<unknown[], unknown>(\n typeof original === 'function' ? (original as (...args: unknown[]) => unknown) : undefined,\n );\n\n spy.mockRestore = (): void => {\n obj[method] = original as T[K];\n };\n\n _spyRegistry.push({ obj: obj as Record<string, unknown>, method: method as string, original });\n obj[method] = spy as unknown as T[K];\n return spy;\n },\n\n /** Creates a standalone mock function, optionally wrapping `impl`. */\n fn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n return _createMockFn(impl);\n },\n\n /** Restores all spies created via `vi.spyOn` to their original values. */\n restoreAllMocks(): void {\n for (const { obj, method, original } of _spyRegistry) {\n obj[method] = original;\n }\n _spyRegistry.length = 0;\n },\n};\n\n/* -------------------------------------------------------------------------- */\n/* Runtime entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Runtime globals object — exported for direct use in unit tests so that\n * test factories can reference runtime's own `it`/`expect`/`beforeAll`/etc.\n * without depending on `globalThis` injection.\n *\n * In a real WebView bundle these are accessed via globals installed by\n * `runTestModule`; in Node tests, import from here directly.\n */\nexport const runtimeGlobals = {\n describe,\n it,\n test,\n expect,\n beforeAll,\n afterAll,\n beforeEach,\n afterEach,\n vi,\n} as const;\n\n/**\n * Installs describe/it/test/expect/afterAll/afterEach/beforeAll/beforeEach/vi\n * as globals, invokes `moduleFactory` to register the user's tests, then\n * executes them and returns a RunReport.\n *\n * This function is exported as `__testBundle.runTestModule` by the IIFE wrapper\n * that bundle.ts generates. The Node-side rpc.ts calls it via `Runtime.evaluate`.\n *\n * @param moduleFactory - A zero-argument function that contains the user's\n * top-level test code (describe/it/test calls). The bundler wraps the entire\n * test module so that its top-level statements become the body of this factory.\n */\nexport async function runTestModule(\n moduleFactory?: () => void | Promise<void>,\n): Promise<RunReport> {\n // Reset state for re-entrant calls within the same page context.\n _pendingTests.length = 0;\n _suiteStack.length = 0;\n _hooks.length = 0;\n _spyRegistry.length = 0;\n\n // Install globals that user test code expects.\n type G = typeof globalThis & {\n describe: typeof describe;\n it: typeof it;\n test: typeof test;\n expect: typeof expect;\n beforeAll: typeof beforeAll;\n afterAll: typeof afterAll;\n beforeEach: typeof beforeEach;\n afterEach: typeof afterEach;\n vi: typeof vi;\n };\n const g = globalThis as G;\n g.describe = describe;\n g.it = it;\n g.test = test;\n g.expect = expect;\n g.beforeAll = beforeAll;\n g.afterAll = afterAll;\n g.beforeEach = beforeEach;\n g.afterEach = afterEach;\n g.vi = vi;\n\n // Run the factory (which registers describe/it/test blocks via globals).\n if (moduleFactory) {\n await moduleFactory();\n }\n\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const results: TestResult[] = [];\n\n // Determine unique suite scopes from registered tests, in discovery order.\n // We need to fire beforeAll/afterAll once per scope (grouped by suitePath).\n //\n // Simplified model: we run beforeAll hooks before the first test in a scope\n // and afterAll hooks after the last test in a scope. Scope identity is the\n // entire suitePath string (e.g. \"Suite A > Suite B\").\n //\n // For module-scope hooks (suitePath=[]), they wrap the entire test run.\n\n // Group tests by their suite key to track beforeAll/afterAll per scope.\n // We keep a Set of scopes for which beforeAll has been fired.\n const firedBeforeAll = new Set<string>();\n // Map from scope key → last index in _pendingTests that belongs to that scope.\n const lastIndexForScope = new Map<string, number>();\n for (let i = 0; i < _pendingTests.length; i++) {\n const key = _pendingTests[i].suitePath.join('\\0');\n lastIndexForScope.set(key, i);\n // Also compute for parent scopes (module scope = '')\n const path = _pendingTests[i].suitePath;\n for (let depth = 0; depth < path.length; depth++) {\n const parentKey = path.slice(0, depth).join('\\0');\n const cur = lastIndexForScope.get(parentKey) ?? -1;\n if (i > cur) lastIndexForScope.set(parentKey, i);\n }\n }\n // Module scope key\n const MODULE_KEY = '';\n if (!lastIndexForScope.has(MODULE_KEY) && _pendingTests.length > 0) {\n lastIndexForScope.set(MODULE_KEY, _pendingTests.length - 1);\n }\n\n // Fire module-scope beforeAll before any tests\n if (_pendingTests.length > 0) {\n const baFns = _hooksFor('beforeAll', []);\n const baErr = await _runHooks(baFns);\n firedBeforeAll.add(MODULE_KEY);\n if (baErr) {\n // If module beforeAll fails, mark all tests as failed.\n for (const pending of _pendingTests) {\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n // Still run module afterAll for cleanup (e.g. flushCapture)\n const aaFns = _hooksFor('afterAll', []);\n await _runHooks(aaFns);\n return { startedAt, duration, passed, failed, skipped, tests: results };\n }\n }\n\n for (let i = 0; i < _pendingTests.length; i++) {\n const pending = _pendingTests[i];\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n\n if (pending.skip) {\n results.push({ name: fullName, status: 'skip', duration: 0 });\n continue;\n }\n\n // Fire suite-scoped beforeAll for any new scopes entered by this test\n for (let depth = 1; depth <= pending.suitePath.length; depth++) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n if (!firedBeforeAll.has(scopeKey)) {\n // Only hooks registered exactly at this scope (not broader/narrower)\n const scopedFns = _hooks\n .filter((h) => h.type === 'beforeAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n const baErr = await _runHooks(scopedFns);\n firedBeforeAll.add(scopeKey);\n if (baErr) {\n // Mark remaining tests in this scope as failed\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n }\n }\n\n // beforeEach\n const beFns = _hooksFor('beforeEach', pending.suitePath);\n const beErr = await _runHooks(beFns);\n\n const tStart = Date.now();\n let testErr: string | undefined;\n let inPageSkip: InPageSkipSentinel | undefined;\n\n if (beErr) {\n testErr = `beforeEach failed: ${beErr.message}`;\n } else {\n try {\n await pending.fn(_makeTestContext(fullName));\n } catch (e) {\n if (e instanceof InPageSkipSentinel) {\n inPageSkip = e;\n } else {\n testErr = e instanceof Error ? e.message : String(e);\n }\n }\n }\n\n // afterEach — always run even if test failed or was skipped mid-body.\n const aeFns = _hooksFor('afterEach', pending.suitePath);\n const aeErr = await _runHooks(aeFns);\n if (aeErr && !testErr && !inPageSkip) testErr = `afterEach failed: ${aeErr.message}`;\n\n if (inPageSkip) {\n // ctx.skip() unwound the body — record as skipped (not pass/fail), same\n // as the static it.skip path. The note (if any) rides on TestResult.note.\n results.push({\n name: fullName,\n status: 'skip',\n duration: Date.now() - tStart,\n ...(inPageSkip.note !== undefined ? { note: inPageSkip.note } : {}),\n });\n } else if (testErr !== undefined) {\n results.push({\n name: fullName,\n status: 'fail',\n duration: Date.now() - tStart,\n error: testErr,\n });\n } else {\n results.push({ name: fullName, status: 'pass', duration: Date.now() - tStart });\n }\n\n // Fire suite-scoped afterAll when this is the last test in each scope\n for (let depth = pending.suitePath.length; depth >= 1; depth--) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n const lastIdx = lastIndexForScope.get(scopeKey) ?? -1;\n if (lastIdx === i) {\n const scopedFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n await _runHooks(scopedFns);\n }\n }\n }\n\n // Fire module-scope afterAll after all tests — critical for flushCapture\n const moduleAfterAllFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.length === 0)\n .map((h) => h.fn);\n await _runHooks(moduleAfterAllFns);\n\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n\n return { startedAt, duration, passed, failed, skipped, tests: results };\n}\n"],"mappings":"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"}
|