@ait-co/devtools 0.1.116 → 0.1.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attach-orchestrator-BEMGBeWm.js +3 -0
- package/dist/attach-orchestrator-BZsqoFjB.js +2797 -0
- package/dist/attach-orchestrator-BZsqoFjB.js.map +1 -0
- package/dist/attach-orchestrator-CFotueL3.js +1749 -0
- package/dist/attach-orchestrator-CFotueL3.js.map +1 -0
- package/dist/attach-orchestrator-DEJfLm7V.js +1749 -0
- package/dist/attach-orchestrator-DEJfLm7V.js.map +1 -0
- package/dist/{cli-BLBo0lkP.js → attach-orchestrator-qFEMBUwb.js} +23 -739
- package/dist/attach-orchestrator-qFEMBUwb.js.map +1 -0
- package/dist/{bundle-KFs4t-wc.d.ts → bundle-CA1TCXED.d.ts} +1 -1
- package/dist/{bundle-KFs4t-wc.d.ts.map → bundle-CA1TCXED.d.ts.map} +1 -1
- package/dist/capture-BmK_cBqQ.d.ts +58 -0
- package/dist/capture-BmK_cBqQ.d.ts.map +1 -0
- package/dist/{cdp-connection-C0AP0tH2.d.ts → cdp-connection-DB2zgthr.d.ts} +1 -1
- package/dist/{cdp-connection-C0AP0tH2.d.ts.map → cdp-connection-DB2zgthr.d.ts.map} +1 -1
- package/dist/cell-BDFMsPXV.js +3 -0
- package/dist/cell-BPWZUm_X.js +85 -0
- package/dist/cell-BPWZUm_X.js.map +1 -0
- package/dist/cell-Cv3e_qwn.js +84 -0
- package/dist/cell-Cv3e_qwn.js.map +1 -0
- package/dist/cell-Dp_pDs_j.js +85 -0
- package/dist/cell-Dp_pDs_j.js.map +1 -0
- package/dist/cell-nrWFeqvG.js +84 -0
- package/dist/cell-nrWFeqvG.js.map +1 -0
- package/dist/cli-BA1ynNNx.js +1039 -0
- package/dist/cli-BA1ynNNx.js.map +1 -0
- package/dist/debug-server-BhHVy3XT.js +1191 -0
- package/dist/debug-server-BhHVy3XT.js.map +1 -0
- package/dist/{debug-server-DcAKrbnq.js → debug-server-T8i9_iCc.js} +5 -3
- package/dist/debug-server-T8i9_iCc.js.map +1 -0
- package/dist/debug-server-krAOy6cl.js +1891 -0
- package/dist/debug-server-krAOy6cl.js.map +1 -0
- package/dist/{debug-server-DlXJARIC.js → debug-server-wo18jR4T.js} +432 -2884
- package/dist/debug-server-wo18jR4T.js.map +1 -0
- package/dist/mcp/cli.js +3 -2
- 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-htlVnEFl.d.ts → pool-U9FrxEuR.d.ts} +5 -5
- package/dist/{pool-htlVnEFl.d.ts.map → pool-U9FrxEuR.d.ts.map} +1 -1
- package/dist/relay-factory-DVI7TrX4.js +69 -0
- package/dist/relay-factory-DVI7TrX4.js.map +1 -0
- package/dist/relay-secret-store-BcVrWwTq.js +153 -0
- package/dist/relay-secret-store-BcVrWwTq.js.map +1 -0
- package/dist/{relay-secret-store-BHcOmaNK.js → relay-secret-store-BlFEhqwb.js} +1 -1
- package/dist/{relay-secret-store-CmqchhR5.js → relay-secret-store-CFc9n0OA.js} +1 -1
- package/dist/{relay-secret-store-CmqchhR5.js.map → relay-secret-store-CFc9n0OA.js.map} +1 -1
- package/dist/{relay-secret-store-CkA7KNUb.js → relay-secret-store-CmDDfZMh.js} +2 -2
- package/dist/{relay-secret-store-CkA7KNUb.js.map → relay-secret-store-CmDDfZMh.js.map} +1 -1
- package/dist/relay-secret-store-DNPJKHNs.js +153 -0
- package/dist/relay-secret-store-DNPJKHNs.js.map +1 -0
- package/dist/relay-url-store-CdA58fgw.js +122 -0
- package/dist/relay-url-store-CdA58fgw.js.map +1 -0
- package/dist/{relay-url-store-C0qukm3R.js → relay-url-store-CnL2zwbH.js} +2 -2
- package/dist/{relay-url-store-C0qukm3R.js.map → relay-url-store-CnL2zwbH.js.map} +1 -1
- package/dist/{relay-url-store-NDtEcOE-.js → relay-url-store-DGQ-HPQC.js} +2 -2
- package/dist/{relay-url-store-NDtEcOE-.js.map → relay-url-store-DGQ-HPQC.js.map} +1 -1
- package/dist/relay-url-store-Dv5HSzwb.js +122 -0
- package/dist/relay-url-store-Dv5HSzwb.js.map +1 -0
- package/dist/{relay-worker-DWW4vZxU.d.ts → relay-worker-D-3aAyDO.d.ts} +28 -4
- package/dist/relay-worker-D-3aAyDO.d.ts.map +1 -0
- package/dist/{runtime-BU-iMHz6.d.ts → runtime-D57Rtnsd.d.ts} +1 -1
- package/dist/{runtime-BU-iMHz6.d.ts.map → runtime-D57Rtnsd.d.ts.map} +1 -1
- package/dist/test-runner/bundle.d.ts +1 -1
- package/dist/test-runner/bundle.js +9 -9
- package/dist/test-runner/bundle.js.map +1 -1
- package/dist/test-runner/capture.d.ts +2 -0
- package/dist/test-runner/capture.js +44 -0
- package/dist/test-runner/capture.js.map +1 -0
- package/dist/test-runner/cli.d.ts +85 -12
- package/dist/test-runner/cli.d.ts.map +1 -1
- package/dist/test-runner/cli.js +2 -2
- package/dist/test-runner/config.d.ts +76 -2
- package/dist/test-runner/config.d.ts.map +1 -1
- package/dist/test-runner/config.js +4 -2
- package/dist/test-runner/config.js.map +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/relay-factory.d.ts +11136 -0
- package/dist/test-runner/relay-factory.d.ts.map +1 -0
- package/dist/test-runner/relay-factory.js +69 -0
- package/dist/test-runner/relay-factory.js.map +1 -0
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/relay-worker.js +66 -20
- package/dist/test-runner/relay-worker.js.map +1 -1
- package/dist/test-runner/report.d.ts +106 -0
- package/dist/test-runner/report.d.ts.map +1 -0
- package/dist/test-runner/report.js +139 -0
- package/dist/test-runner/report.js.map +1 -0
- package/dist/test-runner/rpc.d.ts +2 -2
- package/dist/test-runner/runtime.d.ts +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/{totp-D70zD5tJ.js → totp-0bvlo9Yb.js} +1 -1
- package/dist/{totp-D70zD5tJ.js.map → totp-0bvlo9Yb.js.map} +1 -1
- package/dist/totp-DfekTBk3.js +211 -0
- package/dist/totp-DfekTBk3.js.map +1 -0
- package/dist/totp-zGIlDsZS.js +3 -0
- package/package.json +1 -1
- package/dist/cli-BLBo0lkP.js.map +0 -1
- package/dist/debug-server-DcAKrbnq.js.map +0 -1
- package/dist/debug-server-DlXJARIC.js.map +0 -1
- package/dist/relay-worker-DWW4vZxU.d.ts.map +0 -1
- package/dist/totp-D104cJQN.js +0 -3
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { nearestPackageJsonDir } from "./relay-secret-store-DNPJKHNs.js";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
//#region src/mcp/relay-url-store.ts
|
|
4
|
+
/**
|
|
5
|
+
* Project-local ephemeral URL store (#424).
|
|
6
|
+
*
|
|
7
|
+
* Environment-2 ("AITC Sandbox PWA") cold-start requires two ephemeral URLs:
|
|
8
|
+
* - `relayBaseUrl` — the CDP relay's https base (was `AIT_RELAY_BASE_URL`)
|
|
9
|
+
* - `tunnelBaseUrl` — the app's https tunnel base (was `AIT_TUNNEL_BASE_URL`)
|
|
10
|
+
*
|
|
11
|
+
* Quick-tunnel URLs change every run. Previously the user had to copy-paste them
|
|
12
|
+
* into env vars on every cold-start — a single typo silently broke attach. This
|
|
13
|
+
* module replaces that manual hand-off with a file-based discovery pattern that
|
|
14
|
+
* exactly mirrors the `.ait_relay` TOTP-secret store (relay-secret-store.ts).
|
|
15
|
+
*
|
|
16
|
+
* Two surfaces, intentionally split by who is allowed to write:
|
|
17
|
+
*
|
|
18
|
+
* - {@link writeRelayUrls} — WRITE path, called ONLY from the unplugin
|
|
19
|
+
* (env-2 tunnel boot). Writes JSON to `<projectRoot>/.ait_urls` (0600) on
|
|
20
|
+
* every boot (`flag: 'w'` — overwrite). A single file — no directory is
|
|
21
|
+
* created.
|
|
22
|
+
*
|
|
23
|
+
* - {@link readRelayUrls} — READ-ONLY path, called from the MCP daemon as a
|
|
24
|
+
* fallback when `AIT_RELAY_BASE_URL`/`AIT_TUNNEL_BASE_URL` are not set. It
|
|
25
|
+
* NEVER writes, chmods, or creates anything: it only reads an existing
|
|
26
|
+
* `.ait_urls`. On any failure (missing file / bad JSON / wrong shape) it
|
|
27
|
+
* returns `null` silently, letting the downstream assertion be the single
|
|
28
|
+
* fail-fast.
|
|
29
|
+
*
|
|
30
|
+
* - {@link deleteRelayUrls} — called from the unplugin `cleanup()` on
|
|
31
|
+
* teardown (via `void deleteRelayUrls(...)`). A stale `.ait_urls` pointing
|
|
32
|
+
* at a dead tunnel would cause the MCP daemon to attempt a doomed attach on
|
|
33
|
+
* the next cold-start — deletion is non-negotiable. Silently swallows all
|
|
34
|
+
* errors.
|
|
35
|
+
*
|
|
36
|
+
* Design note: the env vars (`AIT_RELAY_BASE_URL`, `AIT_TUNNEL_BASE_URL`) are
|
|
37
|
+
* PRESERVED as operator overrides. The file is the fallback — env wins.
|
|
38
|
+
*
|
|
39
|
+
* Why `nearestPackageJsonDir` instead of `process.cwd()`: see relay-secret-store.ts.
|
|
40
|
+
* The unplugin (writer) and the MCP daemon (reader) both anchor to the nearest
|
|
41
|
+
* package.json from their respective `projectRoot` inputs, ensuring both sides
|
|
42
|
+
* find the same file.
|
|
43
|
+
*
|
|
44
|
+
* SECRET-HANDLING: `relayBaseUrl` and `tunnelBaseUrl` carry the relay/tunnel host
|
|
45
|
+
* — same sensitivity class as `.ait_relay`. The raw URL values, partial values,
|
|
46
|
+
* and the resolved file path MUST NOT appear in any log, error message, stdout,
|
|
47
|
+
* stderr, or assertion output anywhere in this module or at its call sites.
|
|
48
|
+
* Only boolean pass/fail signals are safe to surface. The file is written mode
|
|
49
|
+
* 0600.
|
|
50
|
+
*/
|
|
51
|
+
/** Project-local ephemeral URL file name (single file, not a directory). */
|
|
52
|
+
const URLS_FILE_NAME = ".ait_urls";
|
|
53
|
+
/**
|
|
54
|
+
* Absolute path to the project-local `.ait_urls` file for a given start
|
|
55
|
+
* directory (resolved against the nearest package.json directory).
|
|
56
|
+
*
|
|
57
|
+
* Exported so tests can compute the expected path without duplicating the
|
|
58
|
+
* resolution logic.
|
|
59
|
+
*/
|
|
60
|
+
function urlsFilePath(start, existsSyncFn) {
|
|
61
|
+
return join(nearestPackageJsonDir(start, existsSyncFn), URLS_FILE_NAME);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reads `<projectRoot>/.ait_urls` and returns the stored URLs, or `null` on
|
|
65
|
+
* any failure.
|
|
66
|
+
*
|
|
67
|
+
* Strictly READ-ONLY: only `existsSync` + `readFileSync`. Never writes,
|
|
68
|
+
* chmods, or creates files/directories.
|
|
69
|
+
*
|
|
70
|
+
* Returns `null` (silently, no throw, no log) on:
|
|
71
|
+
* - missing `projectRoot`
|
|
72
|
+
* - missing `.ait_urls` file
|
|
73
|
+
* - unreadable file (permissions, transient FS error)
|
|
74
|
+
* - invalid JSON
|
|
75
|
+
* - wrong shape (non-object, non-string values)
|
|
76
|
+
*
|
|
77
|
+
* Trims string values before returning. Ignores non-string fields.
|
|
78
|
+
*
|
|
79
|
+
* SECRET-HANDLING: URL values and the file path are never logged.
|
|
80
|
+
*/
|
|
81
|
+
async function readRelayUrls(deps) {
|
|
82
|
+
const { projectRoot, fs: fsDep, existsSync: existsSyncDep } = deps ?? {};
|
|
83
|
+
if (projectRoot === void 0) return null;
|
|
84
|
+
const fs = fsDep ?? await import("node:fs");
|
|
85
|
+
const filePath = urlsFilePath(projectRoot, existsSyncDep ?? fs.existsSync);
|
|
86
|
+
if (!fs.existsSync(filePath)) return null;
|
|
87
|
+
let raw;
|
|
88
|
+
try {
|
|
89
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(raw);
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
100
|
+
const obj = parsed;
|
|
101
|
+
const result = {};
|
|
102
|
+
const relay = obj.relayBaseUrl;
|
|
103
|
+
if (typeof relay === "string") {
|
|
104
|
+
const trimmed = relay.trim();
|
|
105
|
+
if (trimmed !== "") result.relayBaseUrl = trimmed;
|
|
106
|
+
}
|
|
107
|
+
const relayLocal = obj.relayLocalUrl;
|
|
108
|
+
if (typeof relayLocal === "string") {
|
|
109
|
+
const trimmed = relayLocal.trim();
|
|
110
|
+
if (trimmed !== "") result.relayLocalUrl = trimmed;
|
|
111
|
+
}
|
|
112
|
+
const tunnel = obj.tunnelBaseUrl;
|
|
113
|
+
if (typeof tunnel === "string") {
|
|
114
|
+
const trimmed = tunnel.trim();
|
|
115
|
+
if (trimmed !== "") result.tunnelBaseUrl = trimmed;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { readRelayUrls };
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=relay-url-store-Dv5HSzwb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-url-store-Dv5HSzwb.js","names":[],"sources":["../src/mcp/relay-url-store.ts"],"sourcesContent":["/**\n * Project-local ephemeral URL store (#424).\n *\n * Environment-2 (\"AITC Sandbox PWA\") cold-start requires two ephemeral URLs:\n * - `relayBaseUrl` — the CDP relay's https base (was `AIT_RELAY_BASE_URL`)\n * - `tunnelBaseUrl` — the app's https tunnel base (was `AIT_TUNNEL_BASE_URL`)\n *\n * Quick-tunnel URLs change every run. Previously the user had to copy-paste them\n * into env vars on every cold-start — a single typo silently broke attach. This\n * module replaces that manual hand-off with a file-based discovery pattern that\n * exactly mirrors the `.ait_relay` TOTP-secret store (relay-secret-store.ts).\n *\n * Two surfaces, intentionally split by who is allowed to write:\n *\n * - {@link writeRelayUrls} — WRITE path, called ONLY from the unplugin\n * (env-2 tunnel boot). Writes JSON to `<projectRoot>/.ait_urls` (0600) on\n * every boot (`flag: 'w'` — overwrite). A single file — no directory is\n * created.\n *\n * - {@link readRelayUrls} — READ-ONLY path, called from the MCP daemon as a\n * fallback when `AIT_RELAY_BASE_URL`/`AIT_TUNNEL_BASE_URL` are not set. It\n * NEVER writes, chmods, or creates anything: it only reads an existing\n * `.ait_urls`. On any failure (missing file / bad JSON / wrong shape) it\n * returns `null` silently, letting the downstream assertion be the single\n * fail-fast.\n *\n * - {@link deleteRelayUrls} — called from the unplugin `cleanup()` on\n * teardown (via `void deleteRelayUrls(...)`). A stale `.ait_urls` pointing\n * at a dead tunnel would cause the MCP daemon to attempt a doomed attach on\n * the next cold-start — deletion is non-negotiable. Silently swallows all\n * errors.\n *\n * Design note: the env vars (`AIT_RELAY_BASE_URL`, `AIT_TUNNEL_BASE_URL`) are\n * PRESERVED as operator overrides. The file is the fallback — env wins.\n *\n * Why `nearestPackageJsonDir` instead of `process.cwd()`: see relay-secret-store.ts.\n * The unplugin (writer) and the MCP daemon (reader) both anchor to the nearest\n * package.json from their respective `projectRoot` inputs, ensuring both sides\n * find the same file.\n *\n * SECRET-HANDLING: `relayBaseUrl` and `tunnelBaseUrl` carry the relay/tunnel host\n * — same sensitivity class as `.ait_relay`. The raw URL values, partial values,\n * and the resolved file path MUST NOT appear in any log, error message, stdout,\n * stderr, or assertion output anywhere in this module or at its call sites.\n * Only boolean pass/fail signals are safe to surface. The file is written mode\n * 0600.\n */\n\nimport { join } from 'node:path';\nimport { nearestPackageJsonDir } from './relay-secret-store.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Project-local ephemeral URL file name (single file, not a directory). */\nexport const URLS_FILE_NAME = '.ait_urls';\n\n// ---------------------------------------------------------------------------\n// Dependency injection surfaces\n// ---------------------------------------------------------------------------\n\n/** Minimal fs subset needed by {@link writeRelayUrls} — injectable for tests. */\nexport interface RelayUrlWriteFs {\n writeFileSync(path: string, data: string, options: { mode: number; flag: string }): void;\n existsSync(path: string): boolean;\n}\n\n/** Minimal fs subset needed by {@link readRelayUrls} — injectable for tests. */\nexport interface RelayUrlReadFs {\n existsSync(path: string): boolean;\n readFileSync(path: string, encoding: BufferEncoding): string;\n}\n\n/** Minimal fs subset needed by {@link deleteRelayUrls} — injectable for tests. */\nexport interface RelayUrlDeleteFs {\n existsSync(path: string): boolean;\n unlinkSync(path: string): void;\n}\n\n// ---------------------------------------------------------------------------\n// Path helper\n// ---------------------------------------------------------------------------\n\n/**\n * Absolute path to the project-local `.ait_urls` file for a given start\n * directory (resolved against the nearest package.json directory).\n *\n * Exported so tests can compute the expected path without duplicating the\n * resolution logic.\n */\nexport function urlsFilePath(start: string, existsSyncFn: (path: string) => boolean): string {\n return join(nearestPackageJsonDir(start, existsSyncFn), URLS_FILE_NAME);\n}\n\n// ---------------------------------------------------------------------------\n// WRITE path (unplugin only) — overwrite on every boot\n// ---------------------------------------------------------------------------\n\nexport interface WriteRelayUrlsDeps {\n /** Project root (typically Vite `server.config.root`). */\n projectRoot: string;\n /**\n * The CDP relay's https base URL (same value as `AIT_RELAY_BASE_URL`).\n * Omit when the relay was not started (e.g. `cdp:false`).\n * SECRET-HANDLING: never log this value.\n */\n relayBaseUrl?: string;\n /**\n * The CDP relay's LOCAL http base URL (`http://127.0.0.1:<relay-port>`).\n * Set when `cdp: true` and the local relay port is known. Used by the MCP\n * daemon's `bootExternalRelayFamily` to build the Chii inspector URL against\n * the local relay rather than the cloudflare tunnel, so front_end page load\n * and the client WS leg do not traverse the tunnel (issue #530).\n * SECRET-HANDLING: local loopback URL — no tunnel host, safe to surface.\n */\n relayLocalUrl?: string;\n /**\n * The app tunnel's https base URL (same value as `AIT_TUNNEL_BASE_URL`).\n * Omit when no tunnel URL is available.\n * SECRET-HANDLING: never log this value.\n */\n tunnelBaseUrl?: string;\n /** Filesystem operations. Defaults to node:fs synchronous functions. */\n fs?: RelayUrlWriteFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n}\n\n/**\n * Writes `{ relayBaseUrl, tunnelBaseUrl }` (omitting absent keys) to\n * `<projectRoot>/.ait_urls` (mode 0600). Uses `flag: 'w'` (overwrite) because\n * URLs are ephemeral — a fresh URL replaces the previous one on every boot.\n *\n * Unlike the `.ait_relay` secret store this does NOT use `O_EXCL` (`'wx'`):\n * there is no race concern here (only the unplugin writes this file) and the\n * URL must be fresh on every cold-start.\n *\n * Called ONLY from the unplugin (env-2 tunnel boot). The MCP daemon uses\n * {@link readRelayUrls} (read-only) — it must never write.\n *\n * SECRET-HANDLING: URL values are never logged.\n */\nexport async function writeRelayUrls(deps: WriteRelayUrlsDeps): Promise<void> {\n const { projectRoot, relayBaseUrl, tunnelBaseUrl, fs: fsDep, existsSync: existsSyncDep } = deps;\n\n const fs: RelayUrlWriteFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const filePath = urlsFilePath(projectRoot, existsSyncFn);\n\n // Build the payload — omit keys whose values are absent.\n const payload: { relayBaseUrl?: string; relayLocalUrl?: string; tunnelBaseUrl?: string } = {};\n if (typeof relayBaseUrl === 'string' && relayBaseUrl !== '') {\n payload.relayBaseUrl = relayBaseUrl;\n }\n const { relayLocalUrl } = deps;\n if (typeof relayLocalUrl === 'string' && relayLocalUrl !== '') {\n payload.relayLocalUrl = relayLocalUrl;\n }\n if (typeof tunnelBaseUrl === 'string' && tunnelBaseUrl !== '') {\n payload.tunnelBaseUrl = tunnelBaseUrl;\n }\n\n // SECRET-HANDLING: JSON content (which includes URL values) is written to\n // the file only — never to any log, stdout, or stderr.\n const data = JSON.stringify(payload);\n\n // Overwrite on every boot (`flag: 'w'`) — URLs are ephemeral.\n fs.writeFileSync(filePath, data, { mode: 0o600, flag: 'w' });\n}\n\n// ---------------------------------------------------------------------------\n// READ-ONLY path (daemon only) — never writes, chmods, or creates anything\n// ---------------------------------------------------------------------------\n\nexport interface ReadRelayUrlsDeps {\n /** Project root supplied per-debug-session. When omitted, returns `null`. */\n projectRoot?: string;\n /** Read-only filesystem operations. Defaults to node:fs (existsSync + readFileSync). */\n fs?: RelayUrlReadFs;\n /** existsSync used to resolve the nearest package.json directory. Defaults to node:fs. */\n existsSync?: (path: string) => boolean;\n}\n\n/**\n * Reads `<projectRoot>/.ait_urls` and returns the stored URLs, or `null` on\n * any failure.\n *\n * Strictly READ-ONLY: only `existsSync` + `readFileSync`. Never writes,\n * chmods, or creates files/directories.\n *\n * Returns `null` (silently, no throw, no log) on:\n * - missing `projectRoot`\n * - missing `.ait_urls` file\n * - unreadable file (permissions, transient FS error)\n * - invalid JSON\n * - wrong shape (non-object, non-string values)\n *\n * Trims string values before returning. Ignores non-string fields.\n *\n * SECRET-HANDLING: URL values and the file path are never logged.\n */\nexport async function readRelayUrls(deps?: ReadRelayUrlsDeps): Promise<{\n relayBaseUrl?: string;\n relayLocalUrl?: string;\n tunnelBaseUrl?: string;\n} | null> {\n const { projectRoot, fs: fsDep, existsSync: existsSyncDep } = deps ?? {};\n\n if (projectRoot === undefined) {\n return null;\n }\n\n const fs: RelayUrlReadFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const filePath = urlsFilePath(projectRoot, existsSyncFn);\n\n if (!fs.existsSync(filePath)) {\n return null;\n }\n\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, 'utf8');\n } catch {\n // Unreadable file — silent no-op, let the downstream assert be the fail-fast.\n // SECRET-HANDLING: the error and path are not surfaced.\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n // Invalid JSON — silent no-op.\n return null;\n }\n\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return null;\n }\n\n const obj = parsed as Record<string, unknown>;\n const result: { relayBaseUrl?: string; relayLocalUrl?: string; tunnelBaseUrl?: string } = {};\n\n const relay = obj.relayBaseUrl;\n if (typeof relay === 'string') {\n const trimmed = relay.trim();\n if (trimmed !== '') result.relayBaseUrl = trimmed;\n }\n\n const relayLocal = obj.relayLocalUrl;\n if (typeof relayLocal === 'string') {\n const trimmed = relayLocal.trim();\n if (trimmed !== '') result.relayLocalUrl = trimmed;\n }\n\n const tunnel = obj.tunnelBaseUrl;\n if (typeof tunnel === 'string') {\n const trimmed = tunnel.trim();\n if (trimmed !== '') result.tunnelBaseUrl = trimmed;\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// DELETE path (unplugin cleanup only)\n// ---------------------------------------------------------------------------\n\nexport interface DeleteRelayUrlsDeps {\n /** Project root. */\n projectRoot: string;\n /** Filesystem operations. Defaults to node:fs (existsSync + unlinkSync). */\n fs?: RelayUrlDeleteFs;\n /** existsSync used to resolve the nearest package.json directory. */\n existsSync?: (path: string) => boolean;\n}\n\n/**\n * Removes `<projectRoot>/.ait_urls` if present. Silently swallows ENOENT and\n * any other error so cleanup always succeeds.\n *\n * Called ONLY from the unplugin's `cleanup()` on `httpServer 'close'` + signals\n * (via `void deleteRelayUrls(...)`). A stale `.ait_urls` pointing at a dead\n * tunnel would cause the MCP daemon to attempt a doomed attach on the next\n * cold-start — deletion is non-negotiable.\n *\n * SECRET-HANDLING: the file path is never logged.\n */\nexport async function deleteRelayUrls(deps: DeleteRelayUrlsDeps): Promise<void> {\n const { projectRoot, fs: fsDep, existsSync: existsSyncDep } = deps;\n\n const fs: RelayUrlDeleteFs = fsDep ?? (await import('node:fs'));\n const existsSyncFn: (path: string) => boolean = existsSyncDep ?? fs.existsSync;\n\n const filePath = urlsFilePath(projectRoot, existsSyncFn);\n\n try {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n }\n } catch {\n // Swallow ENOENT and any other error — cleanup is best-effort.\n // SECRET-HANDLING: the path is not logged.\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,iBAAiB;;;;;;;;AAmC9B,SAAgB,aAAa,OAAe,cAAiD;AAC3F,QAAO,KAAK,sBAAsB,OAAO,aAAa,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;AA+GzE,eAAsB,cAAc,MAI1B;CACR,MAAM,EAAE,aAAa,IAAI,OAAO,YAAY,kBAAkB,QAAQ,EAAE;AAExE,KAAI,gBAAgB,KAAA,EAClB,QAAO;CAGT,MAAM,KAAqB,SAAU,MAAM,OAAO;CAGlD,MAAM,WAAW,aAAa,aAFkB,iBAAiB,GAAG,WAEZ;AAExD,KAAI,CAAC,GAAG,WAAW,SAAS,CAC1B,QAAO;CAGT,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,UAAU,OAAO;SACjC;AAGN,SAAO;;CAGT,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AAEN,SAAO;;AAGT,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,QAAO;CAGT,MAAM,MAAM;CACZ,MAAM,SAAoF,EAAE;CAE5F,MAAM,QAAQ,IAAI;AAClB,KAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,UAAU,MAAM,MAAM;AAC5B,MAAI,YAAY,GAAI,QAAO,eAAe;;CAG5C,MAAM,aAAa,IAAI;AACvB,KAAI,OAAO,eAAe,UAAU;EAClC,MAAM,UAAU,WAAW,MAAM;AACjC,MAAI,YAAY,GAAI,QAAO,gBAAgB;;CAG7C,MAAM,SAAS,IAAI;AACnB,KAAI,OAAO,WAAW,UAAU;EAC9B,MAAM,UAAU,OAAO,MAAM;AAC7B,MAAI,YAAY,GAAI,QAAO,gBAAgB;;AAG7C,QAAO"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { t as BundleOptions } from "./bundle-
|
|
2
|
-
import { t as
|
|
3
|
-
import {
|
|
1
|
+
import { t as BundleOptions } from "./bundle-CA1TCXED.js";
|
|
2
|
+
import { t as AitCaptureLine } from "./capture-BmK_cBqQ.js";
|
|
3
|
+
import { t as CdpConnection } from "./cdp-connection-DB2zgthr.js";
|
|
4
|
+
import { n as TestResult, t as RunReport } from "./runtime-D57Rtnsd.js";
|
|
4
5
|
|
|
5
6
|
//#region src/test-runner/relay-worker.d.ts
|
|
6
7
|
/** Per-file result in the aggregate `RunReport`. */
|
|
@@ -27,6 +28,17 @@ interface RelayRunReport {
|
|
|
27
28
|
skipped: number;
|
|
28
29
|
total: number;
|
|
29
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* `__AIT_CAPTURE__` lines harvested from the page console during the run
|
|
33
|
+
* (additive field, devtools#696). Empty unless `collectCaptures` was set.
|
|
34
|
+
* Each entry is opaque (`{ category, json }`) — devtools does not interpret
|
|
35
|
+
* the record shape, only forwards it for downstream 2.x↔3.0 diffing.
|
|
36
|
+
*
|
|
37
|
+
* SECRET-HANDLING: only lines matching the `__AIT_CAPTURE__ ` allowlist
|
|
38
|
+
* prefix reach here; relay/wss/scheme noise lines are dropped in
|
|
39
|
+
* `parseCaptureLines`.
|
|
40
|
+
*/
|
|
41
|
+
captures: AitCaptureLine[];
|
|
30
42
|
}
|
|
31
43
|
/** Options for `runTestFilesOverRelay`. */
|
|
32
44
|
interface RelayRunOptions {
|
|
@@ -39,6 +51,18 @@ interface RelayRunOptions {
|
|
|
39
51
|
* Increase for long-running suites or split the file.
|
|
40
52
|
*/
|
|
41
53
|
timeoutMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* When `true`, registers a live `Runtime.consoleAPICalled` listener for the
|
|
56
|
+
* duration of the run and harvests `__AIT_CAPTURE__` lines into
|
|
57
|
+
* `RelayRunReport.captures`. Defaults to **false** so the build-only
|
|
58
|
+
* eval/e2e path (and the Vitest pool) pay no listener/state overhead —
|
|
59
|
+
* `captures` is then an empty array.
|
|
60
|
+
*
|
|
61
|
+
* The listener is registered just BEFORE the first file runs and removed in a
|
|
62
|
+
* `finally` so it never accumulates across runs (no ring-buffer drain — the
|
|
63
|
+
* default 500-entry buffer would silently drop lines via `shift`).
|
|
64
|
+
*/
|
|
65
|
+
collectCaptures?: boolean;
|
|
42
66
|
}
|
|
43
67
|
/**
|
|
44
68
|
* Runs all `files` sequentially over the given CDP `connection`.
|
|
@@ -71,4 +95,4 @@ declare function flattenResults(report: RelayRunReport): Array<TestResult & {
|
|
|
71
95
|
}>;
|
|
72
96
|
//#endregion
|
|
73
97
|
export { runTestFilesOverRelay as a, flattenResults as i, RelayRunOptions as n, RelayRunReport as r, FileResult as t };
|
|
74
|
-
//# sourceMappingURL=relay-worker-
|
|
98
|
+
//# sourceMappingURL=relay-worker-D-3aAyDO.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-worker-D-3aAyDO.d.ts","names":[],"sources":["../src/test-runner/relay-worker.ts"],"mappings":";;;;;;;UAwBiB,UAAA;EAcf;EAZA,IAAA;EAcA;EAZA,MAAA,EAAQ,SAAA;IAAc,KAAA;EAAA;AAAA;;UAIP,cAAA;EAwBS;EAtBxB,SAAA;EA0Be;EAxBf,QAAA;;EAEA,KAAA,EAAO,UAAA;EA0BP;EAxBA,MAAA;IACE,MAAA;IACA,MAAA;IACA,OAAA;IACA,KAAA;EAAA;EA6DuC;;;;;;;;;;EAjDzC,QAAA,EAAU,cAAA;AAAA;;UAIK,eAAA;EAiDN;;;EA7CT,aAAA,GAAgB,aAAA;EA8LY;;;;EAzL5B,SAAA;EAyL2D;;;;;;;;;;;EA7K3D,eAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;iBAwBoB,qBAAA,CACpB,UAAA,EAAY,aAAA,EACZ,KAAA,YACA,IAAA,GAAO,eAAA,GACN,OAAA,CAAQ,cAAA;;;;;iBAiJK,cAAA,CAAe,MAAA,EAAQ,cAAA,GAAiB,KAAA,CAAM,UAAA;EAAe,IAAA;AAAA"}
|
|
@@ -189,4 +189,4 @@ declare const runtimeGlobals: {
|
|
|
189
189
|
declare function runTestModule(moduleFactory?: () => void | Promise<void>): Promise<RunReport>;
|
|
190
190
|
//#endregion
|
|
191
191
|
export { runtimeGlobals as i, TestResult as n, runTestModule as r, RunReport as t };
|
|
192
|
-
//# sourceMappingURL=runtime-
|
|
192
|
+
//# sourceMappingURL=runtime-D57Rtnsd.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-
|
|
1
|
+
{"version":3,"file":"runtime-D57Rtnsd.d.ts","names":[],"sources":["../src/test-runner/runtime.ts"],"mappings":";;AA8BA;;;;;;;;;;AAYA;;;;;;;;;;;;;AASC;UArBgB,UAAA;;EAEf,IAAA;EAgCmD;EA9BnD,MAAA;EAsCyB;EApCzB,QAAA;EAsCiB;EApCjB,KAAA;AAAA;;UAIe,SAAA;EAoCgB;EAlC/B,SAAA;EAgDG;EA9CH,QAAA;EACA,MAAA;EACA,MAAA;EACA,OAAA;EACA,KAAA,EAAO,UAAA;AAAA;;;;;;;;cAcH,gBAAA;;;;;;;UAQI,iBAAA;EA8RQ;EAAA,CA5Rf,gBAAA;EAmSY;EAjSb,QAAA;EAwSU;EAtSV,eAAA,CAAgB,MAAA;AAAA;;KAcb,OAAA,oBAA2B,IAAA;;cAoN1B,WAAA;EAAA;cAIQ,QAAA;EAAA,IAIR,GAAA,CAAA;EAaJ,IAAA,CAAK,QAAA;EAOL,OAAA,CAAQ,QAAA;EAOR,UAAA,CAAA;EAIA,SAAA,CAAA;EAIA,QAAA,CAAA;EAIA,aAAA,CAAA;EAOA,eAAA,CAAgB,CAAA;EAOhB,YAAA,CAAa,CAAA;EAOb,SAAA,CAAU,GAAA;EAOV,OAAA,CAAQ,WAAA;EAwBR,aAAA,CAAc,QAAA,EAAU,MAAA;EAOxB,cAAA,CAAe,OAAA,aAAoB,IAAA;EAiBnC,cAAA,CAAe,IAAA,mBAAuB,IAAA;EAOtC,UAAA,CAAW,OAAA;AAAA;;;;;;;;;UAgBH,YAAA;EAAA,CACP,QAAA,YAAoB,WAAA;EACrB,GAAA,CAAI,IAAA,EAAM,OAAA,GAAU,iBAAA;EACpB,QAAA,IAAY,iBAAA;EACZ,gBAAA,CAAiB,QAAA,EAAU,MAAA,oBAA0B,iBAAA;EACrD,eAAA,CAAgB,QAAA,cAAsB,iBAAA;EACtC,gBAAA,CAAiB,GAAA,WAAc,iBAAA;EAC/B,cAAA,CAAe,EAAA,WAAa,MAAA,GAAS,iBAAA;AAAA;;iBA8B9B,QAAA,CAAS,IAAA,UAAc,EAAA;AAAA,kBAAvB,QAAA;EAAA,yBAeoB,GAAA;EAAA,+BAgCM,iBAAA;EAAA,8BAED,iBAAA;AAAA;;iBA1CzB,EAAA,CAAG,IAAA,UAAc,EAAA,eAAiB,OAAA;AAAA,kBAAlC,EAAA;EAAA,yBAWc,GAAA,gBAAqB,OAAA;EAAA,+BAiBf,WAAA;EAAA,8BACD,WAAA;AAAA;;;;;AApCkB;;KA6BzC,WAAA,IAAe,IAAA,UAAc,EAAA,eAAiB,OAAA;;;;;;;KAiB9C,iBAAA,IAAqB,IAAA,UAAc,EAAA;AAAA,iBAuD/B,SAAA,CAAU,EAAA,eAAiB,OAAA;AAAA,iBAG3B,QAAA,CAAS,EAAA,eAAiB,OAAA;AAAA,iBAG1B,UAAA,CAAW,EAAA,eAAiB,OAAA;AAAA,iBAG5B,SAAA,CAAU,EAAA,eAAiB,OAAA;AAAA,UAQ1B,OAAA;EACR,IAAA;EACA,WAAA;AAAA;AAAA,UAGQ,MAAA;EAAA,IACJ,IAAA,EAAM,CAAA,GAAI,CAAA;EACd,IAAA;IAAQ,KAAA,EAAO,OAAA;EAAA;EACf,kBAAA,CAAmB,EAAA,MAAQ,IAAA,EAAM,CAAA,KAAM,CAAA;EACvC,eAAA,CAAgB,KAAA,EAAO,CAAA;EACvB,WAAA;AAAA;;;;;;;;;cAqFW,cAAA;EAAA;;;;;;;;;IAxLG,yFA4IE,MAAA,mCAAyC,CAAA,EAAC,GAAA,EACnD,CAAA,EAAC,MAAA,EACE,CAAA,GACP,MAAA,sBA/IqD;+BA+J/B,IAAA,OAAa,IAAA,EAAM,CAAA,KAAM,CAAA,GAAI,MAAA,CAAO,CAAA,EAAG,CAAA,GA/Jf;;;;AAAa;;;;;AAiBV;;;;;AAuDL;;AAxEe,iBAgN1C,aAAA,CACpB,aAAA,gBAA6B,OAAA,SAC5B,OAAA,CAAQ,SAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as BundleResult, r as bundleTestFile, t as BundleOptions } from "../bundle-
|
|
1
|
+
import { n as BundleResult, r as bundleTestFile, t as BundleOptions } from "../bundle-CA1TCXED.js";
|
|
2
2
|
export { BundleOptions, BundleResult, bundleTestFile };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { accessSync } from "node:fs";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
|
-
import * as path from "node:path";
|
|
3
|
+
import * as path$1 from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/test-runner/bundle.ts
|
|
6
6
|
/**
|
|
@@ -243,7 +243,7 @@ function userFactoryPlugin(absPath) {
|
|
|
243
243
|
"}"
|
|
244
244
|
].join("\n"),
|
|
245
245
|
loader: "ts",
|
|
246
|
-
resolveDir: path.dirname(absPath)
|
|
246
|
+
resolveDir: path$1.dirname(absPath)
|
|
247
247
|
};
|
|
248
248
|
});
|
|
249
249
|
}
|
|
@@ -267,17 +267,17 @@ function userFactoryPlugin(absPath) {
|
|
|
267
267
|
* error rather than a cryptic failure.
|
|
268
268
|
*/
|
|
269
269
|
function getRuntimePath() {
|
|
270
|
-
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
270
|
+
const dir = path$1.dirname(fileURLToPath(import.meta.url));
|
|
271
271
|
const candidates = [
|
|
272
|
-
path.join(dir, "runtime.ts"),
|
|
273
|
-
path.join(dir, "runtime.js"),
|
|
274
|
-
path.join(dir, "..", "test-runner", "runtime.js")
|
|
272
|
+
path$1.join(dir, "runtime.ts"),
|
|
273
|
+
path$1.join(dir, "runtime.js"),
|
|
274
|
+
path$1.join(dir, "..", "test-runner", "runtime.js")
|
|
275
275
|
];
|
|
276
276
|
for (const candidate of candidates) try {
|
|
277
277
|
accessSync(candidate);
|
|
278
278
|
return candidate;
|
|
279
279
|
} catch {}
|
|
280
|
-
return path.join(dir, "runtime.js");
|
|
280
|
+
return path$1.join(dir, "runtime.js");
|
|
281
281
|
}
|
|
282
282
|
/**
|
|
283
283
|
* Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
|
|
@@ -307,7 +307,7 @@ async function bundleTestFile(absPath, opts) {
|
|
|
307
307
|
stdin: {
|
|
308
308
|
contents: wrapperContent,
|
|
309
309
|
loader: "ts",
|
|
310
|
-
resolveDir: path.dirname(absPath)
|
|
310
|
+
resolveDir: path$1.dirname(absPath)
|
|
311
311
|
},
|
|
312
312
|
bundle: true,
|
|
313
313
|
format: "iife",
|
|
@@ -324,7 +324,7 @@ async function bundleTestFile(absPath, opts) {
|
|
|
324
324
|
treeShaking: true,
|
|
325
325
|
footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
|
|
326
326
|
});
|
|
327
|
-
const warnings = result.warnings.map((w) => `${path.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
|
|
327
|
+
const warnings = result.warnings.map((w) => `${path$1.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
|
|
328
328
|
const outputFile = result.outputFiles?.[0];
|
|
329
329
|
if (!outputFile) throw new Error("bundleTestFile: esbuild produced no output — check entryPoints");
|
|
330
330
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.js","names":[],"sources":["../../src/test-runner/bundle.ts"],"sourcesContent":["/**\n * esbuild-based bundler for user test files.\n *\n * Bundles a single test file into a self-contained IIFE string that can be\n * injected into a WebView via `Runtime.evaluate`. The bundle includes the\n * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and\n * the `runTestModule(factory)` entry point.\n *\n * ## How the wiring works\n *\n * The bundle exposes two exports on `globalThis.__testBundle`:\n * - `runTestModule` — the runtime's entry function.\n * - `__userFactory` — an async function whose body is the user's top-level\n * test registration code (describe/it/test calls).\n *\n * The Node-side RPC (`rpc.ts`) calls:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * `runTestModule` then installs `describe/it/test/expect` as globals, invokes\n * the factory (which registers all tests), runs them, and returns a `RunReport`.\n *\n * ## Why a factory wrapper is needed\n *\n * Naively adding the runtime to `entryPoints` and bundling the user file would\n * fail for two reasons:\n * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE\n * scope. The user's top-level `describe(...)` calls expect them as globals —\n * they are not globals until `runTestModule` installs them.\n * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation\n * time, before the RPC layer calls `runTestModule` to reset state and start\n * the test clock.\n *\n * The factory approach solves both: the user's registration code is deferred\n * into a function that `runTestModule` calls AFTER installing the globals.\n *\n * ## Factory extraction algorithm\n *\n * The `userFactoryPlugin` reads the user file and splits lines into:\n * - **top-level**: `import …` and re-export lines — kept at module scope\n * (the only valid position for static `import` in ESM).\n * - **body**: all other statements — moved into the body of the exported\n * `__userFactory` async function.\n *\n * esbuild processes the re-generated module, following each static import\n * through the normal dependency graph (including the SDK-redirect plugin).\n *\n * ## SDK redirect\n *\n * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via\n * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that\n * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.\n *\n * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.\n */\n\nimport { accessSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n// esbuild is imported for TYPES only at module scope; the runtime module is\n// loaded lazily inside `bundleTestFile` via dynamic import. esbuild runs a\n// startup invariant check (`TextEncoder().encode('') instanceof Uint8Array`)\n// that fails in a jsdom realm — a static import would break every MCP/test\n// module that merely *imports* this file's transitive graph (e.g. debug-server →\n// run_tests). Lazy load keeps esbuild off the import graph until a bundle is\n// actually built, and mirrors the cloudflared/chii dynamic-import precedent.\nimport type * as esbuild from 'esbuild';\n\n/** Options accepted by `bundleTestFile`. */\nexport interface BundleOptions {\n /**\n * Additional esbuild `external` patterns. The SDK package\n * (`@apps-in-toss/web-framework` and `@apps-in-toss/web-framework/*`) is\n * always handled by the SDK redirect plugin — callers may add more patterns\n * to be left as globals.\n */\n extraExternals?: string[];\n /**\n * Global name for the IIFE output object. Defaults to `__testBundle`.\n * The runtime entry uses this to call `__testBundle.runTestModule(__userFactory)`.\n */\n globalName?: string;\n}\n\n/**\n * The result of bundling a test file.\n * `code` is a self-contained IIFE string ready for `Runtime.evaluate`.\n */\nexport interface BundleResult {\n code: string;\n warnings: string[];\n}\n\n/** The SDK package name that mini-app test code imports from. */\nconst SDK_PACKAGE = '@apps-in-toss/web-framework';\n\n/**\n * Names the runtime installs as globals before invoking the user factory.\n * The `vitest` virtual module re-exports each as a lazy getter that reads from\n * `globalThis` at access time. Keep in sync with the globals installed in\n * `runtime.ts#runTestModule`.\n */\nconst VITEST_GLOBAL_NAMES = [\n 'describe',\n 'it',\n 'test',\n 'expect',\n 'beforeAll',\n 'afterAll',\n 'beforeEach',\n 'afterEach',\n 'vi',\n] as const;\n\n/**\n * Matches the bare SDK package and any sub-path import\n * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).\n * Built from {@link SDK_PACKAGE} so the package name has a single source.\n */\nconst SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`);\n\n/**\n * esbuild plugin that intercepts SDK imports and redirects them to the\n * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.\n *\n * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),\n * esbuild resolves it to a virtual module that re-exports all named exports\n * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not\n * be available in the test environment) while still making named imports work.\n *\n * If `window.__sdk` is absent (non-dog-food build), every access throws a\n * descriptive error rather than returning `undefined` silently.\n */\nfunction sdkRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'sdk-redirect',\n setup(build) {\n // Match the bare package and any sub-path imports\n build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({\n path: args.path,\n namespace: 'sdk-redirect',\n }));\n\n build.onLoad({ filter: /.*/, namespace: 'sdk-redirect' }, () => ({\n // Generate a virtual CommonJS-style module so that esbuild does NOT perform\n // strict named-export matching. When `format:'iife'` bundles a CJS module,\n // it wraps it with its own __toCommonJS helper and satisfies named imports\n // via property access on the module.exports object — which is our Proxy.\n // This means `import { getPlatformOS } from '...'` becomes\n // `__proxy.getPlatformOS` at runtime, which correctly reads from window.__sdk.\n contents: `\nvar __proxy = (typeof window !== 'undefined' && window.__sdk)\n ? window.__sdk\n : new Proxy({}, {\n get: function(_t, p) {\n throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));\n }\n });\nmodule.exports = __proxy;\n`,\n loader: 'js',\n }));\n },\n };\n}\n\n/**\n * esbuild plugin that intercepts `import … from 'vitest'` and replaces it with\n * a virtual module that delegates every named import to `globalThis` at ACCESS\n * time (not at bundle-evaluation time).\n *\n * The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/\n * afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle\n * IIFE is evaluated. A value-copy redirect (`export var describe =\n * globalThis.describe`) would therefore capture `undefined` at evaluation time\n * and the user's `describe(...)` calls would be no-ops — registering zero tests.\n *\n * The fix defers the lookup to call time using per-name **getter** exports.\n * We emit a CommonJS module that:\n * 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named\n * import directly to a property access on the module (NOT wrapped under a\n * `default` shim — which is what happens for a bare Proxy whose own-keys\n * are empty, leaving every named import `undefined`);\n * 2. defines each global name as a getter that reads `globalThis[name]` on\n * every access. So `import { describe } from 'vitest'` compiles to\n * `import_vitest.describe`, whose getter returns the real `describe` only\n * when the factory calls it — after `runTestModule` installs the globals.\n *\n * A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes\n * the virtual module through `__toESM`, which enumerates own-keys (none on an\n * empty Proxy target) and therefore exposes zero named exports. Explicit getter\n * properties give `__toESM` real keys to map while keeping access lazy.\n */\nfunction vitestRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'vitest-redirect',\n setup(build) {\n build.onResolve({ filter: /^vitest$/ }, () => ({\n path: 'vitest',\n namespace: 'vitest-redirect',\n }));\n\n build.onLoad({ filter: /^vitest$/, namespace: 'vitest-redirect' }, () => {\n const getters = VITEST_GLOBAL_NAMES.map(\n (name) =>\n `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`,\n ).join('\\n');\n // __esModule lets esbuild __toESM map named imports to these getters\n // directly. Each getter reads globalThis lazily so the value resolves at\n // call time — after runTestModule installs the globals.\n return {\n contents: `Object.defineProperty(exports, '__esModule', { value: true });\\n${getters}\\n`,\n loader: 'js',\n };\n });\n },\n };\n}\n\n/**\n * esbuild plugin that transforms the user test file into a module that exports\n * an async `__userFactory` function. The factory defers the user's top-level\n * test registration code (describe/it/test calls) so it only runs when\n * `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has\n * installed describe/it/test/expect as globals.\n *\n * Algorithm:\n * - Import declarations and re-export statements are kept at module top-level\n * (the only valid ESM position for static `import`). A statement that spans\n * multiple lines — e.g. a named import with one member per line:\n * import {\n * appLogin,\n * getAnonymousKey,\n * } from '@apps-in-toss/web-framework';\n * is tracked as a single block: every line from the opening `import {` /\n * `export {` through the closing `from '…'` (or side-effect `'…'`) line is\n * kept together at top-level. This prevents the member lines and the\n * closing `} from '…'` line from leaking into the factory body, which would\n * leave an unterminated `import {` at module scope (the #678 env3 failure:\n * esbuild threw `Expected \"as\" but found \"{\"` on multi-line SDK imports).\n * - All other lines (describe/it/test calls, local declarations, etc.) are\n * moved into the body of the exported async factory function.\n *\n * This preserves SDK import resolution (the sdk-redirect plugin processes\n * top-level imports normally) while deferring test registration to the factory.\n */\nfunction userFactoryPlugin(absPath: string): esbuild.Plugin {\n const NAMESPACE = 'user-test-factory';\n return {\n name: 'user-test-factory',\n setup(build) {\n // Resolve the virtual \"user-test-factory\" specifier to our namespace.\n build.onResolve({ filter: /^user-test-factory$/ }, () => ({\n path: absPath,\n namespace: NAMESPACE,\n }));\n\n // Load the user file, split imports from body, wrap body in the factory.\n build.onLoad({ filter: /.*/, namespace: NAMESPACE }, async (args) => {\n const source = await fs.readFile(args.path, 'utf8');\n const lines = source.split('\\n');\n\n const topLevelLines: string[] = [];\n const bodyLines: string[] = [];\n\n // Matches `export` value declarations that cannot appear inside a\n // function body. We strip the `export` keyword so they become plain\n // declarations inside the factory.\n const EXPORT_DECLARATION_RE =\n /^(export\\s+)(default\\s+|async\\s+function\\s+|function\\s+|class\\s+|const\\s+|let\\s+|var\\s+)/;\n\n // True when `trimmed` begins a static `import` statement.\n const isImportStart = (trimmed: string): boolean =>\n trimmed.startsWith('import ') ||\n trimmed.startsWith('import{') ||\n trimmed.startsWith(\"import'\") ||\n trimmed.startsWith('import\"');\n\n // A module-scope `import`/`export … from` statement is \"complete\" on a\n // single line when it ends with a quoted module specifier (optionally\n // followed by `;`/whitespace), e.g. `… from '@x';` or side-effect\n // `import './x';`. A line ending in `{` or `,` (the common multi-line\n // named-import shape) is therefore NOT complete and must accumulate\n // further lines until the closing `from '…'` line.\n const endsStatement = (trimmed: string): boolean =>\n /['\"]\\s*;?\\s*$/.test(trimmed.replace(/\\/\\/.*$/, '').trimEnd());\n\n // When set, we are inside an unterminated multi-line import/re-export\n // block: every subsequent line stays at top level until the block ends.\n let inImportBlock = false;\n\n for (const line of lines) {\n const trimmed = line.trimStart();\n const indent = line.slice(0, line.length - trimmed.length);\n\n // Continuation of a multi-line import / re-export block — keep at\n // top level. The block ends on the line that terminates the\n // statement (closing `from '…'` / side-effect `'…'`).\n if (inImportBlock) {\n topLevelLines.push(line);\n if (endsStatement(trimmed)) {\n inImportBlock = false;\n }\n continue;\n }\n\n // Static import declarations must stay at module top level\n // (the ESM spec forbids `import` inside a function body). If the\n // statement does not terminate on this line, open an import block so\n // the member lines and closing `} from '…'` line stay top-level too.\n if (isImportStart(trimmed)) {\n topLevelLines.push(line);\n if (!endsStatement(trimmed)) {\n inImportBlock = true;\n }\n } else if (trimmed.startsWith('export ')) {\n // Determine whether this is a re-export (stays top-level) or a value\n // declaration (goes into the factory, export keyword stripped).\n const m = trimmed.match(EXPORT_DECLARATION_RE);\n if (m) {\n // Value declaration — strip `export ` and move into factory body.\n // e.g. `export function hello()` → `function hello()`\n // `export const x = 1` → `const x = 1`\n bodyLines.push(indent + trimmed.slice('export '.length));\n } else {\n // Re-export or `export type { … }` — stays at top level. A\n // multi-line `export { … } from '…'` opens an import block too.\n topLevelLines.push(line);\n if (/\\bfrom\\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith('{')) {\n inImportBlock = true;\n }\n }\n } else {\n bodyLines.push(line);\n }\n }\n\n const factoryContent = [\n ...topLevelLines,\n '',\n '// biome-ignore lint: generated factory wrapper',\n 'export default async function __userFactory(): Promise<void> {',\n ...bodyLines.map((l) => ` ${l}`),\n '}',\n ].join('\\n');\n\n return {\n contents: factoryContent,\n loader: 'ts',\n resolveDir: path.dirname(absPath),\n };\n });\n },\n };\n}\n\n/**\n * Returns the absolute path to the test-runner runtime module.\n *\n * Searches candidates in priority order:\n * 1. Co-located `runtime.ts` / `runtime.js` — covers the source tree\n * (tsx / ts-node) and the `dist/test-runner/` entry.\n * 2. `../test-runner/runtime.js` — covers the `dist/mcp/cli.js` entry,\n * where `import.meta.url` resolves to `dist/mcp/` (a sibling directory\n * of `dist/test-runner/`). Without this second candidate the MCP entry\n * point would look for `dist/mcp/runtime.js`, which does not exist, and\n * every `run_tests` call would fail with an esbuild \"Could not resolve\"\n * error (#678).\n *\n * Returns the first candidate that exists on disk. Falls back to the\n * co-located `runtime.js` path so esbuild produces a clear \"file not found\"\n * error rather than a cryptic failure.\n */\nfunction getRuntimePath(): string {\n const dir = path.dirname(fileURLToPath(import.meta.url));\n const candidates = [\n path.join(dir, 'runtime.ts'),\n path.join(dir, 'runtime.js'),\n path.join(dir, '..', 'test-runner', 'runtime.js'),\n ];\n for (const candidate of candidates) {\n try {\n accessSync(candidate);\n return candidate;\n } catch {\n // try next candidate\n }\n }\n // Let esbuild produce a \"file not found\" error with a clear path.\n return path.join(dir, 'runtime.js');\n}\n\n/**\n * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.\n *\n * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:\n * - `runTestModule` — the runtime entry (from `runtime.ts`).\n * - `__userFactory` — an async function wrapping the user's test registration\n * code so it runs AFTER `runTestModule` installs the globals.\n *\n * Callers (rpc.ts) invoke:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * @param absPath - Absolute path to the user test file.\n * @param opts - Optional bundling overrides.\n */\nexport async function bundleTestFile(absPath: string, opts?: BundleOptions): Promise<BundleResult> {\n const globalName = opts?.globalName ?? '__testBundle';\n const extraExternals = opts?.extraExternals ?? [];\n\n // Lazy load esbuild at call time (see the module-scope import note).\n const esbuild = await import('esbuild');\n const runtimePath = getRuntimePath();\n\n // Stdin wrapper: import the runtime and the user factory, re-export both.\n // esbuild follows the static imports to include runtime.ts and the user file\n // (via the userFactoryPlugin) in the single IIFE output.\n const wrapperContent = [\n `import { runTestModule } from ${JSON.stringify(runtimePath)};`,\n `import __userFactory from \"user-test-factory\";`,\n `export { runTestModule, __userFactory };`,\n ].join('\\n');\n\n const result = await esbuild.build({\n stdin: {\n contents: wrapperContent,\n loader: 'ts',\n // resolveDir is used for relative imports from the wrapper. Since the\n // wrapper only imports absolute paths (runtimePath) and the virtual\n // \"user-test-factory\" specifier (resolved by plugin), the directory\n // doesn't matter — but we still provide a sensible default.\n resolveDir: path.dirname(absPath),\n },\n bundle: true,\n format: 'iife',\n globalName,\n platform: 'browser',\n target: 'es2022',\n write: false,\n plugins: [userFactoryPlugin(absPath), vitestRedirectPlugin(), sdkRedirectPlugin()],\n external: extraExternals,\n treeShaking: true,\n // Ensure the IIFE result is always reachable via globalThis regardless of\n // the evaluation context. esbuild's `globalName` emits:\n // var __testBundle = (() => { ... })();\n // When `Runtime.evaluate` runs this bundle code inside an outer wrapper\n // (rpc.ts's async IIFE), `var` creates a local variable — NOT a global\n // property — so `globalThis.__testBundle` stays `undefined`. The footer\n // explicitly assigns the local variable to `globalThis` to close that gap.\n footer: {\n js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};`,\n },\n });\n\n const warnings = result.warnings.map(\n (w) =>\n `${path.relative(process.cwd(), w.location?.file ?? '')}:${w.location?.line ?? '?'}: ${w.text}`,\n );\n\n const outputFile = result.outputFiles?.[0];\n if (!outputFile) {\n throw new Error('bundleTestFile: esbuild produced no output — check entryPoints');\n }\n\n return { code: outputFile.text, warnings };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bundle.js","names":["path"],"sources":["../../src/test-runner/bundle.ts"],"sourcesContent":["/**\n * esbuild-based bundler for user test files.\n *\n * Bundles a single test file into a self-contained IIFE string that can be\n * injected into a WebView via `Runtime.evaluate`. The bundle includes the\n * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and\n * the `runTestModule(factory)` entry point.\n *\n * ## How the wiring works\n *\n * The bundle exposes two exports on `globalThis.__testBundle`:\n * - `runTestModule` — the runtime's entry function.\n * - `__userFactory` — an async function whose body is the user's top-level\n * test registration code (describe/it/test calls).\n *\n * The Node-side RPC (`rpc.ts`) calls:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * `runTestModule` then installs `describe/it/test/expect` as globals, invokes\n * the factory (which registers all tests), runs them, and returns a `RunReport`.\n *\n * ## Why a factory wrapper is needed\n *\n * Naively adding the runtime to `entryPoints` and bundling the user file would\n * fail for two reasons:\n * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE\n * scope. The user's top-level `describe(...)` calls expect them as globals —\n * they are not globals until `runTestModule` installs them.\n * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation\n * time, before the RPC layer calls `runTestModule` to reset state and start\n * the test clock.\n *\n * The factory approach solves both: the user's registration code is deferred\n * into a function that `runTestModule` calls AFTER installing the globals.\n *\n * ## Factory extraction algorithm\n *\n * The `userFactoryPlugin` reads the user file and splits lines into:\n * - **top-level**: `import …` and re-export lines — kept at module scope\n * (the only valid position for static `import` in ESM).\n * - **body**: all other statements — moved into the body of the exported\n * `__userFactory` async function.\n *\n * esbuild processes the re-generated module, following each static import\n * through the normal dependency graph (including the SDK-redirect plugin).\n *\n * ## SDK redirect\n *\n * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via\n * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that\n * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.\n *\n * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.\n */\n\nimport { accessSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n// esbuild is imported for TYPES only at module scope; the runtime module is\n// loaded lazily inside `bundleTestFile` via dynamic import. esbuild runs a\n// startup invariant check (`TextEncoder().encode('') instanceof Uint8Array`)\n// that fails in a jsdom realm — a static import would break every MCP/test\n// module that merely *imports* this file's transitive graph (e.g. debug-server →\n// run_tests). Lazy load keeps esbuild off the import graph until a bundle is\n// actually built, and mirrors the cloudflared/chii dynamic-import precedent.\nimport type * as esbuild from 'esbuild';\n\n/** Options accepted by `bundleTestFile`. */\nexport interface BundleOptions {\n /**\n * Additional esbuild `external` patterns. The SDK package\n * (`@apps-in-toss/web-framework` and `@apps-in-toss/web-framework/*`) is\n * always handled by the SDK redirect plugin — callers may add more patterns\n * to be left as globals.\n */\n extraExternals?: string[];\n /**\n * Global name for the IIFE output object. Defaults to `__testBundle`.\n * The runtime entry uses this to call `__testBundle.runTestModule(__userFactory)`.\n */\n globalName?: string;\n}\n\n/**\n * The result of bundling a test file.\n * `code` is a self-contained IIFE string ready for `Runtime.evaluate`.\n */\nexport interface BundleResult {\n code: string;\n warnings: string[];\n}\n\n/** The SDK package name that mini-app test code imports from. */\nconst SDK_PACKAGE = '@apps-in-toss/web-framework';\n\n/**\n * Names the runtime installs as globals before invoking the user factory.\n * The `vitest` virtual module re-exports each as a lazy getter that reads from\n * `globalThis` at access time. Keep in sync with the globals installed in\n * `runtime.ts#runTestModule`.\n */\nconst VITEST_GLOBAL_NAMES = [\n 'describe',\n 'it',\n 'test',\n 'expect',\n 'beforeAll',\n 'afterAll',\n 'beforeEach',\n 'afterEach',\n 'vi',\n] as const;\n\n/**\n * Matches the bare SDK package and any sub-path import\n * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).\n * Built from {@link SDK_PACKAGE} so the package name has a single source.\n */\nconst SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`);\n\n/**\n * esbuild plugin that intercepts SDK imports and redirects them to the\n * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.\n *\n * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),\n * esbuild resolves it to a virtual module that re-exports all named exports\n * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not\n * be available in the test environment) while still making named imports work.\n *\n * If `window.__sdk` is absent (non-dog-food build), every access throws a\n * descriptive error rather than returning `undefined` silently.\n */\nfunction sdkRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'sdk-redirect',\n setup(build) {\n // Match the bare package and any sub-path imports\n build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({\n path: args.path,\n namespace: 'sdk-redirect',\n }));\n\n build.onLoad({ filter: /.*/, namespace: 'sdk-redirect' }, () => ({\n // Generate a virtual CommonJS-style module so that esbuild does NOT perform\n // strict named-export matching. When `format:'iife'` bundles a CJS module,\n // it wraps it with its own __toCommonJS helper and satisfies named imports\n // via property access on the module.exports object — which is our Proxy.\n // This means `import { getPlatformOS } from '...'` becomes\n // `__proxy.getPlatformOS` at runtime, which correctly reads from window.__sdk.\n contents: `\nvar __proxy = (typeof window !== 'undefined' && window.__sdk)\n ? window.__sdk\n : new Proxy({}, {\n get: function(_t, p) {\n throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));\n }\n });\nmodule.exports = __proxy;\n`,\n loader: 'js',\n }));\n },\n };\n}\n\n/**\n * esbuild plugin that intercepts `import … from 'vitest'` and replaces it with\n * a virtual module that delegates every named import to `globalThis` at ACCESS\n * time (not at bundle-evaluation time).\n *\n * The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/\n * afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle\n * IIFE is evaluated. A value-copy redirect (`export var describe =\n * globalThis.describe`) would therefore capture `undefined` at evaluation time\n * and the user's `describe(...)` calls would be no-ops — registering zero tests.\n *\n * The fix defers the lookup to call time using per-name **getter** exports.\n * We emit a CommonJS module that:\n * 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named\n * import directly to a property access on the module (NOT wrapped under a\n * `default` shim — which is what happens for a bare Proxy whose own-keys\n * are empty, leaving every named import `undefined`);\n * 2. defines each global name as a getter that reads `globalThis[name]` on\n * every access. So `import { describe } from 'vitest'` compiles to\n * `import_vitest.describe`, whose getter returns the real `describe` only\n * when the factory calls it — after `runTestModule` installs the globals.\n *\n * A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes\n * the virtual module through `__toESM`, which enumerates own-keys (none on an\n * empty Proxy target) and therefore exposes zero named exports. Explicit getter\n * properties give `__toESM` real keys to map while keeping access lazy.\n */\nfunction vitestRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'vitest-redirect',\n setup(build) {\n build.onResolve({ filter: /^vitest$/ }, () => ({\n path: 'vitest',\n namespace: 'vitest-redirect',\n }));\n\n build.onLoad({ filter: /^vitest$/, namespace: 'vitest-redirect' }, () => {\n const getters = VITEST_GLOBAL_NAMES.map(\n (name) =>\n `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`,\n ).join('\\n');\n // __esModule lets esbuild __toESM map named imports to these getters\n // directly. Each getter reads globalThis lazily so the value resolves at\n // call time — after runTestModule installs the globals.\n return {\n contents: `Object.defineProperty(exports, '__esModule', { value: true });\\n${getters}\\n`,\n loader: 'js',\n };\n });\n },\n };\n}\n\n/**\n * esbuild plugin that transforms the user test file into a module that exports\n * an async `__userFactory` function. The factory defers the user's top-level\n * test registration code (describe/it/test calls) so it only runs when\n * `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has\n * installed describe/it/test/expect as globals.\n *\n * Algorithm:\n * - Import declarations and re-export statements are kept at module top-level\n * (the only valid ESM position for static `import`). A statement that spans\n * multiple lines — e.g. a named import with one member per line:\n * import {\n * appLogin,\n * getAnonymousKey,\n * } from '@apps-in-toss/web-framework';\n * is tracked as a single block: every line from the opening `import {` /\n * `export {` through the closing `from '…'` (or side-effect `'…'`) line is\n * kept together at top-level. This prevents the member lines and the\n * closing `} from '…'` line from leaking into the factory body, which would\n * leave an unterminated `import {` at module scope (the #678 env3 failure:\n * esbuild threw `Expected \"as\" but found \"{\"` on multi-line SDK imports).\n * - All other lines (describe/it/test calls, local declarations, etc.) are\n * moved into the body of the exported async factory function.\n *\n * This preserves SDK import resolution (the sdk-redirect plugin processes\n * top-level imports normally) while deferring test registration to the factory.\n */\nfunction userFactoryPlugin(absPath: string): esbuild.Plugin {\n const NAMESPACE = 'user-test-factory';\n return {\n name: 'user-test-factory',\n setup(build) {\n // Resolve the virtual \"user-test-factory\" specifier to our namespace.\n build.onResolve({ filter: /^user-test-factory$/ }, () => ({\n path: absPath,\n namespace: NAMESPACE,\n }));\n\n // Load the user file, split imports from body, wrap body in the factory.\n build.onLoad({ filter: /.*/, namespace: NAMESPACE }, async (args) => {\n const source = await fs.readFile(args.path, 'utf8');\n const lines = source.split('\\n');\n\n const topLevelLines: string[] = [];\n const bodyLines: string[] = [];\n\n // Matches `export` value declarations that cannot appear inside a\n // function body. We strip the `export` keyword so they become plain\n // declarations inside the factory.\n const EXPORT_DECLARATION_RE =\n /^(export\\s+)(default\\s+|async\\s+function\\s+|function\\s+|class\\s+|const\\s+|let\\s+|var\\s+)/;\n\n // True when `trimmed` begins a static `import` statement.\n const isImportStart = (trimmed: string): boolean =>\n trimmed.startsWith('import ') ||\n trimmed.startsWith('import{') ||\n trimmed.startsWith(\"import'\") ||\n trimmed.startsWith('import\"');\n\n // A module-scope `import`/`export … from` statement is \"complete\" on a\n // single line when it ends with a quoted module specifier (optionally\n // followed by `;`/whitespace), e.g. `… from '@x';` or side-effect\n // `import './x';`. A line ending in `{` or `,` (the common multi-line\n // named-import shape) is therefore NOT complete and must accumulate\n // further lines until the closing `from '…'` line.\n const endsStatement = (trimmed: string): boolean =>\n /['\"]\\s*;?\\s*$/.test(trimmed.replace(/\\/\\/.*$/, '').trimEnd());\n\n // When set, we are inside an unterminated multi-line import/re-export\n // block: every subsequent line stays at top level until the block ends.\n let inImportBlock = false;\n\n for (const line of lines) {\n const trimmed = line.trimStart();\n const indent = line.slice(0, line.length - trimmed.length);\n\n // Continuation of a multi-line import / re-export block — keep at\n // top level. The block ends on the line that terminates the\n // statement (closing `from '…'` / side-effect `'…'`).\n if (inImportBlock) {\n topLevelLines.push(line);\n if (endsStatement(trimmed)) {\n inImportBlock = false;\n }\n continue;\n }\n\n // Static import declarations must stay at module top level\n // (the ESM spec forbids `import` inside a function body). If the\n // statement does not terminate on this line, open an import block so\n // the member lines and closing `} from '…'` line stay top-level too.\n if (isImportStart(trimmed)) {\n topLevelLines.push(line);\n if (!endsStatement(trimmed)) {\n inImportBlock = true;\n }\n } else if (trimmed.startsWith('export ')) {\n // Determine whether this is a re-export (stays top-level) or a value\n // declaration (goes into the factory, export keyword stripped).\n const m = trimmed.match(EXPORT_DECLARATION_RE);\n if (m) {\n // Value declaration — strip `export ` and move into factory body.\n // e.g. `export function hello()` → `function hello()`\n // `export const x = 1` → `const x = 1`\n bodyLines.push(indent + trimmed.slice('export '.length));\n } else {\n // Re-export or `export type { … }` — stays at top level. A\n // multi-line `export { … } from '…'` opens an import block too.\n topLevelLines.push(line);\n if (/\\bfrom\\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith('{')) {\n inImportBlock = true;\n }\n }\n } else {\n bodyLines.push(line);\n }\n }\n\n const factoryContent = [\n ...topLevelLines,\n '',\n '// biome-ignore lint: generated factory wrapper',\n 'export default async function __userFactory(): Promise<void> {',\n ...bodyLines.map((l) => ` ${l}`),\n '}',\n ].join('\\n');\n\n return {\n contents: factoryContent,\n loader: 'ts',\n resolveDir: path.dirname(absPath),\n };\n });\n },\n };\n}\n\n/**\n * Returns the absolute path to the test-runner runtime module.\n *\n * Searches candidates in priority order:\n * 1. Co-located `runtime.ts` / `runtime.js` — covers the source tree\n * (tsx / ts-node) and the `dist/test-runner/` entry.\n * 2. `../test-runner/runtime.js` — covers the `dist/mcp/cli.js` entry,\n * where `import.meta.url` resolves to `dist/mcp/` (a sibling directory\n * of `dist/test-runner/`). Without this second candidate the MCP entry\n * point would look for `dist/mcp/runtime.js`, which does not exist, and\n * every `run_tests` call would fail with an esbuild \"Could not resolve\"\n * error (#678).\n *\n * Returns the first candidate that exists on disk. Falls back to the\n * co-located `runtime.js` path so esbuild produces a clear \"file not found\"\n * error rather than a cryptic failure.\n */\nfunction getRuntimePath(): string {\n const dir = path.dirname(fileURLToPath(import.meta.url));\n const candidates = [\n path.join(dir, 'runtime.ts'),\n path.join(dir, 'runtime.js'),\n path.join(dir, '..', 'test-runner', 'runtime.js'),\n ];\n for (const candidate of candidates) {\n try {\n accessSync(candidate);\n return candidate;\n } catch {\n // try next candidate\n }\n }\n // Let esbuild produce a \"file not found\" error with a clear path.\n return path.join(dir, 'runtime.js');\n}\n\n/**\n * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.\n *\n * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:\n * - `runTestModule` — the runtime entry (from `runtime.ts`).\n * - `__userFactory` — an async function wrapping the user's test registration\n * code so it runs AFTER `runTestModule` installs the globals.\n *\n * Callers (rpc.ts) invoke:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * @param absPath - Absolute path to the user test file.\n * @param opts - Optional bundling overrides.\n */\nexport async function bundleTestFile(absPath: string, opts?: BundleOptions): Promise<BundleResult> {\n const globalName = opts?.globalName ?? '__testBundle';\n const extraExternals = opts?.extraExternals ?? [];\n\n // Lazy load esbuild at call time (see the module-scope import note).\n const esbuild = await import('esbuild');\n const runtimePath = getRuntimePath();\n\n // Stdin wrapper: import the runtime and the user factory, re-export both.\n // esbuild follows the static imports to include runtime.ts and the user file\n // (via the userFactoryPlugin) in the single IIFE output.\n const wrapperContent = [\n `import { runTestModule } from ${JSON.stringify(runtimePath)};`,\n `import __userFactory from \"user-test-factory\";`,\n `export { runTestModule, __userFactory };`,\n ].join('\\n');\n\n const result = await esbuild.build({\n stdin: {\n contents: wrapperContent,\n loader: 'ts',\n // resolveDir is used for relative imports from the wrapper. Since the\n // wrapper only imports absolute paths (runtimePath) and the virtual\n // \"user-test-factory\" specifier (resolved by plugin), the directory\n // doesn't matter — but we still provide a sensible default.\n resolveDir: path.dirname(absPath),\n },\n bundle: true,\n format: 'iife',\n globalName,\n platform: 'browser',\n target: 'es2022',\n write: false,\n plugins: [userFactoryPlugin(absPath), vitestRedirectPlugin(), sdkRedirectPlugin()],\n external: extraExternals,\n treeShaking: true,\n // Ensure the IIFE result is always reachable via globalThis regardless of\n // the evaluation context. esbuild's `globalName` emits:\n // var __testBundle = (() => { ... })();\n // When `Runtime.evaluate` runs this bundle code inside an outer wrapper\n // (rpc.ts's async IIFE), `var` creates a local variable — NOT a global\n // property — so `globalThis.__testBundle` stays `undefined`. The footer\n // explicitly assigns the local variable to `globalThis` to close that gap.\n footer: {\n js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};`,\n },\n });\n\n const warnings = result.warnings.map(\n (w) =>\n `${path.relative(process.cwd(), w.location?.file ?? '')}:${w.location?.line ?? '?'}: ${w.text}`,\n );\n\n const outputFile = result.outputFiles?.[0];\n if (!outputFile) {\n throw new Error('bundleTestFile: esbuild produced no output — check entryPoints');\n }\n\n return { code: outputFile.text, warnings };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FA,MAAM,cAAc;;;;;;;AAQpB,MAAM,sBAAsB;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;AAOD,MAAM,oBAAoB,IAAI,OAAO,IAAI,YAAY,QAAQ,uBAAuB,OAAO,GAAG;;;;;;;;;;;;;AAc9F,SAAS,oBAAoC;AAC3C,QAAO;EACL,MAAM;EACN,MAAM,OAAO;AAEX,SAAM,UAAU,EAAE,QAAQ,mBAAmB,GAAG,UAAU;IACxD,MAAM,KAAK;IACX,WAAW;IACZ,EAAE;AAEH,SAAM,OAAO;IAAE,QAAQ;IAAM,WAAW;IAAgB,SAAS;IAO/D,UAAU;;;;;;;;;;IAUV,QAAQ;IACT,EAAE;;EAEN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BH,SAAS,uBAAuC;AAC9C,QAAO;EACL,MAAM;EACN,MAAM,OAAO;AACX,SAAM,UAAU,EAAE,QAAQ,YAAY,SAAS;IAC7C,MAAM;IACN,WAAW;IACZ,EAAE;AAEH,SAAM,OAAO;IAAE,QAAQ;IAAY,WAAW;IAAmB,QAAQ;AAQvE,WAAO;KACL,UAAU,mEARI,oBAAoB,KACjC,SACC,kCAAkC,KAAK,UAAU,KAAK,CAAC,4DAA4D,KAAK,UAAU,KAAK,CAAC,UAC3I,CAAC,KAAK,KAAK,CAK2E;KACrF,QAAQ;KACT;KACD;;EAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BH,SAAS,kBAAkB,SAAiC;CAC1D,MAAM,YAAY;AAClB,QAAO;EACL,MAAM;EACN,MAAM,OAAO;AAEX,SAAM,UAAU,EAAE,QAAQ,uBAAuB,SAAS;IACxD,MAAM;IACN,WAAW;IACZ,EAAE;AAGH,SAAM,OAAO;IAAE,QAAQ;IAAM,WAAW;IAAW,EAAE,OAAO,SAAS;IAEnE,MAAM,SADS,MAAM,GAAG,SAAS,KAAK,MAAM,OAAO,EAC9B,MAAM,KAAK;IAEhC,MAAM,gBAA0B,EAAE;IAClC,MAAM,YAAsB,EAAE;IAK9B,MAAM,wBACJ;IAGF,MAAM,iBAAiB,YACrB,QAAQ,WAAW,UAAU,IAC7B,QAAQ,WAAW,UAAU,IAC7B,QAAQ,WAAW,UAAU,IAC7B,QAAQ,WAAW,WAAU;IAQ/B,MAAM,iBAAiB,YACrB,gBAAgB,KAAK,QAAQ,QAAQ,WAAW,GAAG,CAAC,SAAS,CAAC;IAIhE,IAAI,gBAAgB;AAEpB,SAAK,MAAM,QAAQ,OAAO;KACxB,MAAM,UAAU,KAAK,WAAW;KAChC,MAAM,SAAS,KAAK,MAAM,GAAG,KAAK,SAAS,QAAQ,OAAO;AAK1D,SAAI,eAAe;AACjB,oBAAc,KAAK,KAAK;AACxB,UAAI,cAAc,QAAQ,CACxB,iBAAgB;AAElB;;AAOF,SAAI,cAAc,QAAQ,EAAE;AAC1B,oBAAc,KAAK,KAAK;AACxB,UAAI,CAAC,cAAc,QAAQ,CACzB,iBAAgB;gBAET,QAAQ,WAAW,UAAU,CAItC,KADU,QAAQ,MAAM,sBAAsB,CAK5C,WAAU,KAAK,SAAS,QAAQ,MAAM,EAAiB,CAAC;UACnD;AAGL,oBAAc,KAAK,KAAK;AACxB,UAAI,WAAW,KAAK,QAAQ,GAAG,CAAC,cAAc,QAAQ,GAAG,QAAQ,SAAS,IAAI,CAC5E,iBAAgB;;SAIpB,WAAU,KAAK,KAAK;;AAaxB,WAAO;KACL,UAVqB;MACrB,GAAG;MACH;MACA;MACA;MACA,GAAG,UAAU,KAAK,MAAM,KAAK,IAAI;MACjC;MACD,CAAC,KAAK,KAAK;KAIV,QAAQ;KACR,YAAYA,OAAK,QAAQ,QAAQ;KAClC;KACD;;EAEL;;;;;;;;;;;;;;;;;;;AAoBH,SAAS,iBAAyB;CAChC,MAAM,MAAMA,OAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CACxD,MAAM,aAAa;EACjBA,OAAK,KAAK,KAAK,aAAa;EAC5BA,OAAK,KAAK,KAAK,aAAa;EAC5BA,OAAK,KAAK,KAAK,MAAM,eAAe,aAAa;EAClD;AACD,MAAK,MAAM,aAAa,WACtB,KAAI;AACF,aAAW,UAAU;AACrB,SAAO;SACD;AAKV,QAAOA,OAAK,KAAK,KAAK,aAAa;;;;;;;;;;;;;;;;AAiBrC,eAAsB,eAAe,SAAiB,MAA6C;CACjG,MAAM,aAAa,MAAM,cAAc;CACvC,MAAM,iBAAiB,MAAM,kBAAkB,EAAE;CAGjD,MAAM,UAAU,MAAM,OAAO;CAC7B,MAAM,cAAc,gBAAgB;CAKpC,MAAM,iBAAiB;EACrB,iCAAiC,KAAK,UAAU,YAAY,CAAC;EAC7D;EACA;EACD,CAAC,KAAK,KAAK;CAEZ,MAAM,SAAS,MAAM,QAAQ,MAAM;EACjC,OAAO;GACL,UAAU;GACV,QAAQ;GAKR,YAAYA,OAAK,QAAQ,QAAQ;GAClC;EACD,QAAQ;EACR,QAAQ;EACR;EACA,UAAU;EACV,QAAQ;EACR,OAAO;EACP,SAAS;GAAC,kBAAkB,QAAQ;GAAE,sBAAsB;GAAE,mBAAmB;GAAC;EAClF,UAAU;EACV,aAAa;EAQb,QAAQ,EACN,IAAI,cAAc,KAAK,UAAU,WAAW,CAAC,MAAM,WAAW,IAC/D;EACF,CAAC;CAEF,MAAM,WAAW,OAAO,SAAS,KAC9B,MACC,GAAGA,OAAK,SAAS,QAAQ,KAAK,EAAE,EAAE,UAAU,QAAQ,GAAG,CAAC,GAAG,EAAE,UAAU,QAAQ,IAAI,IAAI,EAAE,OAC5F;CAED,MAAM,aAAa,OAAO,cAAc;AACxC,KAAI,CAAC,WACH,OAAM,IAAI,MAAM,iEAAiE;AAGnF,QAAO;EAAE,MAAM,WAAW;EAAM;EAAU"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/test-runner/capture.ts
|
|
2
|
+
/** The exact console-line prefix sdk-example's `flushCapture` emits. */
|
|
3
|
+
const CAPTURE_PREFIX = "__AIT_CAPTURE__ ";
|
|
4
|
+
/**
|
|
5
|
+
* Parses raw console line texts into {@link AitCaptureLine}s.
|
|
6
|
+
*
|
|
7
|
+
* Filtering rules (each independently drops a line — never throws):
|
|
8
|
+
* - the line text must `startsWith(CAPTURE_PREFIX)` exactly (allowlist);
|
|
9
|
+
* - there must be a non-empty category token (up to the next space);
|
|
10
|
+
* - the remaining payload must be valid JSON (`JSON.parse` succeeds).
|
|
11
|
+
*
|
|
12
|
+
* Lines that fail any rule (wss/scheme noise, truncated, broken JSON) are
|
|
13
|
+
* silently discarded — capture harvesting is best-effort and must never fail a
|
|
14
|
+
* run or leak a malformed/secret-bearing line.
|
|
15
|
+
*
|
|
16
|
+
* @param raw - Console line objects (only `.text` is read).
|
|
17
|
+
* @returns The captured lines, in input order.
|
|
18
|
+
*/
|
|
19
|
+
function parseCaptureLines(raw) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const { text } of raw) {
|
|
22
|
+
if (!text.startsWith(CAPTURE_PREFIX)) continue;
|
|
23
|
+
const body = text.slice(16);
|
|
24
|
+
const spaceIdx = body.indexOf(" ");
|
|
25
|
+
if (spaceIdx === -1) continue;
|
|
26
|
+
const category = body.slice(0, spaceIdx);
|
|
27
|
+
const json = body.slice(spaceIdx + 1);
|
|
28
|
+
if (category === "" || json === "") continue;
|
|
29
|
+
try {
|
|
30
|
+
JSON.parse(json);
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
out.push({
|
|
35
|
+
category,
|
|
36
|
+
json
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { parseCaptureLines };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","names":[],"sources":["../../src/test-runner/capture.ts"],"sourcesContent":["/**\n * `__AIT_CAPTURE__` console-line parser (devtools#696).\n *\n * env3 test runs inject sdk-example's `aitCapture.ts`, which — when running in a\n * WebView (no filesystem) — emits one `console.log` line per category in the\n * stable form:\n *\n * __AIT_CAPTURE__ <category> <json>\n *\n * The relay-worker harvests `Runtime.consoleAPICalled` events as plain text\n * lines (see `relay-worker.ts`) and hands them here. This module's only job is\n * to recognise the allowlisted prefix, split off the category, and keep the\n * remaining JSON string OPAQUE — devtools does NOT own or interpret the\n * `__AIT_CAPTURE__` record shape (that lives in sdk-example). We carry\n * `{ category, json }` through untouched so the 2.x↔3.0 line comparison stays a\n * downstream concern.\n *\n * Why a strict allowlist prefix (SECRET-HANDLING): the raw console stream may\n * also contain wss/scheme/relay noise lines. Only lines that EXACTLY start with\n * `'__AIT_CAPTURE__ '` pass; everything else is discarded, so a stray relay URL\n * can never be serialised into a capture artifact.\n *\n * react-free, dependency-free — safe to import from any test-runner entry\n * without dragging in the chii/cloudflared Node graph.\n */\n\n/**\n * One parsed capture line. `json` is the raw JSON string exactly as the page\n * emitted it (an array of sdk-example capture records). devtools treats it as\n * opaque — it is validated as parseable JSON but never inspected.\n */\nexport interface AitCaptureLine {\n /** The capture category (first whitespace-delimited token after the prefix). */\n category: string;\n /** The remaining JSON payload, verbatim. Opaque to devtools. */\n json: string;\n}\n\n/** The exact console-line prefix sdk-example's `flushCapture` emits. */\nconst CAPTURE_PREFIX = '__AIT_CAPTURE__ ';\n\n/**\n * Parses raw console line texts into {@link AitCaptureLine}s.\n *\n * Filtering rules (each independently drops a line — never throws):\n * - the line text must `startsWith(CAPTURE_PREFIX)` exactly (allowlist);\n * - there must be a non-empty category token (up to the next space);\n * - the remaining payload must be valid JSON (`JSON.parse` succeeds).\n *\n * Lines that fail any rule (wss/scheme noise, truncated, broken JSON) are\n * silently discarded — capture harvesting is best-effort and must never fail a\n * run or leak a malformed/secret-bearing line.\n *\n * @param raw - Console line objects (only `.text` is read).\n * @returns The captured lines, in input order.\n */\nexport function parseCaptureLines(raw: ReadonlyArray<{ text: string }>): AitCaptureLine[] {\n const out: AitCaptureLine[] = [];\n for (const { text } of raw) {\n // Allowlist: only the exact `__AIT_CAPTURE__ ` prefix passes. wss/scheme\n // noise lines are dropped here (SECRET-HANDLING).\n if (!text.startsWith(CAPTURE_PREFIX)) continue;\n\n const body = text.slice(CAPTURE_PREFIX.length);\n const spaceIdx = body.indexOf(' ');\n // No payload separator → malformed; drop.\n if (spaceIdx === -1) continue;\n\n const category = body.slice(0, spaceIdx);\n const json = body.slice(spaceIdx + 1);\n if (category === '' || json === '') continue;\n\n // Validate the payload is parseable JSON, but keep it as an OPAQUE string —\n // devtools does not own the record shape. Broken JSON is discarded (no throw).\n try {\n JSON.parse(json);\n } catch {\n continue;\n }\n\n out.push({ category, json });\n }\n return out;\n}\n"],"mappings":";;AAuCA,MAAM,iBAAiB;;;;;;;;;;;;;;;;AAiBvB,SAAgB,kBAAkB,KAAwD;CACxF,MAAM,MAAwB,EAAE;AAChC,MAAK,MAAM,EAAE,UAAU,KAAK;AAG1B,MAAI,CAAC,KAAK,WAAW,eAAe,CAAE;EAEtC,MAAM,OAAO,KAAK,MAAM,GAAsB;EAC9C,MAAM,WAAW,KAAK,QAAQ,IAAI;AAElC,MAAI,aAAa,GAAI;EAErB,MAAM,WAAW,KAAK,MAAM,GAAG,SAAS;EACxC,MAAM,OAAO,KAAK,MAAM,WAAW,EAAE;AACrC,MAAI,aAAa,MAAM,SAAS,GAAI;AAIpC,MAAI;AACF,QAAK,MAAM,KAAK;UACV;AACN;;AAGF,MAAI,KAAK;GAAE;GAAU;GAAM,CAAC;;AAE9B,QAAO"}
|
|
@@ -345,6 +345,44 @@ interface BundleOptions {
|
|
|
345
345
|
globalName?: string;
|
|
346
346
|
}
|
|
347
347
|
//#endregion
|
|
348
|
+
//#region src/test-runner/capture.d.ts
|
|
349
|
+
/**
|
|
350
|
+
* `__AIT_CAPTURE__` console-line parser (devtools#696).
|
|
351
|
+
*
|
|
352
|
+
* env3 test runs inject sdk-example's `aitCapture.ts`, which — when running in a
|
|
353
|
+
* WebView (no filesystem) — emits one `console.log` line per category in the
|
|
354
|
+
* stable form:
|
|
355
|
+
*
|
|
356
|
+
* __AIT_CAPTURE__ <category> <json>
|
|
357
|
+
*
|
|
358
|
+
* The relay-worker harvests `Runtime.consoleAPICalled` events as plain text
|
|
359
|
+
* lines (see `relay-worker.ts`) and hands them here. This module's only job is
|
|
360
|
+
* to recognise the allowlisted prefix, split off the category, and keep the
|
|
361
|
+
* remaining JSON string OPAQUE — devtools does NOT own or interpret the
|
|
362
|
+
* `__AIT_CAPTURE__` record shape (that lives in sdk-example). We carry
|
|
363
|
+
* `{ category, json }` through untouched so the 2.x↔3.0 line comparison stays a
|
|
364
|
+
* downstream concern.
|
|
365
|
+
*
|
|
366
|
+
* Why a strict allowlist prefix (SECRET-HANDLING): the raw console stream may
|
|
367
|
+
* also contain wss/scheme/relay noise lines. Only lines that EXACTLY start with
|
|
368
|
+
* `'__AIT_CAPTURE__ '` pass; everything else is discarded, so a stray relay URL
|
|
369
|
+
* can never be serialised into a capture artifact.
|
|
370
|
+
*
|
|
371
|
+
* react-free, dependency-free — safe to import from any test-runner entry
|
|
372
|
+
* without dragging in the chii/cloudflared Node graph.
|
|
373
|
+
*/
|
|
374
|
+
/**
|
|
375
|
+
* One parsed capture line. `json` is the raw JSON string exactly as the page
|
|
376
|
+
* emitted it (an array of sdk-example capture records). devtools treats it as
|
|
377
|
+
* opaque — it is validated as parseable JSON but never inspected.
|
|
378
|
+
*/
|
|
379
|
+
interface AitCaptureLine {
|
|
380
|
+
/** The capture category (first whitespace-delimited token after the prefix). */
|
|
381
|
+
category: string;
|
|
382
|
+
/** The remaining JSON payload, verbatim. Opaque to devtools. */
|
|
383
|
+
json: string;
|
|
384
|
+
}
|
|
385
|
+
//#endregion
|
|
348
386
|
//#region src/test-runner/runtime.d.ts
|
|
349
387
|
/**
|
|
350
388
|
* Thin test runtime for WebView execution.
|
|
@@ -418,6 +456,17 @@ interface RelayRunReport {
|
|
|
418
456
|
skipped: number;
|
|
419
457
|
total: number;
|
|
420
458
|
};
|
|
459
|
+
/**
|
|
460
|
+
* `__AIT_CAPTURE__` lines harvested from the page console during the run
|
|
461
|
+
* (additive field, devtools#696). Empty unless `collectCaptures` was set.
|
|
462
|
+
* Each entry is opaque (`{ category, json }`) — devtools does not interpret
|
|
463
|
+
* the record shape, only forwards it for downstream 2.x↔3.0 diffing.
|
|
464
|
+
*
|
|
465
|
+
* SECRET-HANDLING: only lines matching the `__AIT_CAPTURE__ ` allowlist
|
|
466
|
+
* prefix reach here; relay/wss/scheme noise lines are dropped in
|
|
467
|
+
* `parseCaptureLines`.
|
|
468
|
+
*/
|
|
469
|
+
captures: AitCaptureLine[];
|
|
421
470
|
}
|
|
422
471
|
/** Options for `runTestFilesOverRelay`. */
|
|
423
472
|
interface RelayRunOptions {
|
|
@@ -430,6 +479,18 @@ interface RelayRunOptions {
|
|
|
430
479
|
* Increase for long-running suites or split the file.
|
|
431
480
|
*/
|
|
432
481
|
timeoutMs?: number;
|
|
482
|
+
/**
|
|
483
|
+
* When `true`, registers a live `Runtime.consoleAPICalled` listener for the
|
|
484
|
+
* duration of the run and harvests `__AIT_CAPTURE__` lines into
|
|
485
|
+
* `RelayRunReport.captures`. Defaults to **false** so the build-only
|
|
486
|
+
* eval/e2e path (and the Vitest pool) pay no listener/state overhead —
|
|
487
|
+
* `captures` is then an empty array.
|
|
488
|
+
*
|
|
489
|
+
* The listener is registered just BEFORE the first file runs and removed in a
|
|
490
|
+
* `finally` so it never accumulates across runs (no ring-buffer drain — the
|
|
491
|
+
* default 500-entry buffer would silently drop lines via `shift`).
|
|
492
|
+
*/
|
|
493
|
+
collectCaptures?: boolean;
|
|
433
494
|
}
|
|
434
495
|
//#endregion
|
|
435
496
|
//#region src/test-runner/cli.d.ts
|
|
@@ -445,30 +506,42 @@ interface RunWithConnectionOptions extends RelayRunOptions {
|
|
|
445
506
|
* separate from `main()` so tests can call it without spawning a subprocess.
|
|
446
507
|
*/
|
|
447
508
|
declare function runWithConnection(connection: CdpConnection, files: string[], opts?: RunWithConnectionOptions): Promise<RelayRunReport>;
|
|
509
|
+
/**
|
|
510
|
+
* Decides whether to suppress the QR/attach block on stdout.
|
|
511
|
+
*
|
|
512
|
+
* Suppress when EITHER the user passed `--no-qr-stdout`, OR stdout is not a TTY
|
|
513
|
+
* / `CI` is set / `AIT_NO_QR_STDOUT` is set (non-interactive — a captured stdout
|
|
514
|
+
* must not leak the relay wss + TOTP `at=` code that the QR block encodes). The
|
|
515
|
+
* suppression is whole-chunk: `attachUrl` AND `relayUrl` ride in the same block.
|
|
516
|
+
*
|
|
517
|
+
* Exported for unit testing.
|
|
518
|
+
*/
|
|
519
|
+
declare function shouldSuppressQr(noQrFlag: boolean): boolean;
|
|
448
520
|
/**
|
|
449
521
|
* CLI entry point.
|
|
450
522
|
*
|
|
451
|
-
* Performs a standalone relay attach → run lifecycle
|
|
523
|
+
* Performs a standalone relay attach → run lifecycle, sharing the attach
|
|
524
|
+
* assembly with the Vitest pool via `createRelayConnectionFactory` (single
|
|
525
|
+
* source — no drift):
|
|
452
526
|
*
|
|
453
527
|
* 1. Parse args: globs, --timeout, --cell-sdk-line, --cell-platform,
|
|
454
|
-
* --scheme-url (required for env3), --
|
|
528
|
+
* --scheme-url (required for env3), --report-dir, --no-qr-stdout,
|
|
529
|
+
* --headless, --project-root.
|
|
455
530
|
* 2. Discover test files; exit 1 if none.
|
|
456
|
-
* 3.
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
* 8. runWithConnection(conn, files, { timeoutMs, printSummary: true }).
|
|
462
|
-
* 9. family.stop(); process.exitCode = failed > 0 ? 1 : 0.
|
|
531
|
+
* 3. factory.open() — boot relay → render QR (suppressed on non-interactive
|
|
532
|
+
* stdout) → wait for phone → inject cell → enableDomains. Returns the conn.
|
|
533
|
+
* 4. runWithConnection(conn, files, { timeoutMs, collectCaptures, printSummary }).
|
|
534
|
+
* 5. With --report-dir: write the runner-agnostic report + capture files.
|
|
535
|
+
* 6. factory.close(); process.exitCode = failed > 0 ? 1 : 0.
|
|
463
536
|
*
|
|
464
537
|
* The CLI is not a daemon — no lock, router, SSE, or tools_list is needed.
|
|
465
538
|
* Attach timeout exits with code 1; test failures exit with code 1.
|
|
466
539
|
*
|
|
467
540
|
* SECRET-HANDLING: scheme_url / relay wssUrl / TOTP codes are never written to
|
|
468
|
-
* stdout/stderr directly.
|
|
469
|
-
*
|
|
541
|
+
* stdout/stderr directly. The QR block (which encodes the TOTP `at=` code) is
|
|
542
|
+
* printed only when stdout is interactive AND not suppressed.
|
|
470
543
|
*/
|
|
471
544
|
declare function main(argv?: string[]): Promise<void>;
|
|
472
545
|
//#endregion
|
|
473
|
-
export { RunWithConnectionOptions, main, runWithConnection };
|
|
546
|
+
export { RunWithConnectionOptions, main, runWithConnection, shouldSuppressQr };
|
|
474
547
|
//# sourceMappingURL=cli.d.ts.map
|