@ait-co/devtools 0.1.121 → 0.1.123
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-DEJfLm7V.js → attach-orchestrator-CPMQWkGV.js} +3 -3
- package/dist/{attach-orchestrator-DEJfLm7V.js.map → attach-orchestrator-CPMQWkGV.js.map} +1 -1
- package/dist/{attach-orchestrator-qFEMBUwb.js → attach-orchestrator-CpE0dMew.js} +2 -2
- package/dist/{attach-orchestrator-qFEMBUwb.js.map → attach-orchestrator-CpE0dMew.js.map} +1 -1
- package/dist/{attach-orchestrator-CFotueL3.js → attach-orchestrator-RhOdO3DS.js} +2 -2
- package/dist/{attach-orchestrator-CFotueL3.js.map → attach-orchestrator-RhOdO3DS.js.map} +1 -1
- package/dist/{bundle-CA1TCXED.d.ts → bundle-Cb3GFuw6.d.ts} +1 -1
- package/dist/{bundle-CA1TCXED.d.ts.map → bundle-Cb3GFuw6.d.ts.map} +1 -1
- package/dist/{capture-BmK_cBqQ.d.ts → capture-B1zfGyTo.d.ts} +1 -1
- package/dist/{capture-BmK_cBqQ.d.ts.map → capture-B1zfGyTo.d.ts.map} +1 -1
- package/dist/{cdp-connection-DB2zgthr.d.ts → cdp-connection-Dxw_EY8s.d.ts} +1 -1
- package/dist/{cdp-connection-DB2zgthr.d.ts.map → cdp-connection-Dxw_EY8s.d.ts.map} +1 -1
- package/dist/{cell-Cv3e_qwn.js → cell-Ba9lCI3B.js} +3 -3
- package/dist/{cell-BPWZUm_X.js.map → cell-Ba9lCI3B.js.map} +1 -1
- package/dist/{cell-BPWZUm_X.js → cell-CX92lDI0.js} +3 -3
- package/dist/{cell-Cv3e_qwn.js.map → cell-CX92lDI0.js.map} +1 -1
- package/dist/{cell-nrWFeqvG.js → cell-D0dJSGHi.js} +3 -3
- package/dist/{cell-Cj-u6JTG.js.map → cell-D0dJSGHi.js.map} +1 -1
- package/dist/{debug-server-BhHVy3XT.js → debug-server-Bup2AJ5H.js} +7 -226
- package/dist/debug-server-Bup2AJ5H.js.map +1 -0
- package/dist/{debug-server-Buc8yoa7.js → debug-server-C0A_Kujq.js} +8 -945
- package/dist/debug-server-C0A_Kujq.js.map +1 -0
- package/dist/{debug-server-C_5ag9jj.js → debug-server-DywH5GU-.js} +5 -4
- package/dist/{debug-server-C_5ag9jj.js.map → debug-server-DywH5GU-.js.map} +1 -1
- package/dist/mcp/cli.js +8204 -4
- 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-D5QH6Ydv.d.ts → pool-B9wSRPOU.d.ts} +3 -3
- package/dist/{pool-D5QH6Ydv.d.ts.map → pool-B9wSRPOU.d.ts.map} +1 -1
- package/dist/qr-http-server-CKbtqvKy.js +1505 -0
- package/dist/qr-http-server-CKbtqvKy.js.map +1 -0
- package/dist/qr-http-server-DDmFS7fX.js +1506 -0
- package/dist/qr-http-server-DDmFS7fX.js.map +1 -0
- package/dist/{relay-factory-DVI7TrX4.js → relay-factory-DnHMDxMA.js} +25 -5
- package/dist/relay-factory-DnHMDxMA.js.map +1 -0
- package/dist/{relay-secret-store-DNPJKHNs.js → relay-secret-store-DKxs7zwq.js} +1 -1
- package/dist/{relay-secret-store-DNPJKHNs.js.map → relay-secret-store-DKxs7zwq.js.map} +1 -1
- package/dist/{relay-secret-store-CmDDfZMh.js → relay-secret-store-DhzAnnj-.js} +3 -3
- package/dist/{relay-secret-store-CmDDfZMh.js.map → relay-secret-store-DhzAnnj-.js.map} +1 -1
- package/dist/{relay-url-store-CnL2zwbH.js → relay-url-store-CwKT7i04.js} +2 -2
- package/dist/{relay-url-store-CnL2zwbH.js.map → relay-url-store-CwKT7i04.js.map} +1 -1
- package/dist/{relay-url-store-Dv5HSzwb.js → relay-url-store-xmUuTjXA.js} +2 -2
- package/dist/{relay-url-store-Dv5HSzwb.js.map → relay-url-store-xmUuTjXA.js.map} +1 -1
- package/dist/{relay-worker-BBu1oEqC.d.ts → relay-worker-7biDlEjo.d.ts} +5 -5
- package/dist/{relay-worker-BBu1oEqC.d.ts.map → relay-worker-7biDlEjo.d.ts.map} +1 -1
- package/dist/{runtime-Bm_sT1C9.d.ts → runtime-ozyPnRbU.d.ts} +1 -1
- package/dist/{runtime-Bm_sT1C9.d.ts.map → runtime-ozyPnRbU.d.ts.map} +1 -1
- package/dist/test-runner/bin.d.ts +2 -0
- package/dist/{cli-RhzASQef.js → test-runner/bin.js} +50 -7
- package/dist/test-runner/bin.js.map +1 -0
- package/dist/test-runner/bundle.d.ts +1 -1
- package/dist/test-runner/capture.d.ts +1 -1
- package/dist/test-runner/config.d.ts +1 -1
- package/dist/test-runner/config.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/relay-factory.d.ts.map +1 -1
- package/dist/test-runner/relay-factory.js +24 -4
- package/dist/test-runner/relay-factory.js.map +1 -1
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/report.d.ts +3 -3
- 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-D1pulXLa.js +3 -0
- package/dist/{totp-0bvlo9Yb.js → totp-WY6l0ysP.js} +1 -1
- package/dist/{totp-0bvlo9Yb.js.map → totp-WY6l0ysP.js.map} +1 -1
- package/package.json +2 -2
- package/dist/attach-orchestrator-BTsEHXfq.js +0 -3
- package/dist/attach-orchestrator-DPMBN06p.js +0 -2797
- package/dist/attach-orchestrator-DPMBN06p.js.map +0 -1
- package/dist/cell-Bca_k5ne.js +0 -3
- package/dist/cell-Cj-u6JTG.js +0 -85
- package/dist/cell-nrWFeqvG.js.map +0 -1
- package/dist/cli-RhzASQef.js.map +0 -1
- package/dist/debug-server-BhHVy3XT.js.map +0 -1
- package/dist/debug-server-Buc8yoa7.js.map +0 -1
- package/dist/debug-server-m9lIC9YW.js +0 -5758
- package/dist/debug-server-m9lIC9YW.js.map +0 -1
- package/dist/relay-factory-DVI7TrX4.js.map +0 -1
- package/dist/relay-secret-store-BlFEhqwb.js +0 -3
- package/dist/test-runner/cli.d.ts +0 -547
- package/dist/test-runner/cli.d.ts.map +0 -1
- package/dist/test-runner/cli.js +0 -3
- package/dist/totp-zGIlDsZS.js +0 -3
|
@@ -1,783 +1,18 @@
|
|
|
1
1
|
import { n as buildRelayVerifyAuth, r as generateTotp, t as assertRelayAuthConfigured } from "./totp-DfekTBk3.js";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import "./
|
|
5
|
-
import "./
|
|
2
|
+
import "./test-runner/relay-worker.js";
|
|
3
|
+
import "./relay-factory-DnHMDxMA.js";
|
|
4
|
+
import { a as startTunnelHealthProbe, i as startQuickTunnel, n as makeTunnelStatus, o as logError, r as printAttachBanner, s as logInfo, t as generateAttachToken } from "./attach-orchestrator-CPMQWkGV.js";
|
|
5
|
+
import "./cell-D0dJSGHi.js";
|
|
6
|
+
import "./qr-http-server-CKbtqvKy.js";
|
|
7
|
+
import "./relay-secret-store-DKxs7zwq.js";
|
|
6
8
|
import { createRequire } from "node:module";
|
|
7
|
-
import { accessSync } from "node:fs";
|
|
8
|
-
import * as path$1 from "node:path";
|
|
9
|
-
import path, { isAbsolute, resolve } from "node:path";
|
|
10
9
|
import "@modelcontextprotocol/sdk/server/index.js";
|
|
11
10
|
import "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
11
|
import "@modelcontextprotocol/sdk/types.js";
|
|
13
|
-
import { parseArgs } from "node:util";
|
|
14
|
-
import * as fs from "node:fs/promises";
|
|
15
|
-
import { glob, mkdir, writeFile } from "node:fs/promises";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
12
|
import { EventEmitter } from "node:events";
|
|
18
13
|
import { WebSocket, WebSocketServer } from "ws";
|
|
19
14
|
import { createServer } from "node:http";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Test-file discovery shared by the `devtools-test` CLI and the `run_tests`
|
|
23
|
-
* MCP tool, so both expand glob patterns with identical semantics.
|
|
24
|
-
*
|
|
25
|
-
* Uses Node's built-in `fs/promises` `glob` (Node 22+) — no extra dependency,
|
|
26
|
-
* which keeps the MCP daemon install graph lean (a plain glob lib would land in
|
|
27
|
-
* the `npx … devtools-mcp` path for no benefit).
|
|
28
|
-
*
|
|
29
|
-
* Pure Node IO only (`node:fs/promises` + `node:path`) — react-free, so it is
|
|
30
|
-
* safe to import from the MCP daemon graph.
|
|
31
|
-
*/
|
|
32
|
-
/**
|
|
33
|
-
* Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of
|
|
34
|
-
* ABSOLUTE test file paths, resolved relative to `cwd`.
|
|
35
|
-
*
|
|
36
|
-
* A plain (non-glob) path passes through when it matches a real file; a glob
|
|
37
|
-
* expands against `cwd`. Absolute matches are kept as-is; relative matches are
|
|
38
|
-
* resolved against `cwd`. `bundleTestFile` requires an absolute path, so the
|
|
39
|
-
* absolute output feeds it directly.
|
|
40
|
-
*
|
|
41
|
-
* @param patterns Glob patterns or file paths (e.g. `['src/**\/*.ait.test.ts']`).
|
|
42
|
-
* @param cwd Base directory for relative patterns/results.
|
|
43
|
-
* @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.
|
|
44
|
-
*/
|
|
45
|
-
async function discoverTestFiles(patterns, cwd) {
|
|
46
|
-
const out = /* @__PURE__ */ new Set();
|
|
47
|
-
for await (const match of glob(patterns, { cwd })) out.add(isAbsolute(match) ? match : resolve(cwd, match));
|
|
48
|
-
return [...out].sort();
|
|
49
|
-
}
|
|
50
|
-
//#endregion
|
|
51
|
-
//#region src/test-runner/bundle.ts
|
|
52
|
-
/**
|
|
53
|
-
* esbuild-based bundler for user test files.
|
|
54
|
-
*
|
|
55
|
-
* Bundles a single test file into a self-contained IIFE string that can be
|
|
56
|
-
* injected into a WebView via `Runtime.evaluate`. The bundle includes the
|
|
57
|
-
* test runtime (`runtime.ts`), which provides `describe/it/test/expect` and
|
|
58
|
-
* the `runTestModule(factory)` entry point.
|
|
59
|
-
*
|
|
60
|
-
* ## How the wiring works
|
|
61
|
-
*
|
|
62
|
-
* The bundle exposes two exports on `globalThis.__testBundle`:
|
|
63
|
-
* - `runTestModule` — the runtime's entry function.
|
|
64
|
-
* - `__userFactory` — an async function whose body is the user's top-level
|
|
65
|
-
* test registration code (describe/it/test calls).
|
|
66
|
-
*
|
|
67
|
-
* The Node-side RPC (`rpc.ts`) calls:
|
|
68
|
-
* `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
|
|
69
|
-
*
|
|
70
|
-
* `runTestModule` then installs `describe/it/test/expect` as globals, invokes
|
|
71
|
-
* the factory (which registers all tests), runs them, and returns a `RunReport`.
|
|
72
|
-
*
|
|
73
|
-
* ## Why a factory wrapper is needed
|
|
74
|
-
*
|
|
75
|
-
* Naively adding the runtime to `entryPoints` and bundling the user file would
|
|
76
|
-
* fail for two reasons:
|
|
77
|
-
* 1. `describe/it/test/expect` from the runtime are module-local in the IIFE
|
|
78
|
-
* scope. The user's top-level `describe(...)` calls expect them as globals —
|
|
79
|
-
* they are not globals until `runTestModule` installs them.
|
|
80
|
-
* 2. Even with globals pre-installed, the user file runs at IIFE-evaluation
|
|
81
|
-
* time, before the RPC layer calls `runTestModule` to reset state and start
|
|
82
|
-
* the test clock.
|
|
83
|
-
*
|
|
84
|
-
* The factory approach solves both: the user's registration code is deferred
|
|
85
|
-
* into a function that `runTestModule` calls AFTER installing the globals.
|
|
86
|
-
*
|
|
87
|
-
* ## Factory extraction algorithm
|
|
88
|
-
*
|
|
89
|
-
* The `userFactoryPlugin` reads the user file and splits lines into:
|
|
90
|
-
* - **top-level**: `import …` and re-export lines — kept at module scope
|
|
91
|
-
* (the only valid position for static `import` in ESM).
|
|
92
|
-
* - **body**: all other statements — moved into the body of the exported
|
|
93
|
-
* `__userFactory` async function.
|
|
94
|
-
*
|
|
95
|
-
* esbuild processes the re-generated module, following each static import
|
|
96
|
-
* through the normal dependency graph (including the SDK-redirect plugin).
|
|
97
|
-
*
|
|
98
|
-
* ## SDK redirect
|
|
99
|
-
*
|
|
100
|
-
* Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via
|
|
101
|
-
* the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that
|
|
102
|
-
* `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.
|
|
103
|
-
*
|
|
104
|
-
* SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
|
|
105
|
-
*/
|
|
106
|
-
/** The SDK package name that mini-app test code imports from. */
|
|
107
|
-
const SDK_PACKAGE = "@apps-in-toss/web-framework";
|
|
108
|
-
/**
|
|
109
|
-
* Names the runtime installs as globals before invoking the user factory.
|
|
110
|
-
* The `vitest` virtual module re-exports each as a lazy getter that reads from
|
|
111
|
-
* `globalThis` at access time. Keep in sync with the globals installed in
|
|
112
|
-
* `runtime.ts#runTestModule`.
|
|
113
|
-
*/
|
|
114
|
-
const VITEST_GLOBAL_NAMES = [
|
|
115
|
-
"describe",
|
|
116
|
-
"it",
|
|
117
|
-
"test",
|
|
118
|
-
"expect",
|
|
119
|
-
"beforeAll",
|
|
120
|
-
"afterAll",
|
|
121
|
-
"beforeEach",
|
|
122
|
-
"afterEach",
|
|
123
|
-
"vi"
|
|
124
|
-
];
|
|
125
|
-
/**
|
|
126
|
-
* Matches the bare SDK package and any sub-path import
|
|
127
|
-
* (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).
|
|
128
|
-
* Built from {@link SDK_PACKAGE} so the package name has a single source.
|
|
129
|
-
*/
|
|
130
|
-
const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
131
|
-
/**
|
|
132
|
-
* esbuild plugin that intercepts SDK imports and redirects them to the
|
|
133
|
-
* `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.
|
|
134
|
-
*
|
|
135
|
-
* Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),
|
|
136
|
-
* esbuild resolves it to a virtual module that re-exports all named exports
|
|
137
|
-
* via `window.__sdk[name]`. This avoids bundling the real SDK (which may not
|
|
138
|
-
* be available in the test environment) while still making named imports work.
|
|
139
|
-
*
|
|
140
|
-
* If `window.__sdk` is absent (non-dog-food build), every access throws a
|
|
141
|
-
* descriptive error rather than returning `undefined` silently.
|
|
142
|
-
*/
|
|
143
|
-
function sdkRedirectPlugin() {
|
|
144
|
-
return {
|
|
145
|
-
name: "sdk-redirect",
|
|
146
|
-
setup(build) {
|
|
147
|
-
build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({
|
|
148
|
-
path: args.path,
|
|
149
|
-
namespace: "sdk-redirect"
|
|
150
|
-
}));
|
|
151
|
-
build.onLoad({
|
|
152
|
-
filter: /.*/,
|
|
153
|
-
namespace: "sdk-redirect"
|
|
154
|
-
}, () => ({
|
|
155
|
-
contents: `
|
|
156
|
-
var __proxy = (typeof window !== 'undefined' && window.__sdk)
|
|
157
|
-
? window.__sdk
|
|
158
|
-
: new Proxy({}, {
|
|
159
|
-
get: function(_t, p) {
|
|
160
|
-
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
module.exports = __proxy;
|
|
164
|
-
`,
|
|
165
|
-
loader: "js"
|
|
166
|
-
}));
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* esbuild plugin that intercepts `import … from 'vitest'` and replaces it with
|
|
172
|
-
* a virtual module that delegates every named import to `globalThis` at ACCESS
|
|
173
|
-
* time (not at bundle-evaluation time).
|
|
174
|
-
*
|
|
175
|
-
* The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/
|
|
176
|
-
* afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle
|
|
177
|
-
* IIFE is evaluated. A value-copy redirect (`export var describe =
|
|
178
|
-
* globalThis.describe`) would therefore capture `undefined` at evaluation time
|
|
179
|
-
* and the user's `describe(...)` calls would be no-ops — registering zero tests.
|
|
180
|
-
*
|
|
181
|
-
* The fix defers the lookup to call time using per-name **getter** exports.
|
|
182
|
-
* We emit a CommonJS module that:
|
|
183
|
-
* 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named
|
|
184
|
-
* import directly to a property access on the module (NOT wrapped under a
|
|
185
|
-
* `default` shim — which is what happens for a bare Proxy whose own-keys
|
|
186
|
-
* are empty, leaving every named import `undefined`);
|
|
187
|
-
* 2. defines each global name as a getter that reads `globalThis[name]` on
|
|
188
|
-
* every access. So `import { describe } from 'vitest'` compiles to
|
|
189
|
-
* `import_vitest.describe`, whose getter returns the real `describe` only
|
|
190
|
-
* when the factory calls it — after `runTestModule` installs the globals.
|
|
191
|
-
*
|
|
192
|
-
* A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes
|
|
193
|
-
* the virtual module through `__toESM`, which enumerates own-keys (none on an
|
|
194
|
-
* empty Proxy target) and therefore exposes zero named exports. Explicit getter
|
|
195
|
-
* properties give `__toESM` real keys to map while keeping access lazy.
|
|
196
|
-
*/
|
|
197
|
-
function vitestRedirectPlugin() {
|
|
198
|
-
return {
|
|
199
|
-
name: "vitest-redirect",
|
|
200
|
-
setup(build) {
|
|
201
|
-
build.onResolve({ filter: /^vitest$/ }, () => ({
|
|
202
|
-
path: "vitest",
|
|
203
|
-
namespace: "vitest-redirect"
|
|
204
|
-
}));
|
|
205
|
-
build.onLoad({
|
|
206
|
-
filter: /^vitest$/,
|
|
207
|
-
namespace: "vitest-redirect"
|
|
208
|
-
}, () => {
|
|
209
|
-
return {
|
|
210
|
-
contents: `Object.defineProperty(exports, '__esModule', { value: true });\n${VITEST_GLOBAL_NAMES.map((name) => `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`).join("\n")}\n`,
|
|
211
|
-
loader: "js"
|
|
212
|
-
};
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* esbuild plugin that transforms the user test file into a module that exports
|
|
219
|
-
* an async `__userFactory` function. The factory defers the user's top-level
|
|
220
|
-
* test registration code (describe/it/test calls) so it only runs when
|
|
221
|
-
* `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has
|
|
222
|
-
* installed describe/it/test/expect as globals.
|
|
223
|
-
*
|
|
224
|
-
* Algorithm:
|
|
225
|
-
* - Import declarations and re-export statements are kept at module top-level
|
|
226
|
-
* (the only valid ESM position for static `import`). A statement that spans
|
|
227
|
-
* multiple lines — e.g. a named import with one member per line:
|
|
228
|
-
* import {
|
|
229
|
-
* appLogin,
|
|
230
|
-
* getAnonymousKey,
|
|
231
|
-
* } from '@apps-in-toss/web-framework';
|
|
232
|
-
* is tracked as a single block: every line from the opening `import {` /
|
|
233
|
-
* `export {` through the closing `from '…'` (or side-effect `'…'`) line is
|
|
234
|
-
* kept together at top-level. This prevents the member lines and the
|
|
235
|
-
* closing `} from '…'` line from leaking into the factory body, which would
|
|
236
|
-
* leave an unterminated `import {` at module scope (the #678 env3 failure:
|
|
237
|
-
* esbuild threw `Expected "as" but found "{"` on multi-line SDK imports).
|
|
238
|
-
* - All other lines (describe/it/test calls, local declarations, etc.) are
|
|
239
|
-
* moved into the body of the exported async factory function.
|
|
240
|
-
*
|
|
241
|
-
* This preserves SDK import resolution (the sdk-redirect plugin processes
|
|
242
|
-
* top-level imports normally) while deferring test registration to the factory.
|
|
243
|
-
*/
|
|
244
|
-
function userFactoryPlugin(absPath) {
|
|
245
|
-
const NAMESPACE = "user-test-factory";
|
|
246
|
-
return {
|
|
247
|
-
name: "user-test-factory",
|
|
248
|
-
setup(build) {
|
|
249
|
-
build.onResolve({ filter: /^user-test-factory$/ }, () => ({
|
|
250
|
-
path: absPath,
|
|
251
|
-
namespace: NAMESPACE
|
|
252
|
-
}));
|
|
253
|
-
build.onLoad({
|
|
254
|
-
filter: /.*/,
|
|
255
|
-
namespace: NAMESPACE
|
|
256
|
-
}, async (args) => {
|
|
257
|
-
const lines = (await fs.readFile(args.path, "utf8")).split("\n");
|
|
258
|
-
const topLevelLines = [];
|
|
259
|
-
const bodyLines = [];
|
|
260
|
-
const EXPORT_DECLARATION_RE = /^(export\s+)(default\s+|async\s+function\s+|function\s+|class\s+|const\s+|let\s+|var\s+)/;
|
|
261
|
-
const isImportStart = (trimmed) => trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import'") || trimmed.startsWith("import\"");
|
|
262
|
-
const endsStatement = (trimmed) => /['"]\s*;?\s*$/.test(trimmed.replace(/\/\/.*$/, "").trimEnd());
|
|
263
|
-
let inImportBlock = false;
|
|
264
|
-
for (const line of lines) {
|
|
265
|
-
const trimmed = line.trimStart();
|
|
266
|
-
const indent = line.slice(0, line.length - trimmed.length);
|
|
267
|
-
if (inImportBlock) {
|
|
268
|
-
topLevelLines.push(line);
|
|
269
|
-
if (endsStatement(trimmed)) inImportBlock = false;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (isImportStart(trimmed)) {
|
|
273
|
-
topLevelLines.push(line);
|
|
274
|
-
if (!endsStatement(trimmed)) inImportBlock = true;
|
|
275
|
-
} else if (trimmed.startsWith("export ")) if (trimmed.match(EXPORT_DECLARATION_RE)) bodyLines.push(indent + trimmed.slice(7));
|
|
276
|
-
else {
|
|
277
|
-
topLevelLines.push(line);
|
|
278
|
-
if (/\bfrom\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith("{")) inImportBlock = true;
|
|
279
|
-
}
|
|
280
|
-
else bodyLines.push(line);
|
|
281
|
-
}
|
|
282
|
-
return {
|
|
283
|
-
contents: [
|
|
284
|
-
...topLevelLines,
|
|
285
|
-
"",
|
|
286
|
-
"// biome-ignore lint: generated factory wrapper",
|
|
287
|
-
"export default async function __userFactory(): Promise<void> {",
|
|
288
|
-
...bodyLines.map((l) => ` ${l}`),
|
|
289
|
-
"}"
|
|
290
|
-
].join("\n"),
|
|
291
|
-
loader: "ts",
|
|
292
|
-
resolveDir: path$1.dirname(absPath)
|
|
293
|
-
};
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Returns the absolute filesystem path to the test-runner runtime module
|
|
300
|
-
* (dist/test-runner/runtime.js — a fully self-contained page-side bundle).
|
|
301
|
-
*
|
|
302
|
-
* Rolldown code-splitting duplicates this bundling logic into shared chunks
|
|
303
|
-
* emitted at ARBITRARY dist depths: the `devtools-test` CLI pulls it from
|
|
304
|
-
* dist/test-runner/bundle.js (dir = dist/test-runner/), while the `devtools-mcp`
|
|
305
|
-
* daemon (dist/mcp/cli.js) pulls it through a ROOT chunk
|
|
306
|
-
* (dist/debug-server-<hash>.js, dir = dist/). A fixed `..`-hop candidate list
|
|
307
|
-
* is therefore wrong from at least one chunk — the live #697 regression.
|
|
308
|
-
*
|
|
309
|
-
* This resolves WITHOUT assuming chunk depth: from `import.meta.url`'s dir it
|
|
310
|
-
* probes the co-located `runtime.js` and the nested `test-runner/runtime.js`,
|
|
311
|
-
* then ascends one directory at a time (bounded) repeating both probes. The
|
|
312
|
-
* nested probe catches dist/test-runner/runtime.js from the dist/ root level no
|
|
313
|
-
* matter which depth the chunk was hoisted to (root, dist/mcp/, or a future
|
|
314
|
-
* relocation). The build always emits dist/test-runner/runtime.js (tsdown entry
|
|
315
|
-
* `'test-runner/runtime'`; guarded by scripts/check-test-runner-dist.sh).
|
|
316
|
-
*
|
|
317
|
-
* An ABSOLUTE path is returned deliberately: esbuild loads it as a literal file
|
|
318
|
-
* read, bypassing Node module resolution entirely, so this works identically in
|
|
319
|
-
* the npx-daemon context (its own dist tree) and the consumer-CLI context
|
|
320
|
-
* (the mini-app's installed @ait-co/devtools dist) — neither needs the package
|
|
321
|
-
* to be node-resolvable from the caller.
|
|
322
|
-
*/
|
|
323
|
-
function getRuntimePath() {
|
|
324
|
-
const startDir = path$1.dirname(fileURLToPath(import.meta.url));
|
|
325
|
-
const RELATIVE_PROBES = [
|
|
326
|
-
["runtime.js"],
|
|
327
|
-
["test-runner", "runtime.js"],
|
|
328
|
-
["runtime.ts"],
|
|
329
|
-
["test-runner", "runtime.ts"]
|
|
330
|
-
];
|
|
331
|
-
let dir = startDir;
|
|
332
|
-
for (let i = 0; i < 12; i++) {
|
|
333
|
-
for (const segs of RELATIVE_PROBES) {
|
|
334
|
-
const candidate = path$1.join(dir, ...segs);
|
|
335
|
-
try {
|
|
336
|
-
accessSync(candidate);
|
|
337
|
-
return candidate;
|
|
338
|
-
} catch {}
|
|
339
|
-
}
|
|
340
|
-
const parent = path$1.dirname(dir);
|
|
341
|
-
if (parent === dir) break;
|
|
342
|
-
dir = parent;
|
|
343
|
-
}
|
|
344
|
-
return path$1.join(startDir, "runtime.js");
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
|
|
348
|
-
*
|
|
349
|
-
* The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
|
|
350
|
-
* - `runTestModule` — the runtime entry (from `runtime.ts`).
|
|
351
|
-
* - `__userFactory` — an async function wrapping the user's test registration
|
|
352
|
-
* code so it runs AFTER `runTestModule` installs the globals.
|
|
353
|
-
*
|
|
354
|
-
* Callers (rpc.ts) invoke:
|
|
355
|
-
* `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
|
|
356
|
-
*
|
|
357
|
-
* @param absPath - Absolute path to the user test file.
|
|
358
|
-
* @param opts - Optional bundling overrides.
|
|
359
|
-
*/
|
|
360
|
-
async function bundleTestFile(absPath, opts) {
|
|
361
|
-
const globalName = opts?.globalName ?? "__testBundle";
|
|
362
|
-
const extraExternals = opts?.extraExternals ?? [];
|
|
363
|
-
const esbuild = await import("esbuild");
|
|
364
|
-
const runtimePath = getRuntimePath();
|
|
365
|
-
const wrapperContent = [
|
|
366
|
-
`import { runTestModule } from ${JSON.stringify(runtimePath)};`,
|
|
367
|
-
`import __userFactory from "user-test-factory";`,
|
|
368
|
-
`export { runTestModule, __userFactory };`
|
|
369
|
-
].join("\n");
|
|
370
|
-
const result = await esbuild.build({
|
|
371
|
-
stdin: {
|
|
372
|
-
contents: wrapperContent,
|
|
373
|
-
loader: "ts",
|
|
374
|
-
resolveDir: path$1.dirname(absPath)
|
|
375
|
-
},
|
|
376
|
-
bundle: true,
|
|
377
|
-
format: "iife",
|
|
378
|
-
globalName,
|
|
379
|
-
platform: "browser",
|
|
380
|
-
target: "es2022",
|
|
381
|
-
write: false,
|
|
382
|
-
plugins: [
|
|
383
|
-
userFactoryPlugin(absPath),
|
|
384
|
-
vitestRedirectPlugin(),
|
|
385
|
-
sdkRedirectPlugin()
|
|
386
|
-
],
|
|
387
|
-
external: extraExternals,
|
|
388
|
-
treeShaking: true,
|
|
389
|
-
footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
|
|
390
|
-
});
|
|
391
|
-
const warnings = result.warnings.map((w) => `${path$1.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
|
|
392
|
-
const outputFile = result.outputFiles?.[0];
|
|
393
|
-
if (!outputFile) throw new Error("bundleTestFile: esbuild produced no output — check entryPoints");
|
|
394
|
-
return {
|
|
395
|
-
code: outputFile.text,
|
|
396
|
-
warnings
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
//#endregion
|
|
400
|
-
//#region src/test-runner/capture.ts
|
|
401
|
-
/** The exact console-line prefix sdk-example's `flushCapture` emits. */
|
|
402
|
-
const CAPTURE_PREFIX = "__AIT_CAPTURE__ ";
|
|
403
|
-
/**
|
|
404
|
-
* Parses raw console line texts into {@link AitCaptureLine}s.
|
|
405
|
-
*
|
|
406
|
-
* Filtering rules (each independently drops a line — never throws):
|
|
407
|
-
* - the line text must `startsWith(CAPTURE_PREFIX)` exactly (allowlist);
|
|
408
|
-
* - there must be a non-empty category token (up to the next space);
|
|
409
|
-
* - the remaining payload must be valid JSON (`JSON.parse` succeeds).
|
|
410
|
-
*
|
|
411
|
-
* Lines that fail any rule (wss/scheme noise, truncated, broken JSON) are
|
|
412
|
-
* silently discarded — capture harvesting is best-effort and must never fail a
|
|
413
|
-
* run or leak a malformed/secret-bearing line.
|
|
414
|
-
*
|
|
415
|
-
* @param raw - Console line objects (only `.text` is read).
|
|
416
|
-
* @returns The captured lines, in input order.
|
|
417
|
-
*/
|
|
418
|
-
function parseCaptureLines(raw) {
|
|
419
|
-
const out = [];
|
|
420
|
-
for (const { text } of raw) {
|
|
421
|
-
if (!text.startsWith(CAPTURE_PREFIX)) continue;
|
|
422
|
-
const body = text.slice(16);
|
|
423
|
-
const spaceIdx = body.indexOf(" ");
|
|
424
|
-
if (spaceIdx === -1) continue;
|
|
425
|
-
const category = body.slice(0, spaceIdx);
|
|
426
|
-
const json = body.slice(spaceIdx + 1);
|
|
427
|
-
if (category === "" || json === "") continue;
|
|
428
|
-
try {
|
|
429
|
-
JSON.parse(json);
|
|
430
|
-
} catch {
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
out.push({
|
|
434
|
-
category,
|
|
435
|
-
json
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
return out;
|
|
439
|
-
}
|
|
440
|
-
//#endregion
|
|
441
|
-
//#region src/test-runner/rpc.ts
|
|
442
|
-
/** Maximum milliseconds to wait for a single evaluate round-trip. */
|
|
443
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
444
|
-
/**
|
|
445
|
-
* Wraps bundle code in a self-executing IIFE that:
|
|
446
|
-
* 1. Evaluates the bundle (registering describe/it/test).
|
|
447
|
-
* 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.
|
|
448
|
-
* 3. Returns a JSON-serialised `RunReport` string.
|
|
449
|
-
*
|
|
450
|
-
* The double-serialisation (RunReport → JSON string → returnByValue string)
|
|
451
|
-
* is intentional: CDP `returnByValue` reliably transports strings; deeply
|
|
452
|
-
* nested objects can lose fidelity across the Chii relay.
|
|
453
|
-
*
|
|
454
|
-
* SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
|
|
455
|
-
*/
|
|
456
|
-
function buildRunTestsExpression(bundleCode) {
|
|
457
|
-
return `(async () => { try { ${bundleCode} } catch(e) { return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)}); } if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') { return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'}); } try { const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory); return JSON.stringify({ok:true,value:report}); } catch(e) { return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)}); }})()`;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
|
|
461
|
-
* evaluate call into a typed `RpcRunResult`.
|
|
462
|
-
*
|
|
463
|
-
* Throws only on parse failure — an `ok:false` envelope is a normal result.
|
|
464
|
-
*
|
|
465
|
-
* SECRET-HANDLING: `rawValue` is not included in error messages.
|
|
466
|
-
*/
|
|
467
|
-
function parseRunTestsResult(rawValue) {
|
|
468
|
-
if (typeof rawValue !== "string") throw new Error(`rpc.parseRunTestsResult: unexpected return type "${typeof rawValue}" — expected JSON string`);
|
|
469
|
-
let parsed;
|
|
470
|
-
try {
|
|
471
|
-
parsed = JSON.parse(rawValue);
|
|
472
|
-
} catch {
|
|
473
|
-
throw new Error("rpc.parseRunTestsResult: bridge returned non-JSON string");
|
|
474
|
-
}
|
|
475
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("rpc.parseRunTestsResult: parsed result is not an object");
|
|
476
|
-
const obj = parsed;
|
|
477
|
-
if (obj.ok === true) return {
|
|
478
|
-
ok: true,
|
|
479
|
-
report: obj.value
|
|
480
|
-
};
|
|
481
|
-
if (obj.ok === false) return {
|
|
482
|
-
ok: false,
|
|
483
|
-
error: typeof obj.error === "string" ? obj.error : String(obj.error)
|
|
484
|
-
};
|
|
485
|
-
throw new Error("rpc.parseRunTestsResult: result missing \"ok\" field");
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Injects `bundleCode` into the attached page and awaits test execution.
|
|
489
|
-
*
|
|
490
|
-
* Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the
|
|
491
|
-
* async IIFE to settle. The 30-second CDP command timeout covers even
|
|
492
|
-
* long-running test suites; split into smaller files if you hit it.
|
|
493
|
-
*
|
|
494
|
-
* @param connection - Active CDP connection (relay or local).
|
|
495
|
-
* @param bundleCode - IIFE bundle string from `bundleTestFile`.
|
|
496
|
-
* @param timeoutMs - Override the default 30 s timeout.
|
|
497
|
-
*
|
|
498
|
-
* SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.
|
|
499
|
-
*/
|
|
500
|
-
async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
501
|
-
const expression = buildRunTestsExpression(bundleCode);
|
|
502
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs));
|
|
503
|
-
const evalPromise = connection.send("Runtime.evaluate", {
|
|
504
|
-
expression,
|
|
505
|
-
returnByValue: true,
|
|
506
|
-
awaitPromise: true
|
|
507
|
-
});
|
|
508
|
-
const cdpResult = await Promise.race([evalPromise, timeoutPromise]);
|
|
509
|
-
if (cdpResult.exceptionDetails) {
|
|
510
|
-
const msg = cdpResult.exceptionDetails.exception?.description ?? cdpResult.exceptionDetails.text ?? "Runtime.evaluate threw an exception";
|
|
511
|
-
throw new Error(`rpc.injectAndRunBundle: ${msg}`);
|
|
512
|
-
}
|
|
513
|
-
return parseRunTestsResult(cdpResult.result.value);
|
|
514
|
-
}
|
|
515
|
-
//#endregion
|
|
516
|
-
//#region src/test-runner/relay-worker.ts
|
|
517
|
-
/**
|
|
518
|
-
* Runs all `files` sequentially over the given CDP `connection`.
|
|
519
|
-
*
|
|
520
|
-
* For each file:
|
|
521
|
-
* 1. Bundle with esbuild (includes SDK shim + runtime).
|
|
522
|
-
* 2. Inject into the attached page via `Runtime.evaluate`.
|
|
523
|
-
* 3. Await the `RunReport` JSON response.
|
|
524
|
-
* 4. Accumulate results.
|
|
525
|
-
*
|
|
526
|
-
* Returns a `RelayRunReport` with per-file results and flattened totals.
|
|
527
|
-
*
|
|
528
|
-
* This function does NOT open or manage the relay connection — the caller
|
|
529
|
-
* is responsible for attaching and closing it.
|
|
530
|
-
*
|
|
531
|
-
* TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
|
|
532
|
-
* so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
|
|
533
|
-
*
|
|
534
|
-
* @param connection - Active CDP connection (relay or local kind).
|
|
535
|
-
* @param files - Absolute paths to test files, run in order.
|
|
536
|
-
* @param opts - Optional per-run overrides.
|
|
537
|
-
*/
|
|
538
|
-
async function runTestFilesOverRelay(connection, files, opts) {
|
|
539
|
-
const wallStart = Date.now();
|
|
540
|
-
const startedAt = new Date(wallStart).toISOString();
|
|
541
|
-
const fileResults = [];
|
|
542
|
-
let domainsEnabled = false;
|
|
543
|
-
try {
|
|
544
|
-
await connection.enableDomains();
|
|
545
|
-
domainsEnabled = true;
|
|
546
|
-
} catch (e) {
|
|
547
|
-
process.stderr.write(`relay-worker: enableDomains() failed before run — console capture may be empty (${e instanceof Error ? e.message : String(e)})\n`);
|
|
548
|
-
}
|
|
549
|
-
const collectCaptures = opts?.collectCaptures === true;
|
|
550
|
-
const liveConsole = [];
|
|
551
|
-
let unsubscribeConsole;
|
|
552
|
-
if (collectCaptures && domainsEnabled) unsubscribeConsole = connection.on("Runtime.consoleAPICalled", (event) => {
|
|
553
|
-
liveConsole.push(event);
|
|
554
|
-
});
|
|
555
|
-
try {
|
|
556
|
-
for (const file of files) {
|
|
557
|
-
let fileEntry;
|
|
558
|
-
try {
|
|
559
|
-
const { code } = await bundleTestFile(file, opts?.bundleOptions);
|
|
560
|
-
const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
|
|
561
|
-
if (rpcResult.ok) fileEntry = {
|
|
562
|
-
file,
|
|
563
|
-
result: rpcResult.report
|
|
564
|
-
};
|
|
565
|
-
else fileEntry = {
|
|
566
|
-
file,
|
|
567
|
-
result: { error: rpcResult.error }
|
|
568
|
-
};
|
|
569
|
-
} catch (e) {
|
|
570
|
-
fileEntry = {
|
|
571
|
-
file,
|
|
572
|
-
result: { error: e instanceof Error ? e.message : String(e) }
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
fileResults.push(fileEntry);
|
|
576
|
-
}
|
|
577
|
-
} finally {
|
|
578
|
-
unsubscribeConsole?.();
|
|
579
|
-
}
|
|
580
|
-
const captures = collectCaptures ? parseCaptureLines(liveConsole.map((e) => ({ text: renderConsoleLineText(e) }))) : [];
|
|
581
|
-
const totals = fileResults.reduce((acc, { result }) => {
|
|
582
|
-
if ("error" in result) {
|
|
583
|
-
acc.failed += 1;
|
|
584
|
-
acc.total += 1;
|
|
585
|
-
} else {
|
|
586
|
-
acc.passed += result.passed;
|
|
587
|
-
acc.failed += result.failed;
|
|
588
|
-
acc.skipped += result.skipped;
|
|
589
|
-
acc.total += result.passed + result.failed + result.skipped;
|
|
590
|
-
}
|
|
591
|
-
return acc;
|
|
592
|
-
}, {
|
|
593
|
-
passed: 0,
|
|
594
|
-
failed: 0,
|
|
595
|
-
skipped: 0,
|
|
596
|
-
total: 0
|
|
597
|
-
});
|
|
598
|
-
return {
|
|
599
|
-
startedAt,
|
|
600
|
-
duration: Date.now() - wallStart,
|
|
601
|
-
files: fileResults,
|
|
602
|
-
totals,
|
|
603
|
-
captures
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
/**
|
|
607
|
-
* Renders one `Runtime.consoleAPICalled` event to a single line of text, the
|
|
608
|
-
* same way `tools.ts#normalizeConsoleMessage` does (args rendered + space-
|
|
609
|
-
* joined). Inlined here (≈8 lines) so this module avoids importing `tools.ts`,
|
|
610
|
-
* which would drag the heavy MCP/Node graph (server-lock, parent-watcher, …)
|
|
611
|
-
* onto the test-runner entry.
|
|
612
|
-
*
|
|
613
|
-
* SECRET-HANDLING: this only stringifies console args; the caller's
|
|
614
|
-
* allowlist-prefix parser then discards everything that is not a genuine
|
|
615
|
-
* `__AIT_CAPTURE__` line.
|
|
616
|
-
*/
|
|
617
|
-
function renderConsoleLineText(event) {
|
|
618
|
-
return event.args.map((arg) => {
|
|
619
|
-
if (arg.value !== void 0) {
|
|
620
|
-
if (typeof arg.value === "string") return arg.value;
|
|
621
|
-
try {
|
|
622
|
-
return JSON.stringify(arg.value);
|
|
623
|
-
} catch {
|
|
624
|
-
return String(arg.value);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
if (arg.description !== void 0) return arg.description;
|
|
628
|
-
if (arg.className !== void 0) return arg.className;
|
|
629
|
-
return arg.subtype ?? arg.type;
|
|
630
|
-
}).join(" ");
|
|
631
|
-
}
|
|
632
|
-
//#endregion
|
|
633
|
-
//#region src/test-runner/report.ts
|
|
634
|
-
/**
|
|
635
|
-
* Runner-agnostic report serialisation for env3 test runs (devtools#696).
|
|
636
|
-
*
|
|
637
|
-
* Both env3 execution paths — the Vitest custom pool (`pool.ts`) and the
|
|
638
|
-
* standalone `devtools-test` CLI (`cli.ts`) — call the same core
|
|
639
|
-
* `runTestFilesOverRelay` and so produce the same {@link RelayRunReport}. This
|
|
640
|
-
* module is the single, runner-neutral place that turns that in-memory report
|
|
641
|
-
* into a stable on-disk artifact so a 2.x run and a 3.0 run can be diffed
|
|
642
|
-
* cell-by-cell after the fact.
|
|
643
|
-
*
|
|
644
|
-
* The serialised schema is deliberately MINIMAL and secret-free:
|
|
645
|
-
*
|
|
646
|
-
* - file paths are stored RELATIVE to `projectRoot` (no absolute `/Users/...`
|
|
647
|
-
* leakage — see {@link RunnerAgnosticReport.files});
|
|
648
|
-
* - the cell metadata (sdkLine/platform) is baked INTO the body, not only the
|
|
649
|
-
* filename, so a moved artifact never loses its provenance;
|
|
650
|
-
* - NO relay wss / scheme / TOTP / relayUrl fields exist in the schema at all
|
|
651
|
-
* (enforced by the type + this comment) — error strings are the matcher
|
|
652
|
-
* message only, inherited from rpc.ts which already strips expression/value.
|
|
653
|
-
*
|
|
654
|
-
* react-free — depends only on the type-level `RelayRunReport` and `node:fs` /
|
|
655
|
-
* `node:path`. Safe to bundle without pulling the chii/cloudflared graph.
|
|
656
|
-
*/
|
|
657
|
-
/**
|
|
658
|
-
* Converts an absolute (or already-relative) file path to a projectRoot-relative
|
|
659
|
-
* one. `path.relative` returns `''` when the paths are equal — guard that to the
|
|
660
|
-
* basename so the field is never empty.
|
|
661
|
-
*
|
|
662
|
-
* SECRET-HANDLING: this is the single choke point that strips absolute project
|
|
663
|
-
* paths from the artifact.
|
|
664
|
-
*/
|
|
665
|
-
function relativise(projectRoot, file) {
|
|
666
|
-
const rel = path.relative(projectRoot, file);
|
|
667
|
-
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return path.basename(file);
|
|
668
|
-
return rel;
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Serialises a {@link RelayRunReport} into the runner-agnostic, secret-free
|
|
672
|
-
* on-disk shape. Pure — no IO; testable with a plain report + meta.
|
|
673
|
-
*
|
|
674
|
-
* @param report - The core relay run report.
|
|
675
|
-
* @param meta - Cell axes + projectRoot (projectRoot is consumed, not stored).
|
|
676
|
-
*/
|
|
677
|
-
function serializeRelayReport(report, meta) {
|
|
678
|
-
return {
|
|
679
|
-
cell: {
|
|
680
|
-
sdkLine: meta.sdkLine,
|
|
681
|
-
platform: meta.platform
|
|
682
|
-
},
|
|
683
|
-
startedAt: report.startedAt,
|
|
684
|
-
duration: report.duration,
|
|
685
|
-
totals: report.totals,
|
|
686
|
-
files: report.files.map((f) => {
|
|
687
|
-
const file = relativise(meta.projectRoot, f.file);
|
|
688
|
-
if ("error" in f.result) return {
|
|
689
|
-
file,
|
|
690
|
-
error: f.result.error
|
|
691
|
-
};
|
|
692
|
-
return {
|
|
693
|
-
file,
|
|
694
|
-
duration: f.result.duration,
|
|
695
|
-
passed: f.result.passed,
|
|
696
|
-
failed: f.result.failed,
|
|
697
|
-
skipped: f.result.skipped,
|
|
698
|
-
tests: f.result.tests
|
|
699
|
-
};
|
|
700
|
-
})
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Writes the serialised report to `<dir>/<sdkLine>.<platform>.json`, creating
|
|
705
|
-
* `dir` if needed. Returns the absolute path written.
|
|
706
|
-
*
|
|
707
|
-
* The cell-suffixed filename keeps 2.x and 3.0 (and per-platform) runs as
|
|
708
|
-
* distinct artifacts in the same directory; the same cell metadata is also baked
|
|
709
|
-
* into the body so a renamed/moved file still carries its provenance.
|
|
710
|
-
*
|
|
711
|
-
* SECRET-HANDLING: the written body contains no relay/secret fields (the schema
|
|
712
|
-
* has none). `dir`/`projectRoot` are local filesystem paths, never logged here.
|
|
713
|
-
*
|
|
714
|
-
* @param report - The core relay run report.
|
|
715
|
-
* @param dir - Output directory (created recursively if missing).
|
|
716
|
-
* @param meta - Cell axes + projectRoot.
|
|
717
|
-
* @returns The absolute path of the written file.
|
|
718
|
-
*/
|
|
719
|
-
async function writeReportArtifact(report, dir, meta) {
|
|
720
|
-
const serialised = serializeRelayReport(report, meta);
|
|
721
|
-
await mkdir(dir, { recursive: true });
|
|
722
|
-
const outFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.json`);
|
|
723
|
-
await writeFile(outFile, `${JSON.stringify(serialised, null, 2)}\n`, "utf8");
|
|
724
|
-
return outFile;
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Writes harvested `__AIT_CAPTURE__` lines to per-category files under `dir`,
|
|
728
|
-
* named `<category>.<sdkLine>.<platform>.json` — the SAME convention
|
|
729
|
-
* sdk-example's env1 `flushCapture` uses on the filesystem, so env1 and env3
|
|
730
|
-
* capture artifacts line up for diffing.
|
|
731
|
-
*
|
|
732
|
-
* Each line's `json` payload is an opaque JSON array of capture records. Lines
|
|
733
|
-
* sharing a category are concatenated into one array, in harvest order.
|
|
734
|
-
*
|
|
735
|
-
* SECRET-HANDLING: only allowlist-prefixed capture lines reach here (the parser
|
|
736
|
-
* dropped wss/scheme noise); the `json` payload is written verbatim but is a
|
|
737
|
-
* capture record array, not a relay/secret.
|
|
738
|
-
*
|
|
739
|
-
* @param captures - Parsed capture lines (from `RelayRunReport.captures`).
|
|
740
|
-
* @param dir - Output directory (created recursively if missing).
|
|
741
|
-
* @param cell - Cell axes for the filename suffix.
|
|
742
|
-
* @returns The absolute paths written (one per category), in category order.
|
|
743
|
-
*/
|
|
744
|
-
async function writeCaptureArtifacts(captures, dir, cell) {
|
|
745
|
-
if (captures.length === 0) return [];
|
|
746
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
747
|
-
for (const { category, json } of captures) {
|
|
748
|
-
let merged = byCategory.get(category);
|
|
749
|
-
if (!merged) {
|
|
750
|
-
merged = [];
|
|
751
|
-
byCategory.set(category, merged);
|
|
752
|
-
}
|
|
753
|
-
const parsed = JSON.parse(json);
|
|
754
|
-
if (Array.isArray(parsed)) merged.push(...parsed);
|
|
755
|
-
else merged.push(parsed);
|
|
756
|
-
}
|
|
757
|
-
await mkdir(dir, { recursive: true });
|
|
758
|
-
const written = [];
|
|
759
|
-
for (const [category, records] of byCategory) {
|
|
760
|
-
const outFile = path.join(dir, `${category}.${cell.sdkLine}.${cell.platform}.json`);
|
|
761
|
-
await writeFile(outFile, `${JSON.stringify(records, null, 2)}\n`, "utf8");
|
|
762
|
-
written.push(outFile);
|
|
763
|
-
}
|
|
764
|
-
return written;
|
|
765
|
-
}
|
|
766
|
-
//#endregion
|
|
767
|
-
//#region src/test-runner/cli.ts
|
|
768
|
-
/**
|
|
769
|
-
* `devtools-test` CLI.
|
|
770
|
-
*
|
|
771
|
-
* Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)
|
|
772
|
-
* and exposes `runWithConnection` — the pure run core that bundles, injects, and
|
|
773
|
-
* collects each file over a CDP connection. The CLI's `main()` performs a
|
|
774
|
-
* standalone relay attach (boot relay → QR → phone scan → cell inject → run).
|
|
775
|
-
*
|
|
776
|
-
* NOTE: no shebang in this source file — the tsdown entry's `banner` option
|
|
777
|
-
* injects `#!/usr/bin/env node` into the compiled output (same pattern as
|
|
778
|
-
* `src/mcp/cli.ts`).
|
|
779
|
-
*/
|
|
780
|
-
const USAGE = `
|
|
15
|
+
`
|
|
781
16
|
devtools-test — run mini-app tests on a real device WebView over the CDP relay
|
|
782
17
|
|
|
783
18
|
USAGE
|
|
@@ -824,178 +59,6 @@ EXAMPLE
|
|
|
824
59
|
--timeout 60000
|
|
825
60
|
|
|
826
61
|
`.trimStart();
|
|
827
|
-
/**
|
|
828
|
-
* Runs `files` over `connection` and returns the aggregate report.
|
|
829
|
-
* This pure function is the testable core of the CLI (and is what the
|
|
830
|
-
* `run_tests` MCP tool calls against the daemon's attached connection); it is
|
|
831
|
-
* separate from `main()` so tests can call it without spawning a subprocess.
|
|
832
|
-
*/
|
|
833
|
-
async function runWithConnection(connection, files, opts) {
|
|
834
|
-
const report = await runTestFilesOverRelay(connection, files, opts);
|
|
835
|
-
if (opts?.printSummary) {
|
|
836
|
-
const { totals } = report;
|
|
837
|
-
process.stdout.write(`\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\n`);
|
|
838
|
-
}
|
|
839
|
-
return report;
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Decides whether to suppress the QR/attach block on stdout.
|
|
843
|
-
*
|
|
844
|
-
* Suppress when EITHER the user passed `--no-qr-stdout`, OR stdout is not a TTY
|
|
845
|
-
* / `CI` is set / `AIT_NO_QR_STDOUT` is set (non-interactive — a captured stdout
|
|
846
|
-
* must not leak the relay wss + TOTP `at=` code that the QR block encodes). The
|
|
847
|
-
* suppression is whole-chunk: `attachUrl` AND `relayUrl` ride in the same block.
|
|
848
|
-
*
|
|
849
|
-
* Exported for unit testing.
|
|
850
|
-
*/
|
|
851
|
-
function shouldSuppressQr(noQrFlag) {
|
|
852
|
-
return noQrFlag || !process.stdout.isTTY || process.env.CI !== void 0 || process.env.AIT_NO_QR_STDOUT !== void 0;
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* CLI entry point.
|
|
856
|
-
*
|
|
857
|
-
* Performs a standalone relay attach → run lifecycle, sharing the attach
|
|
858
|
-
* assembly with the Vitest pool via `createRelayConnectionFactory` (single
|
|
859
|
-
* source — no drift):
|
|
860
|
-
*
|
|
861
|
-
* 1. Parse args: globs, --timeout, --cell-sdk-line, --cell-platform,
|
|
862
|
-
* --scheme-url (required for env3), --report-dir, --no-qr-stdout,
|
|
863
|
-
* --headless, --project-root.
|
|
864
|
-
* 2. Discover test files; exit 1 if none.
|
|
865
|
-
* 3. factory.open() — boot relay → render QR (suppressed on non-interactive
|
|
866
|
-
* stdout) → wait for phone → inject cell → enableDomains. Returns the conn.
|
|
867
|
-
* 4. runWithConnection(conn, files, { timeoutMs, collectCaptures, printSummary }).
|
|
868
|
-
* 5. With --report-dir: write the runner-agnostic report + capture files.
|
|
869
|
-
* 6. factory.close(); process.exitCode = failed > 0 ? 1 : 0.
|
|
870
|
-
*
|
|
871
|
-
* The CLI is not a daemon — no lock, router, SSE, or tools_list is needed.
|
|
872
|
-
* Attach timeout exits with code 1; test failures exit with code 1.
|
|
873
|
-
*
|
|
874
|
-
* SECRET-HANDLING: scheme_url / relay wssUrl / TOTP codes are never written to
|
|
875
|
-
* stdout/stderr directly. The QR block (which encodes the TOTP `at=` code) is
|
|
876
|
-
* printed only when stdout is interactive AND not suppressed.
|
|
877
|
-
*/
|
|
878
|
-
async function main(argv = process.argv.slice(2)) {
|
|
879
|
-
let parsed;
|
|
880
|
-
try {
|
|
881
|
-
parsed = parseArgs({
|
|
882
|
-
args: argv,
|
|
883
|
-
options: {
|
|
884
|
-
help: {
|
|
885
|
-
type: "boolean",
|
|
886
|
-
short: "h"
|
|
887
|
-
},
|
|
888
|
-
timeout: { type: "string" },
|
|
889
|
-
"scheme-url": { type: "string" },
|
|
890
|
-
"cell-sdk-line": { type: "string" },
|
|
891
|
-
"cell-platform": { type: "string" },
|
|
892
|
-
"report-dir": { type: "string" },
|
|
893
|
-
"no-qr-stdout": { type: "boolean" },
|
|
894
|
-
headless: { type: "boolean" },
|
|
895
|
-
"project-root": { type: "string" }
|
|
896
|
-
},
|
|
897
|
-
allowPositionals: true
|
|
898
|
-
});
|
|
899
|
-
} catch (e) {
|
|
900
|
-
process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
901
|
-
process.exitCode = 1;
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
if (parsed.values.help || argv.length === 0) {
|
|
905
|
-
process.stdout.write(USAGE);
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
const vals = parsed.values;
|
|
909
|
-
const rawTimeout = typeof vals.timeout === "string" ? vals.timeout : void 0;
|
|
910
|
-
const timeoutMs = rawTimeout !== void 0 ? parseInt(rawTimeout, 10) : 3e4;
|
|
911
|
-
if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
912
|
-
process.stderr.write(`devtools-test: --timeout must be a positive integer\n`);
|
|
913
|
-
process.exitCode = 1;
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
const schemeUrl = typeof vals["scheme-url"] === "string" ? vals["scheme-url"] : "";
|
|
917
|
-
if (schemeUrl === "") {
|
|
918
|
-
process.stderr.write("devtools-test: --scheme-url is required for standalone relay attach.\n Pass the intoss-private:// URL from `ait deploy --scheme-only`.\n");
|
|
919
|
-
process.exitCode = 1;
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
const headless = vals.headless === true;
|
|
923
|
-
const projectRoot = typeof vals["project-root"] === "string" ? vals["project-root"] : process.cwd();
|
|
924
|
-
const reportDir = typeof vals["report-dir"] === "string" ? vals["report-dir"] : void 0;
|
|
925
|
-
const suppressQr = shouldSuppressQr(vals["no-qr-stdout"] === true);
|
|
926
|
-
const cellSdkLine = typeof vals["cell-sdk-line"] === "string" ? vals["cell-sdk-line"] : void 0;
|
|
927
|
-
const cellPlatform = typeof vals["cell-platform"] === "string" ? vals["cell-platform"] : process.env.AIT_CELL_PLATFORM;
|
|
928
|
-
const hasCell = cellSdkLine !== void 0 || cellPlatform !== void 0;
|
|
929
|
-
const cell = {
|
|
930
|
-
sdkLine: cellSdkLine ?? "2.x",
|
|
931
|
-
platform: cellPlatform ?? "mock"
|
|
932
|
-
};
|
|
933
|
-
const globs = parsed.positionals;
|
|
934
|
-
if (globs.length === 0) {
|
|
935
|
-
process.stderr.write(`devtools-test: at least one glob pattern is required\n`);
|
|
936
|
-
process.stdout.write(USAGE);
|
|
937
|
-
process.exitCode = 1;
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
const files = await discoverTestFiles(globs, process.cwd());
|
|
941
|
-
if (files.length === 0) {
|
|
942
|
-
process.stderr.write(`devtools-test: no test files matched ${globs.join(", ")}\n`);
|
|
943
|
-
process.exitCode = 1;
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
process.stderr.write(`devtools-test: found ${files.length} test file(s)\n`);
|
|
947
|
-
if (hasCell) process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\n`);
|
|
948
|
-
const factory = createRelayConnectionFactory({
|
|
949
|
-
schemeUrl,
|
|
950
|
-
projectRoot,
|
|
951
|
-
timeoutMs,
|
|
952
|
-
headless,
|
|
953
|
-
cell: hasCell ? cell : void 0,
|
|
954
|
-
onQrContent: (chunks) => {
|
|
955
|
-
if (suppressQr) {
|
|
956
|
-
process.stdout.write("QR suppressed (non-interactive)\n");
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
for (const chunk of chunks) process.stdout.write(`${chunk}\n`);
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
let connection;
|
|
963
|
-
try {
|
|
964
|
-
connection = await factory.open();
|
|
965
|
-
} catch (e) {
|
|
966
|
-
process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
967
|
-
process.exitCode = 1;
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
970
|
-
let exitCode = 0;
|
|
971
|
-
try {
|
|
972
|
-
const report = await runWithConnection(connection, files, {
|
|
973
|
-
timeoutMs,
|
|
974
|
-
printSummary: true,
|
|
975
|
-
collectCaptures: reportDir !== void 0
|
|
976
|
-
});
|
|
977
|
-
if (reportDir !== void 0) try {
|
|
978
|
-
const reportPath = await writeReportArtifact(report, reportDir, {
|
|
979
|
-
sdkLine: cell.sdkLine,
|
|
980
|
-
platform: cell.platform,
|
|
981
|
-
projectRoot
|
|
982
|
-
});
|
|
983
|
-
process.stderr.write(`devtools-test: wrote report ${reportPath}\n`);
|
|
984
|
-
const capturePaths = await writeCaptureArtifacts(report.captures, `${reportDir}/.ait-capture`, cell);
|
|
985
|
-
if (capturePaths.length > 0) process.stderr.write(`devtools-test: wrote ${capturePaths.length} capture file(s)\n`);
|
|
986
|
-
} catch (e) {
|
|
987
|
-
process.stderr.write(`devtools-test: failed to write report artifacts: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
988
|
-
}
|
|
989
|
-
exitCode = report.totals.failed > 0 ? 1 : 0;
|
|
990
|
-
} finally {
|
|
991
|
-
await factory.close(connection);
|
|
992
|
-
process.exitCode = exitCode;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
if (import.meta.url === new URL(process.argv[1], "file://").href) main().catch((e) => {
|
|
996
|
-
process.stderr.write(`devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
997
|
-
process.exitCode = 1;
|
|
998
|
-
});
|
|
999
62
|
//#endregion
|
|
1000
63
|
//#region src/shared/relay-auth-close.ts
|
|
1001
64
|
/**
|
|
@@ -1906,4 +969,4 @@ async function bootRelayFamily(options = {}) {
|
|
|
1906
969
|
//#endregion
|
|
1907
970
|
export { bootRelayFamily, buildRelayVerifyAuth };
|
|
1908
971
|
|
|
1909
|
-
//# sourceMappingURL=debug-server-
|
|
972
|
+
//# sourceMappingURL=debug-server-C0A_Kujq.js.map
|