@ait-co/devtools 0.1.107 → 0.1.109
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/bundle-KFs4t-wc.d.ts +96 -0
- package/dist/bundle-KFs4t-wc.d.ts.map +1 -0
- package/dist/cdp-connection-C0AP0tH2.d.ts +277 -0
- package/dist/cdp-connection-C0AP0tH2.d.ts.map +1 -0
- package/dist/in-app/auto.d.ts +17 -0
- package/dist/in-app/auto.d.ts.map +1 -1
- package/dist/in-app/auto.js +76 -0
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts +48 -1
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +60 -1
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +651 -9
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +59 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +1 -1
- package/dist/pool-CuVMzWGB.d.ts +14577 -0
- package/dist/pool-CuVMzWGB.d.ts.map +1 -0
- package/dist/relay-worker-xxanNQGs.d.ts +74 -0
- package/dist/relay-worker-xxanNQGs.d.ts.map +1 -0
- package/dist/runtime-Wi5d6Ywz.d.ts +50 -0
- package/dist/runtime-Wi5d6Ywz.d.ts.map +1 -0
- package/dist/test-runner/bundle.d.ts +2 -0
- package/dist/test-runner/bundle.js +232 -0
- package/dist/test-runner/bundle.js.map +1 -0
- package/dist/test-runner/cli.d.ts +462 -0
- package/dist/test-runner/cli.d.ts.map +1 -0
- package/dist/test-runner/cli.js +516 -0
- package/dist/test-runner/cli.js.map +1 -0
- package/dist/test-runner/config.d.ts +80 -0
- package/dist/test-runner/config.d.ts.map +1 -0
- package/dist/test-runner/config.js +54 -0
- package/dist/test-runner/config.js.map +1 -0
- package/dist/test-runner/pool.d.ts +2 -0
- package/dist/test-runner/pool.js +136 -0
- package/dist/test-runner/pool.js.map +1 -0
- package/dist/test-runner/relay-worker.d.ts +2 -0
- package/dist/test-runner/relay-worker.js +96 -0
- package/dist/test-runner/relay-worker.js.map +1 -0
- package/dist/test-runner/rpc.d.ts +53 -0
- package/dist/test-runner/rpc.d.ts.map +1 -0
- package/dist/test-runner/rpc.js +78 -0
- package/dist/test-runner/rpc.js.map +1 -0
- package/dist/test-runner/task-graph.d.ts +38 -0
- package/dist/test-runner/task-graph.d.ts.map +1 -0
- package/dist/test-runner/task-graph.js +182 -0
- package/dist/test-runner/task-graph.js.map +1 -0
- package/dist/unplugin/index.d.cts +13 -32
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +13 -32
- package/dist/unplugin/index.d.ts.map +1 -1
- package/package.json +13 -3
package/dist/mcp/cli.js
CHANGED
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-WY6l0ysP.js";
|
|
3
3
|
import { t as loadRelaySecretReadOnly } from "../relay-secret-store-DhzAnnj-.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { accessSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { argv } from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
10
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import * as fs from "node:fs/promises";
|
|
13
|
+
import { glob } from "node:fs/promises";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
11
16
|
import { EventEmitter } from "node:events";
|
|
12
17
|
import { WebSocket, WebSocketServer } from "ws";
|
|
13
18
|
import { randomBytes } from "node:crypto";
|
|
@@ -15,7 +20,6 @@ import { createServer } from "node:http";
|
|
|
15
20
|
import { spawn } from "node:child_process";
|
|
16
21
|
import net from "node:net";
|
|
17
22
|
import { homedir, platform } from "node:os";
|
|
18
|
-
import { join } from "node:path";
|
|
19
23
|
import { Tunnel, bin, install } from "cloudflared";
|
|
20
24
|
//#region \0rolldown/runtime.js
|
|
21
25
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
@@ -119,6 +123,511 @@ function startMaxAgeWatchdog(onExpired, opts = {}) {
|
|
|
119
123
|
} };
|
|
120
124
|
}
|
|
121
125
|
//#endregion
|
|
126
|
+
//#region src/test-runner/discover.ts
|
|
127
|
+
/**
|
|
128
|
+
* Test-file discovery shared by the `devtools-test` CLI and the `run_tests`
|
|
129
|
+
* MCP tool, so both expand glob patterns with identical semantics.
|
|
130
|
+
*
|
|
131
|
+
* Uses Node's built-in `fs/promises` `glob` (Node 22+) — no extra dependency,
|
|
132
|
+
* which keeps the MCP daemon install graph lean (a plain glob lib would land in
|
|
133
|
+
* the `npx … devtools-mcp` path for no benefit).
|
|
134
|
+
*
|
|
135
|
+
* Pure Node IO only (`node:fs/promises` + `node:path`) — react-free, so it is
|
|
136
|
+
* safe to import from the MCP daemon graph.
|
|
137
|
+
*/
|
|
138
|
+
/**
|
|
139
|
+
* Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of
|
|
140
|
+
* ABSOLUTE test file paths, resolved relative to `cwd`.
|
|
141
|
+
*
|
|
142
|
+
* A plain (non-glob) path passes through when it matches a real file; a glob
|
|
143
|
+
* expands against `cwd`. Absolute matches are kept as-is; relative matches are
|
|
144
|
+
* resolved against `cwd`. `bundleTestFile` requires an absolute path, so the
|
|
145
|
+
* absolute output feeds it directly.
|
|
146
|
+
*
|
|
147
|
+
* @param patterns Glob patterns or file paths (e.g. `['src/**\/*.phone.test.ts']`).
|
|
148
|
+
* @param cwd Base directory for relative patterns/results.
|
|
149
|
+
* @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.
|
|
150
|
+
*/
|
|
151
|
+
async function discoverTestFiles(patterns, cwd) {
|
|
152
|
+
const out = /* @__PURE__ */ new Set();
|
|
153
|
+
for await (const match of glob(patterns, { cwd })) out.add(isAbsolute(match) ? match : resolve(cwd, match));
|
|
154
|
+
return [...out].sort();
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/test-runner/bundle.ts
|
|
158
|
+
/**
|
|
159
|
+
* esbuild-based bundler for user test files.
|
|
160
|
+
*
|
|
161
|
+
* Bundles a single test file into a self-contained IIFE string that can be
|
|
162
|
+
* injected into a WebView via `Runtime.evaluate`. The bundle includes the
|
|
163
|
+
* test runtime (`runtime.ts`), which provides `describe/it/test/expect` and
|
|
164
|
+
* the `runTestModule(factory)` entry point.
|
|
165
|
+
*
|
|
166
|
+
* ## How the wiring works
|
|
167
|
+
*
|
|
168
|
+
* The bundle exposes two exports on `globalThis.__testBundle`:
|
|
169
|
+
* - `runTestModule` — the runtime's entry function.
|
|
170
|
+
* - `__userFactory` — an async function whose body is the user's top-level
|
|
171
|
+
* test registration code (describe/it/test calls).
|
|
172
|
+
*
|
|
173
|
+
* The Node-side RPC (`rpc.ts`) calls:
|
|
174
|
+
* `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
|
|
175
|
+
*
|
|
176
|
+
* `runTestModule` then installs `describe/it/test/expect` as globals, invokes
|
|
177
|
+
* the factory (which registers all tests), runs them, and returns a `RunReport`.
|
|
178
|
+
*
|
|
179
|
+
* ## Why a factory wrapper is needed
|
|
180
|
+
*
|
|
181
|
+
* Naively adding the runtime to `entryPoints` and bundling the user file would
|
|
182
|
+
* fail for two reasons:
|
|
183
|
+
* 1. `describe/it/test/expect` from the runtime are module-local in the IIFE
|
|
184
|
+
* scope. The user's top-level `describe(...)` calls expect them as globals —
|
|
185
|
+
* they are not globals until `runTestModule` installs them.
|
|
186
|
+
* 2. Even with globals pre-installed, the user file runs at IIFE-evaluation
|
|
187
|
+
* time, before the RPC layer calls `runTestModule` to reset state and start
|
|
188
|
+
* the test clock.
|
|
189
|
+
*
|
|
190
|
+
* The factory approach solves both: the user's registration code is deferred
|
|
191
|
+
* into a function that `runTestModule` calls AFTER installing the globals.
|
|
192
|
+
*
|
|
193
|
+
* ## Factory extraction algorithm
|
|
194
|
+
*
|
|
195
|
+
* The `userFactoryPlugin` reads the user file and splits lines into:
|
|
196
|
+
* - **top-level**: `import …` and re-export lines — kept at module scope
|
|
197
|
+
* (the only valid position for static `import` in ESM).
|
|
198
|
+
* - **body**: all other statements — moved into the body of the exported
|
|
199
|
+
* `__userFactory` async function.
|
|
200
|
+
*
|
|
201
|
+
* esbuild processes the re-generated module, following each static import
|
|
202
|
+
* through the normal dependency graph (including the SDK-redirect plugin).
|
|
203
|
+
*
|
|
204
|
+
* ## SDK redirect
|
|
205
|
+
*
|
|
206
|
+
* Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via
|
|
207
|
+
* the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that
|
|
208
|
+
* `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.
|
|
209
|
+
*
|
|
210
|
+
* SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
|
|
211
|
+
*/
|
|
212
|
+
/**
|
|
213
|
+
* Matches the bare SDK package and any sub-path import
|
|
214
|
+
* (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).
|
|
215
|
+
* Built from {@link SDK_PACKAGE} so the package name has a single source.
|
|
216
|
+
*/
|
|
217
|
+
const SDK_IMPORT_FILTER = new RegExp(`^${"@apps-in-toss/web-framework".replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
218
|
+
/**
|
|
219
|
+
* esbuild plugin that intercepts SDK imports and redirects them to the
|
|
220
|
+
* `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.
|
|
221
|
+
*
|
|
222
|
+
* Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),
|
|
223
|
+
* esbuild resolves it to a virtual module that re-exports all named exports
|
|
224
|
+
* via `window.__sdk[name]`. This avoids bundling the real SDK (which may not
|
|
225
|
+
* be available in the test environment) while still making named imports work.
|
|
226
|
+
*
|
|
227
|
+
* If `window.__sdk` is absent (non-dog-food build), every access throws a
|
|
228
|
+
* descriptive error rather than returning `undefined` silently.
|
|
229
|
+
*/
|
|
230
|
+
function sdkRedirectPlugin() {
|
|
231
|
+
return {
|
|
232
|
+
name: "sdk-redirect",
|
|
233
|
+
setup(build) {
|
|
234
|
+
build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({
|
|
235
|
+
path: args.path,
|
|
236
|
+
namespace: "sdk-redirect"
|
|
237
|
+
}));
|
|
238
|
+
build.onLoad({
|
|
239
|
+
filter: /.*/,
|
|
240
|
+
namespace: "sdk-redirect"
|
|
241
|
+
}, () => ({
|
|
242
|
+
contents: `
|
|
243
|
+
var __proxy = (typeof window !== 'undefined' && window.__sdk)
|
|
244
|
+
? window.__sdk
|
|
245
|
+
: new Proxy({}, {
|
|
246
|
+
get: function(_t, p) {
|
|
247
|
+
throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
module.exports = __proxy;
|
|
251
|
+
`,
|
|
252
|
+
loader: "js"
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* esbuild plugin that transforms the user test file into a module that exports
|
|
259
|
+
* an async `__userFactory` function. The factory defers the user's top-level
|
|
260
|
+
* test registration code (describe/it/test calls) so it only runs when
|
|
261
|
+
* `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has
|
|
262
|
+
* installed describe/it/test/expect as globals.
|
|
263
|
+
*
|
|
264
|
+
* Algorithm:
|
|
265
|
+
* - Lines matching import declarations or re-export statements are kept at
|
|
266
|
+
* module top-level (the only valid ESM position for static `import`).
|
|
267
|
+
* - All other lines (describe/it/test calls, local declarations, etc.) are
|
|
268
|
+
* moved into the body of the exported async factory function.
|
|
269
|
+
*
|
|
270
|
+
* This preserves SDK import resolution (the sdk-redirect plugin processes
|
|
271
|
+
* top-level imports normally) while deferring test registration to the factory.
|
|
272
|
+
*/
|
|
273
|
+
function userFactoryPlugin(absPath) {
|
|
274
|
+
const NAMESPACE = "user-test-factory";
|
|
275
|
+
return {
|
|
276
|
+
name: "user-test-factory",
|
|
277
|
+
setup(build) {
|
|
278
|
+
build.onResolve({ filter: /^user-test-factory$/ }, () => ({
|
|
279
|
+
path: absPath,
|
|
280
|
+
namespace: NAMESPACE
|
|
281
|
+
}));
|
|
282
|
+
build.onLoad({
|
|
283
|
+
filter: /.*/,
|
|
284
|
+
namespace: NAMESPACE
|
|
285
|
+
}, async (args) => {
|
|
286
|
+
const lines = (await fs.readFile(args.path, "utf8")).split("\n");
|
|
287
|
+
const topLevelLines = [];
|
|
288
|
+
const bodyLines = [];
|
|
289
|
+
const EXPORT_DECLARATION_RE = /^(export\s+)(default\s+|async\s+function\s+|function\s+|class\s+|const\s+|let\s+|var\s+)/;
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
const trimmed = line.trimStart();
|
|
292
|
+
const indent = line.slice(0, line.length - trimmed.length);
|
|
293
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import'") || trimmed.startsWith("import\"")) topLevelLines.push(line);
|
|
294
|
+
else if (trimmed.startsWith("export ")) if (trimmed.match(EXPORT_DECLARATION_RE)) bodyLines.push(indent + trimmed.slice(7));
|
|
295
|
+
else topLevelLines.push(line);
|
|
296
|
+
else bodyLines.push(line);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
contents: [
|
|
300
|
+
...topLevelLines,
|
|
301
|
+
"",
|
|
302
|
+
"// biome-ignore lint: generated factory wrapper",
|
|
303
|
+
"export default async function __userFactory(): Promise<void> {",
|
|
304
|
+
...bodyLines.map((l) => ` ${l}`),
|
|
305
|
+
"}"
|
|
306
|
+
].join("\n"),
|
|
307
|
+
loader: "ts",
|
|
308
|
+
resolveDir: path.dirname(absPath)
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Returns the absolute path to the co-located runtime module.
|
|
316
|
+
*
|
|
317
|
+
* In the source tree (running via tsx / ts-node) the file is `runtime.ts`.
|
|
318
|
+
* After `tsdown` compiles to `dist/test-runner/`, it becomes `runtime.js`.
|
|
319
|
+
* We try both extensions to support both environments.
|
|
320
|
+
*/
|
|
321
|
+
function getRuntimePath() {
|
|
322
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
323
|
+
for (const ext of [".ts", ".js"]) {
|
|
324
|
+
const candidate = path.join(dir, `runtime${ext}`);
|
|
325
|
+
try {
|
|
326
|
+
accessSync(candidate);
|
|
327
|
+
return candidate;
|
|
328
|
+
} catch {}
|
|
329
|
+
}
|
|
330
|
+
return path.join(dir, "runtime.js");
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
|
|
334
|
+
*
|
|
335
|
+
* The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
|
|
336
|
+
* - `runTestModule` — the runtime entry (from `runtime.ts`).
|
|
337
|
+
* - `__userFactory` — an async function wrapping the user's test registration
|
|
338
|
+
* code so it runs AFTER `runTestModule` installs the globals.
|
|
339
|
+
*
|
|
340
|
+
* Callers (rpc.ts) invoke:
|
|
341
|
+
* `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
|
|
342
|
+
*
|
|
343
|
+
* @param absPath - Absolute path to the user test file.
|
|
344
|
+
* @param opts - Optional bundling overrides.
|
|
345
|
+
*/
|
|
346
|
+
async function bundleTestFile(absPath, opts) {
|
|
347
|
+
const globalName = opts?.globalName ?? "__testBundle";
|
|
348
|
+
const extraExternals = opts?.extraExternals ?? [];
|
|
349
|
+
const esbuild = await import("esbuild");
|
|
350
|
+
const runtimePath = getRuntimePath();
|
|
351
|
+
const wrapperContent = [
|
|
352
|
+
`import { runTestModule } from ${JSON.stringify(runtimePath)};`,
|
|
353
|
+
`import __userFactory from "user-test-factory";`,
|
|
354
|
+
`export { runTestModule, __userFactory };`
|
|
355
|
+
].join("\n");
|
|
356
|
+
const result = await esbuild.build({
|
|
357
|
+
stdin: {
|
|
358
|
+
contents: wrapperContent,
|
|
359
|
+
loader: "ts",
|
|
360
|
+
resolveDir: path.dirname(absPath)
|
|
361
|
+
},
|
|
362
|
+
bundle: true,
|
|
363
|
+
format: "iife",
|
|
364
|
+
globalName,
|
|
365
|
+
platform: "browser",
|
|
366
|
+
target: "es2022",
|
|
367
|
+
write: false,
|
|
368
|
+
plugins: [userFactoryPlugin(absPath), sdkRedirectPlugin()],
|
|
369
|
+
external: extraExternals,
|
|
370
|
+
treeShaking: true,
|
|
371
|
+
footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
|
|
372
|
+
});
|
|
373
|
+
const warnings = result.warnings.map((w) => `${path.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
|
|
374
|
+
const outputFile = result.outputFiles?.[0];
|
|
375
|
+
if (!outputFile) throw new Error("bundleTestFile: esbuild produced no output — check entryPoints");
|
|
376
|
+
return {
|
|
377
|
+
code: outputFile.text,
|
|
378
|
+
warnings
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/test-runner/rpc.ts
|
|
383
|
+
/** Maximum milliseconds to wait for a single evaluate round-trip. */
|
|
384
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
385
|
+
/**
|
|
386
|
+
* Wraps bundle code in a self-executing IIFE that:
|
|
387
|
+
* 1. Evaluates the bundle (registering describe/it/test).
|
|
388
|
+
* 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.
|
|
389
|
+
* 3. Returns a JSON-serialised `RunReport` string.
|
|
390
|
+
*
|
|
391
|
+
* The double-serialisation (RunReport → JSON string → returnByValue string)
|
|
392
|
+
* is intentional: CDP `returnByValue` reliably transports strings; deeply
|
|
393
|
+
* nested objects can lose fidelity across the Chii relay.
|
|
394
|
+
*
|
|
395
|
+
* SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
|
|
396
|
+
*/
|
|
397
|
+
function buildRunTestsExpression(bundleCode) {
|
|
398
|
+
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)}); }})()`;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
|
|
402
|
+
* evaluate call into a typed `RpcRunResult`.
|
|
403
|
+
*
|
|
404
|
+
* Throws only on parse failure — an `ok:false` envelope is a normal result.
|
|
405
|
+
*
|
|
406
|
+
* SECRET-HANDLING: `rawValue` is not included in error messages.
|
|
407
|
+
*/
|
|
408
|
+
function parseRunTestsResult(rawValue) {
|
|
409
|
+
if (typeof rawValue !== "string") throw new Error(`rpc.parseRunTestsResult: unexpected return type "${typeof rawValue}" — expected JSON string`);
|
|
410
|
+
let parsed;
|
|
411
|
+
try {
|
|
412
|
+
parsed = JSON.parse(rawValue);
|
|
413
|
+
} catch {
|
|
414
|
+
throw new Error("rpc.parseRunTestsResult: bridge returned non-JSON string");
|
|
415
|
+
}
|
|
416
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("rpc.parseRunTestsResult: parsed result is not an object");
|
|
417
|
+
const obj = parsed;
|
|
418
|
+
if (obj.ok === true) return {
|
|
419
|
+
ok: true,
|
|
420
|
+
report: obj.value
|
|
421
|
+
};
|
|
422
|
+
if (obj.ok === false) return {
|
|
423
|
+
ok: false,
|
|
424
|
+
error: typeof obj.error === "string" ? obj.error : String(obj.error)
|
|
425
|
+
};
|
|
426
|
+
throw new Error("rpc.parseRunTestsResult: result missing \"ok\" field");
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Injects `bundleCode` into the attached page and awaits test execution.
|
|
430
|
+
*
|
|
431
|
+
* Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the
|
|
432
|
+
* async IIFE to settle. The 30-second CDP command timeout covers even
|
|
433
|
+
* long-running test suites; split into smaller files if you hit it.
|
|
434
|
+
*
|
|
435
|
+
* @param connection - Active CDP connection (relay or local).
|
|
436
|
+
* @param bundleCode - IIFE bundle string from `bundleTestFile`.
|
|
437
|
+
* @param timeoutMs - Override the default 30 s timeout.
|
|
438
|
+
*
|
|
439
|
+
* SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.
|
|
440
|
+
*/
|
|
441
|
+
async function injectAndRunBundle(connection, bundleCode, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
442
|
+
const expression = buildRunTestsExpression(bundleCode);
|
|
443
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs));
|
|
444
|
+
const evalPromise = connection.send("Runtime.evaluate", {
|
|
445
|
+
expression,
|
|
446
|
+
returnByValue: true,
|
|
447
|
+
awaitPromise: true
|
|
448
|
+
});
|
|
449
|
+
const cdpResult = await Promise.race([evalPromise, timeoutPromise]);
|
|
450
|
+
if (cdpResult.exceptionDetails) {
|
|
451
|
+
const msg = cdpResult.exceptionDetails.exception?.description ?? cdpResult.exceptionDetails.text ?? "Runtime.evaluate threw an exception";
|
|
452
|
+
throw new Error(`rpc.injectAndRunBundle: ${msg}`);
|
|
453
|
+
}
|
|
454
|
+
return parseRunTestsResult(cdpResult.result.value);
|
|
455
|
+
}
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/test-runner/relay-worker.ts
|
|
458
|
+
/**
|
|
459
|
+
* Runs all `files` sequentially over the given CDP `connection`.
|
|
460
|
+
*
|
|
461
|
+
* For each file:
|
|
462
|
+
* 1. Bundle with esbuild (includes SDK shim + runtime).
|
|
463
|
+
* 2. Inject into the attached page via `Runtime.evaluate`.
|
|
464
|
+
* 3. Await the `RunReport` JSON response.
|
|
465
|
+
* 4. Accumulate results.
|
|
466
|
+
*
|
|
467
|
+
* Returns a `RelayRunReport` with per-file results and flattened totals.
|
|
468
|
+
*
|
|
469
|
+
* This function does NOT open or manage the relay connection — the caller
|
|
470
|
+
* is responsible for attaching and closing it.
|
|
471
|
+
*
|
|
472
|
+
* TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
|
|
473
|
+
* so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
|
|
474
|
+
*
|
|
475
|
+
* @param connection - Active CDP connection (relay or local kind).
|
|
476
|
+
* @param files - Absolute paths to test files, run in order.
|
|
477
|
+
* @param opts - Optional per-run overrides.
|
|
478
|
+
*/
|
|
479
|
+
async function runTestFilesOverRelay(connection, files, opts) {
|
|
480
|
+
const wallStart = Date.now();
|
|
481
|
+
const startedAt = new Date(wallStart).toISOString();
|
|
482
|
+
const fileResults = [];
|
|
483
|
+
for (const file of files) {
|
|
484
|
+
let fileEntry;
|
|
485
|
+
try {
|
|
486
|
+
const { code } = await bundleTestFile(file, opts?.bundleOptions);
|
|
487
|
+
const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
|
|
488
|
+
if (rpcResult.ok) fileEntry = {
|
|
489
|
+
file,
|
|
490
|
+
result: rpcResult.report
|
|
491
|
+
};
|
|
492
|
+
else fileEntry = {
|
|
493
|
+
file,
|
|
494
|
+
result: { error: rpcResult.error }
|
|
495
|
+
};
|
|
496
|
+
} catch (e) {
|
|
497
|
+
fileEntry = {
|
|
498
|
+
file,
|
|
499
|
+
result: { error: e instanceof Error ? e.message : String(e) }
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
fileResults.push(fileEntry);
|
|
503
|
+
}
|
|
504
|
+
const totals = fileResults.reduce((acc, { result }) => {
|
|
505
|
+
if ("error" in result) {
|
|
506
|
+
acc.failed += 1;
|
|
507
|
+
acc.total += 1;
|
|
508
|
+
} else {
|
|
509
|
+
acc.passed += result.passed;
|
|
510
|
+
acc.failed += result.failed;
|
|
511
|
+
acc.skipped += result.skipped;
|
|
512
|
+
acc.total += result.passed + result.failed + result.skipped;
|
|
513
|
+
}
|
|
514
|
+
return acc;
|
|
515
|
+
}, {
|
|
516
|
+
passed: 0,
|
|
517
|
+
failed: 0,
|
|
518
|
+
skipped: 0,
|
|
519
|
+
total: 0
|
|
520
|
+
});
|
|
521
|
+
return {
|
|
522
|
+
startedAt,
|
|
523
|
+
duration: Date.now() - wallStart,
|
|
524
|
+
files: fileResults,
|
|
525
|
+
totals
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/test-runner/cli.ts
|
|
530
|
+
/**
|
|
531
|
+
* `devtools-test` CLI.
|
|
532
|
+
*
|
|
533
|
+
* Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)
|
|
534
|
+
* and exposes `runWithConnection` — the pure run core that bundles, injects, and
|
|
535
|
+
* collects each file over a CDP connection. Today the run path that has a live
|
|
536
|
+
* connection is the `run_tests` MCP tool (it runs these files against the
|
|
537
|
+
* daemon's attached page); the CLI's own standalone relay attach (resolve CDP
|
|
538
|
+
* URL → attach → run → close) is not wired yet, so `main()` resolves the matched
|
|
539
|
+
* files and points the operator at the MCP tool.
|
|
540
|
+
*
|
|
541
|
+
* NOTE: no shebang in this source file — the tsdown entry's `banner` option
|
|
542
|
+
* injects `#!/usr/bin/env node` into the compiled output (same pattern as
|
|
543
|
+
* `src/mcp/cli.ts`).
|
|
544
|
+
*/
|
|
545
|
+
const USAGE = `
|
|
546
|
+
devtools-test — run mini-app tests on a real device WebView over the CDP relay
|
|
547
|
+
|
|
548
|
+
USAGE
|
|
549
|
+
devtools-test <glob> [<glob> ...] [options]
|
|
550
|
+
|
|
551
|
+
OPTIONS
|
|
552
|
+
--timeout <ms> Per-file evaluate timeout in ms (default: 30000)
|
|
553
|
+
--help, -h Show this help message
|
|
554
|
+
|
|
555
|
+
DESCRIPTION
|
|
556
|
+
Bundles each matched test file with esbuild (SDK imports redirected to
|
|
557
|
+
window.__sdk), injects the bundle into the attached WebView via
|
|
558
|
+
Runtime.evaluate, and returns a RunReport.
|
|
559
|
+
|
|
560
|
+
A live CDP relay connection must be active before running tests. Use the
|
|
561
|
+
\`run_tests\` MCP tool (via \`devtools-mcp\` / \`/ait debug\`) to run these files
|
|
562
|
+
against an attached page — the CLI's own standalone relay attach is not wired
|
|
563
|
+
yet (it currently resolves the matched files and defers to that tool).
|
|
564
|
+
|
|
565
|
+
EXAMPLE
|
|
566
|
+
devtools-test 'src/**/*.phone.test.ts' --timeout 60000
|
|
567
|
+
|
|
568
|
+
`.trimStart();
|
|
569
|
+
/**
|
|
570
|
+
* Runs `files` over `connection` and returns the aggregate report.
|
|
571
|
+
* This pure function is the testable core of the CLI (and is what the
|
|
572
|
+
* `run_tests` MCP tool calls against the daemon's attached connection); it is
|
|
573
|
+
* separate from `main()` so tests can call it without spawning a subprocess.
|
|
574
|
+
*
|
|
575
|
+
* A standalone CLI relay attach/detach lifecycle (connect via Chii relay URL,
|
|
576
|
+
* `enableDomains`, run, then close) is not wired into `main()` yet.
|
|
577
|
+
*/
|
|
578
|
+
async function runWithConnection(connection, files, opts) {
|
|
579
|
+
const report = await runTestFilesOverRelay(connection, files, opts);
|
|
580
|
+
if (opts?.printSummary) {
|
|
581
|
+
const { totals } = report;
|
|
582
|
+
process.stdout.write(`\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\n`);
|
|
583
|
+
}
|
|
584
|
+
return report;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* CLI entry point.
|
|
588
|
+
*
|
|
589
|
+
* Resolves the matched test files and prints a "relay attach required" notice:
|
|
590
|
+
* the CLI's own standalone relay attach (resolve CDP URL, attach, run, close) is
|
|
591
|
+
* not wired yet, so today these files run via the `run_tests` MCP tool against
|
|
592
|
+
* the daemon's attached page.
|
|
593
|
+
*/
|
|
594
|
+
async function main$1(argv = process.argv.slice(2)) {
|
|
595
|
+
let parsed;
|
|
596
|
+
try {
|
|
597
|
+
parsed = parseArgs({
|
|
598
|
+
args: argv,
|
|
599
|
+
options: {
|
|
600
|
+
help: {
|
|
601
|
+
type: "boolean",
|
|
602
|
+
short: "h"
|
|
603
|
+
},
|
|
604
|
+
timeout: { type: "string" }
|
|
605
|
+
},
|
|
606
|
+
allowPositionals: true
|
|
607
|
+
});
|
|
608
|
+
} catch (e) {
|
|
609
|
+
process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
610
|
+
process.exitCode = 1;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (parsed.values.help || argv.length === 0) {
|
|
614
|
+
process.stdout.write(USAGE);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const files = await discoverTestFiles(parsed.positionals, process.cwd());
|
|
618
|
+
if (files.length === 0) {
|
|
619
|
+
process.stderr.write(`devtools-test: no test files matched ${parsed.positionals.join(", ")}\n`);
|
|
620
|
+
process.exitCode = 1;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
process.stderr.write(`devtools-test: matched ${files.length} test file(s), but direct CLI relay attach is not yet wired.\n Use the devtools-mcp server (\`devtools-mcp\`) to start a debug session,\n then the \`run_tests\` MCP tool to run these files against the attached page.\n`);
|
|
624
|
+
process.exitCode = 1;
|
|
625
|
+
}
|
|
626
|
+
if (import.meta.url === new URL(process.argv[1], "file://").href) main$1().catch((e) => {
|
|
627
|
+
process.stderr.write(`devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
628
|
+
process.exitCode = 1;
|
|
629
|
+
});
|
|
630
|
+
//#endregion
|
|
122
631
|
//#region src/mcp/ait-chii-source.ts
|
|
123
632
|
function isObject$4(value) {
|
|
124
633
|
return typeof value === "object" && value !== null;
|
|
@@ -213,7 +722,11 @@ const ALLOWED_KEYS = new Set([
|
|
|
213
722
|
"errorKind",
|
|
214
723
|
"reason",
|
|
215
724
|
"prevTargetId",
|
|
216
|
-
"mode"
|
|
725
|
+
"mode",
|
|
726
|
+
"fileCount",
|
|
727
|
+
"passed",
|
|
728
|
+
"failed",
|
|
729
|
+
"skipped"
|
|
217
730
|
]);
|
|
218
731
|
/**
|
|
219
732
|
* Patterns that match secret values.
|
|
@@ -4033,6 +4546,34 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4033
4546
|
required: []
|
|
4034
4547
|
},
|
|
4035
4548
|
availableIn: "both"
|
|
4549
|
+
},
|
|
4550
|
+
{
|
|
4551
|
+
name: "run_tests",
|
|
4552
|
+
description: "Runs mini-app test files on the attached page over CDP (Runtime.evaluate). Each matched file is bundled with esbuild (SDK imports redirected to the live mock/SDK), injected into the attached WebView, and executed; returns per-file results plus flattened totals (passed/failed/skipped/total). Requires an attached page — call list_pages first to confirm one is attached. Files run SEQUENTIALLY (single-attach model: the relay/local target serves one page), and one run_tests call runs at a time (a concurrent call is rejected). Test verification (assert/snapshot) is delegated to the in-page Vitest runtime; this tool is the transport + report. The per-file results array is the progress record — on partial failure you see exactly which files passed/failed/timed-out. In a relay-live session this is a state-mutating injection and is blocked unless confirm=true (confirm is ignored in every non-live session: mock/local, relay-dev, relay-mobile). debug-mode only — dev-mode (--mode=dev) has no CDP. Tier C (both mock/local and relay). The devtools-test CLI shares this run core and file discovery, but its standalone relay attach is not wired yet — run via this tool for now.",
|
|
4553
|
+
inputSchema: {
|
|
4554
|
+
type: "object",
|
|
4555
|
+
properties: {
|
|
4556
|
+
files: {
|
|
4557
|
+
type: "array",
|
|
4558
|
+
items: { type: "string" },
|
|
4559
|
+
description: "Glob patterns or file paths to run (e.g. [\"src/**/*.phone.test.ts\"]). Resolved relative to projectRoot when given, else the daemon cwd. Required, non-empty."
|
|
4560
|
+
},
|
|
4561
|
+
projectRoot: {
|
|
4562
|
+
type: "string",
|
|
4563
|
+
description: "Absolute path to the mini-app project root used as the glob base. Pass this because the daemon's cwd is fixed at launch. Optional."
|
|
4564
|
+
},
|
|
4565
|
+
timeout_ms: {
|
|
4566
|
+
type: "number",
|
|
4567
|
+
description: "Per-file evaluate timeout in ms (default 30000, range 1000–600000). Out-of-range/invalid values fall back to the default."
|
|
4568
|
+
},
|
|
4569
|
+
confirm: {
|
|
4570
|
+
type: "boolean",
|
|
4571
|
+
description: "Required (true) to run in a relay-live session — test injection mutates page state. Ignored in every non-live session (mock/local, relay-dev, relay-mobile)."
|
|
4572
|
+
}
|
|
4573
|
+
},
|
|
4574
|
+
required: ["files"]
|
|
4575
|
+
},
|
|
4576
|
+
availableIn: "both"
|
|
4036
4577
|
}
|
|
4037
4578
|
];
|
|
4038
4579
|
const DEBUG_TOOL_NAMES = new Set(DEBUG_TOOL_DEFINITIONS.map((t) => t.name));
|
|
@@ -4855,7 +5396,7 @@ async function readMcpSdkVersion() {
|
|
|
4855
5396
|
* some test environments that skip the build step).
|
|
4856
5397
|
*/
|
|
4857
5398
|
function readDevtoolsVersion() {
|
|
4858
|
-
return "0.1.
|
|
5399
|
+
return "0.1.109";
|
|
4859
5400
|
}
|
|
4860
5401
|
/**
|
|
4861
5402
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -5103,9 +5644,11 @@ async function renderQr(text) {
|
|
|
5103
5644
|
return `${lines.join("\n")}\n`;
|
|
5104
5645
|
}
|
|
5105
5646
|
/**
|
|
5106
|
-
* Renders the attach banner (relay URL +
|
|
5647
|
+
* Renders the attach banner (relay URL + unicode half-block QR) as a string.
|
|
5107
5648
|
*
|
|
5108
|
-
* The QR
|
|
5649
|
+
* The QR is produced by `renderQr` (a half-block matrix, not the
|
|
5650
|
+
* `qrcode-terminal` ASCII art used by the unplugin banner) and encodes the
|
|
5651
|
+
* base `wssUrl` only. When `totpEnabled` is true, a note
|
|
5109
5652
|
* is added that attach URLs generated by `build_attach_url` will include a
|
|
5110
5653
|
* live TOTP code (`at=`) appended at call time.
|
|
5111
5654
|
*
|
|
@@ -5385,6 +5928,16 @@ function isRelayMode(mode) {
|
|
|
5385
5928
|
return mode === "relay-sandbox" || mode === "relay-staging" || mode === "relay-live";
|
|
5386
5929
|
}
|
|
5387
5930
|
/**
|
|
5931
|
+
* Single-attach guard for `run_tests` (#646). Two concurrent runs injecting
|
|
5932
|
+
* into the same single-attach page would interleave `Runtime.evaluate` and
|
|
5933
|
+
* corrupt each other's `globalThis.__testBundle`. The model is "reject the
|
|
5934
|
+
* second", not "queue" — a module-level flag is process-wide, which matches the
|
|
5935
|
+
* single physical attached page (only one target is live at a time). The
|
|
5936
|
+
* entry-time `conn` snapshot ensures a run finishes on the connection it started
|
|
5937
|
+
* on even if `router.active` flips mid-run.
|
|
5938
|
+
*/
|
|
5939
|
+
let runTestsInFlight = false;
|
|
5940
|
+
/**
|
|
5388
5941
|
* Waits for the first target matching `filterFn` to attach, using the
|
|
5389
5942
|
* event-driven `waitForFirstTarget()` when the connection supports it
|
|
5390
5943
|
* (interface-optional member, present on `ChiiCdpConnection`), or falling
|
|
@@ -5395,6 +5948,12 @@ function isRelayMode(mode) {
|
|
|
5395
5948
|
* to resolve before the relay had observed the first inbound CDP message from
|
|
5396
5949
|
* the phone.
|
|
5397
5950
|
*
|
|
5951
|
+
* Timeout note: callers (e.g. the `build_attach_url` path) always pass an
|
|
5952
|
+
* explicit `timeoutMs`, sourced from the factory's `waitForAttachTimeoutMs`
|
|
5953
|
+
* (default 60 000). That value is forwarded to `waitForFirstTarget`, so it
|
|
5954
|
+
* overrides that method's own 90 000 signature default — the effective
|
|
5955
|
+
* wait on the tool path is 60 s, not 90 s.
|
|
5956
|
+
*
|
|
5398
5957
|
* @param connection - The CDP connection (production or fake).
|
|
5399
5958
|
* @param filterFn - Resolves when this predicate is satisfied.
|
|
5400
5959
|
* @param timeoutMs - Maximum wait time in ms.
|
|
@@ -5447,7 +6006,7 @@ function createDebugServer(deps) {
|
|
|
5447
6006
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
5448
6007
|
const server = new Server({
|
|
5449
6008
|
name: "ait-debug",
|
|
5450
|
-
version: "0.1.
|
|
6009
|
+
version: "0.1.109"
|
|
5451
6010
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
5452
6011
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
5453
6012
|
const conn = router.active;
|
|
@@ -5950,6 +6509,35 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
5950
6509
|
if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk", conn.kind === "local");
|
|
5951
6510
|
return envelopeResult$1(sdkResult, name, env, conn.listTargets().length > 0);
|
|
5952
6511
|
}
|
|
6512
|
+
case "run_tests": {
|
|
6513
|
+
const rawFiles = request.params.arguments?.files;
|
|
6514
|
+
if (!Array.isArray(rawFiles) || rawFiles.length === 0) return mcpError("run_tests: files 인자가 비어 있습니다. 실행할 테스트 파일 glob을 배열로 전달하세요.");
|
|
6515
|
+
const patterns = rawFiles.filter((p) => typeof p === "string" && p !== "");
|
|
6516
|
+
if (patterns.length === 0) return mcpError("run_tests: files 인자에 유효한 문자열 glob이 없습니다.");
|
|
6517
|
+
const rawRoot = request.params.arguments?.projectRoot;
|
|
6518
|
+
const projectRoot = typeof rawRoot === "string" ? rawRoot : process.cwd();
|
|
6519
|
+
const rawTimeout = request.params.arguments?.timeout_ms;
|
|
6520
|
+
const timeoutMs = typeof rawTimeout === "number" && rawTimeout >= 1e3 && rawTimeout <= 6e5 ? rawTimeout : void 0;
|
|
6521
|
+
if (conn.kind === "relay" && getLiveIntent() && request.params.arguments?.confirm !== true) return liveGuardError("run_tests");
|
|
6522
|
+
if (runTestsInFlight) return mcpError("run_tests: 이미 다른 테스트 실행이 진행 중입니다 (single-attach 모델: 페이지는 한 번에 하나의 실행만 처리). 완료 후 다시 시도하세요.");
|
|
6523
|
+
runTestsInFlight = true;
|
|
6524
|
+
try {
|
|
6525
|
+
const files = await discoverTestFiles(patterns, projectRoot);
|
|
6526
|
+
if (files.length === 0) return mcpError(`run_tests: 매칭된 테스트 파일이 없습니다 (patterns: ${patterns.join(", ")}).`);
|
|
6527
|
+
if (conn.listTargets().length === 0) return pageMissingError("run_tests");
|
|
6528
|
+
logInfo("run_tests.start", { fileCount: files.length });
|
|
6529
|
+
const report = await runWithConnection(conn, files, { timeoutMs });
|
|
6530
|
+
logInfo("run_tests.done", {
|
|
6531
|
+
passed: report.totals.passed,
|
|
6532
|
+
failed: report.totals.failed,
|
|
6533
|
+
skipped: report.totals.skipped
|
|
6534
|
+
});
|
|
6535
|
+
const runAttached = conn.listTargets().length > 0;
|
|
6536
|
+
return envelopeResult$1(toRunTestsResult(report), name, env, runAttached);
|
|
6537
|
+
} finally {
|
|
6538
|
+
runTestsInFlight = false;
|
|
6539
|
+
}
|
|
6540
|
+
}
|
|
5953
6541
|
default: return unknownTool(name);
|
|
5954
6542
|
}
|
|
5955
6543
|
} catch (err) {
|
|
@@ -6035,6 +6623,31 @@ function envelopeResult$1(value, tool, env, attached) {
|
|
|
6035
6623
|
text: JSON.stringify(wrapped, null, 2)
|
|
6036
6624
|
}] };
|
|
6037
6625
|
}
|
|
6626
|
+
/**
|
|
6627
|
+
* Maps a {@link RelayRunReport} to a flat, agent-friendly object for the
|
|
6628
|
+
* `run_tests` tool result. SECRET-HANDLING: a RelayRunReport carries only
|
|
6629
|
+
* startedAt/duration/totals and per-file `{file, result}` — file paths are
|
|
6630
|
+
* surfaced (allowed), relay wss/TOTP URLs never appear in it. No stripping
|
|
6631
|
+
* needed; this only reshapes for readability.
|
|
6632
|
+
*/
|
|
6633
|
+
function toRunTestsResult(report) {
|
|
6634
|
+
return {
|
|
6635
|
+
startedAt: report.startedAt,
|
|
6636
|
+
duration: report.duration,
|
|
6637
|
+
totals: report.totals,
|
|
6638
|
+
files: report.files.map((f) => "error" in f.result ? {
|
|
6639
|
+
file: f.file,
|
|
6640
|
+
error: f.result.error
|
|
6641
|
+
} : {
|
|
6642
|
+
file: f.file,
|
|
6643
|
+
duration: f.result.duration,
|
|
6644
|
+
passed: f.result.passed,
|
|
6645
|
+
failed: f.result.failed,
|
|
6646
|
+
skipped: f.result.skipped,
|
|
6647
|
+
tests: f.result.tests
|
|
6648
|
+
})
|
|
6649
|
+
};
|
|
6650
|
+
}
|
|
6038
6651
|
function unknownTool(name) {
|
|
6039
6652
|
return mcpError(`알 수 없는 tool: ${name}`);
|
|
6040
6653
|
}
|
|
@@ -7456,6 +8069,34 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
7456
8069
|
required: []
|
|
7457
8070
|
},
|
|
7458
8071
|
availableIn: "both"
|
|
8072
|
+
},
|
|
8073
|
+
{
|
|
8074
|
+
name: "run_tests",
|
|
8075
|
+
description: "Runs mini-app test files on the attached page over CDP (Runtime.evaluate). NOT available in dev-mode (no CDP connection). Switch to `--mode=local` or `--mode=debug`.",
|
|
8076
|
+
inputSchema: {
|
|
8077
|
+
type: "object",
|
|
8078
|
+
properties: {
|
|
8079
|
+
files: {
|
|
8080
|
+
type: "array",
|
|
8081
|
+
items: { type: "string" },
|
|
8082
|
+
description: "Glob patterns or file paths to run."
|
|
8083
|
+
},
|
|
8084
|
+
projectRoot: {
|
|
8085
|
+
type: "string",
|
|
8086
|
+
description: "Glob base directory."
|
|
8087
|
+
},
|
|
8088
|
+
timeout_ms: {
|
|
8089
|
+
type: "number",
|
|
8090
|
+
description: "Per-file evaluate timeout in ms."
|
|
8091
|
+
},
|
|
8092
|
+
confirm: {
|
|
8093
|
+
type: "boolean",
|
|
8094
|
+
description: "Required in relay-live sessions."
|
|
8095
|
+
}
|
|
8096
|
+
},
|
|
8097
|
+
required: ["files"]
|
|
8098
|
+
},
|
|
8099
|
+
availableIn: "both"
|
|
7459
8100
|
}
|
|
7460
8101
|
];
|
|
7461
8102
|
/** All tool names served in dev-mode (including tier-filter stubs). */
|
|
@@ -7468,7 +8109,8 @@ const CDP_ONLY_TOOL_NAMES = new Set([
|
|
|
7468
8109
|
"take_snapshot",
|
|
7469
8110
|
"list_console_messages",
|
|
7470
8111
|
"list_network_requests",
|
|
7471
|
-
"list_exceptions"
|
|
8112
|
+
"list_exceptions",
|
|
8113
|
+
"run_tests"
|
|
7472
8114
|
]);
|
|
7473
8115
|
/**
|
|
7474
8116
|
* Tier B tools — relay-only per RFC #277.
|
|
@@ -7585,7 +8227,7 @@ function createDevServer(deps = {}) {
|
|
|
7585
8227
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
7586
8228
|
const server = new Server({
|
|
7587
8229
|
name: "ait-devtools",
|
|
7588
|
-
version: "0.1.
|
|
8230
|
+
version: "0.1.109"
|
|
7589
8231
|
}, { capabilities: { tools: {} } });
|
|
7590
8232
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
7591
8233
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|