@john523100/vela-mcp 0.6.0
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/README.md +91 -0
- package/dist/adapter.d.ts +31 -0
- package/dist/adapter.js +32 -0
- package/dist/adapter.js.map +1 -0
- package/dist/capture.d.ts +41 -0
- package/dist/capture.js +96 -0
- package/dist/capture.js.map +1 -0
- package/dist/cdp.d.ts +18 -0
- package/dist/cdp.js +60 -0
- package/dist/cdp.js.map +1 -0
- package/dist/debug.d.ts +40 -0
- package/dist/debug.js +133 -0
- package/dist/debug.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +146 -0
- package/dist/index.js.map +1 -0
- package/dist/launcher.d.ts +17 -0
- package/dist/launcher.js +128 -0
- package/dist/launcher.js.map +1 -0
- package/dist/ports.d.ts +2 -0
- package/dist/ports.js +22 -0
- package/dist/ports.js.map +1 -0
- package/dist/probe.d.ts +15 -0
- package/dist/probe.js +40 -0
- package/dist/probe.js.map +1 -0
- package/dist/session.d.ts +26 -0
- package/dist/session.js +31 -0
- package/dist/session.js.map +1 -0
- package/dist/spec.d.ts +39 -0
- package/dist/spec.js +116 -0
- package/dist/spec.js.map +1 -0
- package/dist/targets.d.ts +24 -0
- package/dist/targets.js +29 -0
- package/dist/targets.js.map +1 -0
- package/dist/tools/capture.d.ts +21 -0
- package/dist/tools/capture.js +30 -0
- package/dist/tools/capture.js.map +1 -0
- package/dist/tools/debug.d.ts +50 -0
- package/dist/tools/debug.js +65 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/eval.d.ts +9 -0
- package/dist/tools/eval.js +7 -0
- package/dist/tools/eval.js.map +1 -0
- package/dist/tools/ipc.d.ts +14 -0
- package/dist/tools/ipc.js +19 -0
- package/dist/tools/ipc.js.map +1 -0
- package/dist/tools/launch.d.ts +16 -0
- package/dist/tools/launch.js +20 -0
- package/dist/tools/launch.js.map +1 -0
- package/dist/tools/main.d.ts +7 -0
- package/dist/tools/main.js +12 -0
- package/dist/tools/main.js.map +1 -0
- package/dist/tools/screenshot.d.ts +7 -0
- package/dist/tools/screenshot.js +16 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/spec.d.ts +18 -0
- package/dist/tools/spec.js +14 -0
- package/dist/tools/spec.js.map +1 -0
- package/dist/tools/targets.d.ts +13 -0
- package/dist/tools/targets.js +15 -0
- package/dist/tools/targets.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { SessionRegistry } from "./session.js";
|
|
6
|
+
import { launchTool } from "./tools/launch.js";
|
|
7
|
+
import { evalTool } from "./tools/eval.js";
|
|
8
|
+
import { screenshotTool } from "./tools/screenshot.js";
|
|
9
|
+
import { targetsTool } from "./tools/targets.js";
|
|
10
|
+
import { ipcListTool, ipcInvokeTool } from "./tools/ipc.js";
|
|
11
|
+
import { mainEvalTool } from "./tools/main.js";
|
|
12
|
+
import { DebugRegistry } from "./debug.js";
|
|
13
|
+
import { debugAttachTool, debugSetBreakpointTool, debugWaitPausedTool, debugEvalFrameTool, debugStepTool, debugResumeTool, debugDetachTool } from "./tools/debug.js";
|
|
14
|
+
import { specSaveTool, specRunTool, specListTool } from "./tools/spec.js";
|
|
15
|
+
import { CaptureRegistry } from "./capture.js";
|
|
16
|
+
import { captureStartTool, captureReadTool, captureStopTool } from "./tools/capture.js";
|
|
17
|
+
const registry = new SessionRegistry();
|
|
18
|
+
const debugReg = new DebugRegistry();
|
|
19
|
+
const capReg = new CaptureRegistry();
|
|
20
|
+
const server = new McpServer({ name: "vela", version: "0.6.0" });
|
|
21
|
+
server.tool("vela_launch", "Start (mode 'launch', default) an Electron app by absolute projectPath, OR attach (mode 'attach') to an already-running CDP endpoint by port (any Chromium/Electron/web app started with --remote-debugging-port). Returns a sessionId and the CDP targets.", {
|
|
22
|
+
projectPath: z.string().optional().describe("Absolute path to the Electron project (required for mode 'launch')"),
|
|
23
|
+
mode: z.enum(["launch", "attach"]).optional().describe("'launch' (default) starts the app; 'attach' connects to a running --remote-debugging-port"),
|
|
24
|
+
port: z.number().optional().describe("CDP remote-debugging port (required for mode 'attach')")
|
|
25
|
+
}, async ({ projectPath, mode, port }) => {
|
|
26
|
+
const res = await launchTool(registry, { projectPath, mode, port });
|
|
27
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
|
|
28
|
+
});
|
|
29
|
+
server.tool("vela_targets", "List the session's CDP targets with stable logical names (renderer, webview#N, page#N). Use a returned name as the `target` for vela_eval / vela_screenshot.", { sessionId: z.string() }, async ({ sessionId }) => {
|
|
30
|
+
const res = await targetsTool(registry, { sessionId });
|
|
31
|
+
return { content: [{ type: "text", text: JSON.stringify(res.targets, null, 2) }] };
|
|
32
|
+
});
|
|
33
|
+
server.tool("vela_eval", "Evaluate a JavaScript expression in the chosen target's main world for a session (default: the renderer). Returns the JSON-serializable result.", {
|
|
34
|
+
sessionId: z.string(),
|
|
35
|
+
expression: z.string(),
|
|
36
|
+
awaitPromise: z.boolean().optional(),
|
|
37
|
+
target: z.string().optional().describe("Logical target name (e.g. renderer, webview#0, page#0); defaults to renderer")
|
|
38
|
+
}, async ({ sessionId, expression, awaitPromise, target }) => {
|
|
39
|
+
const res = await evalTool(registry, { sessionId, expression, awaitPromise, target });
|
|
40
|
+
return { content: [{ type: "text", text: JSON.stringify(res.value) }] };
|
|
41
|
+
});
|
|
42
|
+
server.tool("vela_screenshot", "Capture a PNG screenshot of the chosen target for a session (default: the renderer). Returns the file path on disk.", {
|
|
43
|
+
sessionId: z.string(),
|
|
44
|
+
target: z.string().optional().describe("Logical target name (e.g. renderer, webview#0, page#0); defaults to renderer")
|
|
45
|
+
}, async ({ sessionId, target }) => {
|
|
46
|
+
const res = await screenshotTool(registry, { sessionId, target });
|
|
47
|
+
return { content: [{ type: "text", text: res.path }] };
|
|
48
|
+
});
|
|
49
|
+
server.tool("vela_ipc_list", "List the app's registered IPC channels (ipcMain.handle/on) with their kind. Requires the vela-agent dev probe in the target app.", { sessionId: z.string() }, async ({ sessionId }) => {
|
|
50
|
+
const res = await ipcListTool(registry, { sessionId });
|
|
51
|
+
return { content: [{ type: "text", text: JSON.stringify(res.channels, null, 2) }] };
|
|
52
|
+
});
|
|
53
|
+
server.tool("vela_ipc_invoke", "Invoke an ipcMain.handle channel by name with JSON args and return its result. Requires the vela-agent dev probe.", {
|
|
54
|
+
sessionId: z.string(),
|
|
55
|
+
channel: z.string(),
|
|
56
|
+
args: z.array(z.any()).optional()
|
|
57
|
+
}, async ({ sessionId, channel, args }) => {
|
|
58
|
+
const res = await ipcInvokeTool(registry, { sessionId, channel, args });
|
|
59
|
+
return { content: [{ type: "text", text: JSON.stringify(res.result) }] };
|
|
60
|
+
});
|
|
61
|
+
server.tool("vela_main_eval", "Evaluate a JavaScript expression in the app's MAIN process scope and return the result. Requires the vela-agent dev probe.", { sessionId: z.string(), code: z.string() }, async ({ sessionId, code }) => {
|
|
62
|
+
const res = await mainEvalTool(registry, { sessionId, code });
|
|
63
|
+
return { content: [{ type: "text", text: JSON.stringify(res.value) }] };
|
|
64
|
+
});
|
|
65
|
+
server.tool("vela_debug_attach", "Attach a debugger to a target ('renderer', 'webview#N', 'page#N', or 'main' for the main process). Required before setting breakpoints.", { sessionId: z.string(), target: z.string() }, async ({ sessionId, target }) => {
|
|
66
|
+
const res = await debugAttachTool(registry, debugReg, { sessionId, target });
|
|
67
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
68
|
+
});
|
|
69
|
+
server.tool("vela_debug_set_breakpoint", "Set a breakpoint by a URL regex (e.g. 'app\\\\.js') and a 1-based line number on an attached target.", { sessionId: z.string(), target: z.string(), urlRegex: z.string(), line: z.number() }, async ({ sessionId, target, urlRegex, line }) => {
|
|
70
|
+
const res = await debugSetBreakpointTool(debugReg, { sessionId, target, urlRegex, line });
|
|
71
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
72
|
+
});
|
|
73
|
+
server.tool("vela_debug_wait_paused", "Block until the target hits a breakpoint (or is already paused); returns the call stack. Trigger the code path in a separate, non-awaited call first.", { sessionId: z.string(), target: z.string(), timeoutMs: z.number().optional() }, async ({ sessionId, target, timeoutMs }) => {
|
|
74
|
+
const res = await debugWaitPausedTool(debugReg, { sessionId, target, timeoutMs });
|
|
75
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
|
|
76
|
+
});
|
|
77
|
+
server.tool("vela_debug_eval_frame", "Evaluate an expression in a paused call frame (frameIndex 0 = top) to read locals/scope.", { sessionId: z.string(), target: z.string(), frameIndex: z.number().optional(), expression: z.string() }, async ({ sessionId, target, frameIndex, expression }) => {
|
|
78
|
+
const res = await debugEvalFrameTool(debugReg, { sessionId, target, frameIndex, expression });
|
|
79
|
+
return { content: [{ type: "text", text: JSON.stringify(res.value) }] };
|
|
80
|
+
});
|
|
81
|
+
server.tool("vela_debug_step", "Step the paused target: kind 'over' (default), 'into', or 'out'. Then call vela_debug_wait_paused again.", { sessionId: z.string(), target: z.string(), kind: z.enum(["over", "into", "out"]).optional() }, async ({ sessionId, target, kind }) => {
|
|
82
|
+
const res = await debugStepTool(debugReg, { sessionId, target, kind });
|
|
83
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
84
|
+
});
|
|
85
|
+
server.tool("vela_debug_resume", "Resume execution of a paused target.", { sessionId: z.string(), target: z.string() }, async ({ sessionId, target }) => {
|
|
86
|
+
const res = await debugResumeTool(debugReg, { sessionId, target });
|
|
87
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
88
|
+
});
|
|
89
|
+
server.tool("vela_debug_detach", "Detach the debugger from a target (resumes if paused, removes breakpoints and the debug connection).", { sessionId: z.string(), target: z.string() }, async ({ sessionId, target }) => {
|
|
90
|
+
const res = await debugDetachTool(debugReg, { sessionId, target });
|
|
91
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
92
|
+
});
|
|
93
|
+
server.tool("vela_spec_save", "Save a verified interaction as a re-runnable spec: a name, the app projectPath, and ordered steps (tool=eval/ipc_invoke/ipc_list/targets/main_eval/screenshot, args, optional expect={equals|truthy|contains|matches}).", {
|
|
94
|
+
name: z.string(),
|
|
95
|
+
projectPath: z.string(),
|
|
96
|
+
steps: z.array(z.object({
|
|
97
|
+
tool: z.enum(["eval", "ipc_invoke", "ipc_list", "targets", "main_eval", "screenshot"]),
|
|
98
|
+
args: z.record(z.any()).optional(),
|
|
99
|
+
expect: z.object({
|
|
100
|
+
equals: z.any().optional(),
|
|
101
|
+
truthy: z.boolean().optional(),
|
|
102
|
+
contains: z.string().optional(),
|
|
103
|
+
matches: z.string().optional()
|
|
104
|
+
}).optional(),
|
|
105
|
+
label: z.string().optional()
|
|
106
|
+
})),
|
|
107
|
+
specsDir: z.string().optional()
|
|
108
|
+
}, async (args) => {
|
|
109
|
+
const res = await specSaveTool(args);
|
|
110
|
+
return { content: [{ type: "text", text: res.saved }] };
|
|
111
|
+
});
|
|
112
|
+
server.tool("vela_spec_run", "Re-run a saved spec by name: launches its app, runs each step, applies assertions, and returns a pass/fail report.", { name: z.string(), specsDir: z.string().optional() }, async ({ name, specsDir }) => {
|
|
113
|
+
const report = await specRunTool({ name, specsDir });
|
|
114
|
+
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
115
|
+
});
|
|
116
|
+
server.tool("vela_spec_list", "List saved spec names.", { specsDir: z.string().optional() }, async ({ specsDir }) => {
|
|
117
|
+
const res = await specListTool({ specsDir });
|
|
118
|
+
return { content: [{ type: "text", text: JSON.stringify(res.specs) }] };
|
|
119
|
+
});
|
|
120
|
+
server.tool("vela_capture_start", "Start capturing console output and/or network requests for a target (default 'renderer'). Pass console:true and/or network:true (console defaults on if neither given).", { sessionId: z.string(), target: z.string().optional(), console: z.boolean().optional(), network: z.boolean().optional() }, async ({ sessionId, target, console, network }) => {
|
|
121
|
+
const res = await captureStartTool(registry, capReg, { sessionId, target, console, network });
|
|
122
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
123
|
+
});
|
|
124
|
+
server.tool("vela_capture_read", "Read the console lines and network requests (method/url/status/mimeType) captured so far for a target.", { sessionId: z.string(), target: z.string().optional() }, async ({ sessionId, target }) => {
|
|
125
|
+
const res = await captureReadTool(capReg, { sessionId, target });
|
|
126
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
|
|
127
|
+
});
|
|
128
|
+
server.tool("vela_capture_stop", "Stop capturing for a target and close its capture connection.", { sessionId: z.string(), target: z.string().optional() }, async ({ sessionId, target }) => {
|
|
129
|
+
const res = await captureStopTool(capReg, { sessionId, target });
|
|
130
|
+
return { content: [{ type: "text", text: JSON.stringify(res) }] };
|
|
131
|
+
});
|
|
132
|
+
async function main() {
|
|
133
|
+
const transport = new StdioServerTransport();
|
|
134
|
+
await server.connect(transport);
|
|
135
|
+
const shutdown = async () => {
|
|
136
|
+
await registry.disposeAll();
|
|
137
|
+
process.exit(0);
|
|
138
|
+
};
|
|
139
|
+
process.on("SIGINT", shutdown);
|
|
140
|
+
process.on("SIGTERM", shutdown);
|
|
141
|
+
}
|
|
142
|
+
main().catch((err) => {
|
|
143
|
+
console.error(err);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
export interface LaunchedApp {
|
|
3
|
+
port: number;
|
|
4
|
+
inspectPort: number;
|
|
5
|
+
child: ChildProcess;
|
|
6
|
+
probe?: {
|
|
7
|
+
port: number;
|
|
8
|
+
token: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Kill a child process and resolve once it has actually exited.
|
|
13
|
+
* Falls back to SIGKILL if the process does not exit within `forceAfterMs`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function killAndWait(child: ChildProcess, forceAfterMs?: number): Promise<void>;
|
|
16
|
+
/** Launch an Electron app in dev with a CDP debug port and wait until its renderer is reachable. */
|
|
17
|
+
export declare function launchElectron(projectPath: string): Promise<LaunchedApp>;
|
package/dist/launcher.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { getFreePort } from "./ports.js";
|
|
6
|
+
import { CdpClient } from "./cdp.js";
|
|
7
|
+
import { pickPageTarget } from "./targets.js";
|
|
8
|
+
import { ProbeClient } from "./probe.js";
|
|
9
|
+
/** Resolve the Electron executable path from the target project's own dependency tree. */
|
|
10
|
+
function resolveElectronBinary(projectPath) {
|
|
11
|
+
const requireFromProject = createRequire(path.join(projectPath, "package.json"));
|
|
12
|
+
// The `electron` package's main export is the path to the binary.
|
|
13
|
+
const binary = requireFromProject("electron");
|
|
14
|
+
if (typeof binary !== "string") {
|
|
15
|
+
throw new Error(`Could not resolve electron binary from ${projectPath}`);
|
|
16
|
+
}
|
|
17
|
+
return binary;
|
|
18
|
+
}
|
|
19
|
+
/** Best-effort wait until the renderer's document has parsed (inline scripts have run). */
|
|
20
|
+
async function waitForDocumentReady(port, timeoutMs = 10_000) {
|
|
21
|
+
const cdp = new CdpClient(port);
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
while (Date.now() - start < timeoutMs) {
|
|
24
|
+
try {
|
|
25
|
+
const { value } = await cdp.evaluate("document.readyState", false);
|
|
26
|
+
// 'interactive' = DOMContentLoaded (all parser-inserted inline scripts have executed); 'complete' = fully loaded.
|
|
27
|
+
if (value === "interactive" || value === "complete")
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// renderer not evaluable yet
|
|
32
|
+
}
|
|
33
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
34
|
+
}
|
|
35
|
+
// best-effort: don't fail the launch if readiness can't be confirmed in time
|
|
36
|
+
}
|
|
37
|
+
async function waitForPageTarget(port, timeoutMs = 20_000) {
|
|
38
|
+
const cdp = new CdpClient(port);
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
// Poll until a renderer page target is reachable.
|
|
41
|
+
// eslint-disable-next-line no-constant-condition
|
|
42
|
+
while (true) {
|
|
43
|
+
try {
|
|
44
|
+
const targets = await cdp.listTargets();
|
|
45
|
+
if (pickPageTarget(targets))
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// debug endpoint not up yet
|
|
50
|
+
}
|
|
51
|
+
if (Date.now() - start > timeoutMs) {
|
|
52
|
+
throw new Error(`Timed out waiting for renderer page target on port ${port}`);
|
|
53
|
+
}
|
|
54
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Kill a child process and resolve once it has actually exited.
|
|
59
|
+
* Falls back to SIGKILL if the process does not exit within `forceAfterMs`.
|
|
60
|
+
*/
|
|
61
|
+
export function killAndWait(child, forceAfterMs = 5_000) {
|
|
62
|
+
// Already exited? Nothing to wait for.
|
|
63
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const force = setTimeout(() => child.kill("SIGKILL"), forceAfterMs);
|
|
67
|
+
child.once("exit", () => {
|
|
68
|
+
clearTimeout(force);
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
child.kill();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Poll the probe's /health up to `timeoutMs`; returns true if it ever answers.
|
|
76
|
+
* 1s is enough: an integrated probe calls attach() at main-process load, well before
|
|
77
|
+
* the renderer becomes ready (which is what we already awaited), so it answers immediately.
|
|
78
|
+
* Projects without the probe simply pay this short bounded wait once.
|
|
79
|
+
*/
|
|
80
|
+
async function detectProbe(port, token, timeoutMs = 1_000) {
|
|
81
|
+
const client = new ProbeClient(port, token);
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
while (Date.now() - start < timeoutMs) {
|
|
84
|
+
if (await client.health())
|
|
85
|
+
return true;
|
|
86
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
/** Launch an Electron app in dev with a CDP debug port and wait until its renderer is reachable. */
|
|
91
|
+
export async function launchElectron(projectPath) {
|
|
92
|
+
const port = await getFreePort();
|
|
93
|
+
const probePort = await getFreePort();
|
|
94
|
+
const inspectPort = await getFreePort();
|
|
95
|
+
const probeToken = randomUUID();
|
|
96
|
+
const electron = resolveElectronBinary(projectPath);
|
|
97
|
+
const child = spawn(electron, [".", `--remote-debugging-port=${port}`, `--inspect=${inspectPort}`], {
|
|
98
|
+
cwd: projectPath,
|
|
99
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
100
|
+
env: {
|
|
101
|
+
...process.env,
|
|
102
|
+
NODE_ENV: "development",
|
|
103
|
+
VELA_PROBE_PORT: String(probePort),
|
|
104
|
+
VELA_PROBE_TOKEN: probeToken
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// Permanent listener so a later 'error' event never throws as an unhandled emitter error.
|
|
108
|
+
child.on("error", (err) => {
|
|
109
|
+
console.error(`[vela] electron process error: ${err.message}`);
|
|
110
|
+
});
|
|
111
|
+
const spawnFailed = new Promise((_, reject) => {
|
|
112
|
+
child.once("error", reject);
|
|
113
|
+
});
|
|
114
|
+
spawnFailed.catch(() => { });
|
|
115
|
+
try {
|
|
116
|
+
await Promise.race([waitForPageTarget(port), spawnFailed]);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
await killAndWait(child);
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
// Wait until the renderer has parsed so inline-script globals are present before any eval.
|
|
123
|
+
await waitForDocumentReady(port);
|
|
124
|
+
// The probe is optional: projects that haven't integrated vela-agent simply won't answer.
|
|
125
|
+
const hasProbe = await detectProbe(probePort, probeToken);
|
|
126
|
+
return { port, inspectPort, child, probe: hasProbe ? { port: probePort, token: probeToken } : undefined };
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=launcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launcher.js","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"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"}
|
package/dist/ports.d.ts
ADDED
package/dist/ports.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
/** Ask the OS for an unused TCP port bound to loopback. */
|
|
3
|
+
export function getFreePort() {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const srv = net.createServer();
|
|
6
|
+
srv.unref();
|
|
7
|
+
srv.on("error", reject);
|
|
8
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
9
|
+
const addr = srv.address();
|
|
10
|
+
if (addr && typeof addr === "object") {
|
|
11
|
+
const port = addr.port;
|
|
12
|
+
// NOTE: the port is released here and handed to Electron afterwards; there is a
|
|
13
|
+
// small TOCTOU window where another process could claim it. Accepted for local use.
|
|
14
|
+
srv.close(() => resolve(port));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
srv.close(() => reject(new Error("Failed to acquire a free port")));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,2DAA2D;AAC3D,MAAM,UAAU,WAAW;IACzB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC;QAC/B,GAAG,CAAC,KAAK,EAAE,CAAC;QACZ,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACxB,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;YAC9B,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;YAC3B,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;gBACvB,gFAAgF;gBAChF,oFAAoF;gBACpF,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YACjC,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC,CAAC,CAAC;YACtE,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/probe.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface IpcChannelInfo {
|
|
2
|
+
channel: string;
|
|
3
|
+
kind: "handle" | "on";
|
|
4
|
+
}
|
|
5
|
+
/** Client for a running vela-agent dev probe (localhost HTTP JSON-RPC). */
|
|
6
|
+
export declare class ProbeClient {
|
|
7
|
+
private readonly port;
|
|
8
|
+
private readonly token;
|
|
9
|
+
constructor(port: number, token: string);
|
|
10
|
+
private rpc;
|
|
11
|
+
health(): Promise<boolean>;
|
|
12
|
+
ipcList(): Promise<IpcChannelInfo[]>;
|
|
13
|
+
ipcInvoke(channel: string, args: unknown[]): Promise<unknown>;
|
|
14
|
+
mainEval(code: string): Promise<unknown>;
|
|
15
|
+
}
|
package/dist/probe.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Client for a running vela-agent dev probe (localhost HTTP JSON-RPC). */
|
|
2
|
+
export class ProbeClient {
|
|
3
|
+
port;
|
|
4
|
+
token;
|
|
5
|
+
constructor(port, token) {
|
|
6
|
+
this.port = port;
|
|
7
|
+
this.token = token;
|
|
8
|
+
}
|
|
9
|
+
async rpc(method, params) {
|
|
10
|
+
const res = await fetch(`http://127.0.0.1:${this.port}/rpc`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "content-type": "application/json", "x-vela-token": this.token },
|
|
13
|
+
body: JSON.stringify({ method, params }),
|
|
14
|
+
signal: AbortSignal.timeout(10_000)
|
|
15
|
+
});
|
|
16
|
+
const j = (await res.json());
|
|
17
|
+
if (!j.ok)
|
|
18
|
+
throw new Error(j.error ?? "probe RPC error");
|
|
19
|
+
return j.result;
|
|
20
|
+
}
|
|
21
|
+
async health() {
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(`http://127.0.0.1:${this.port}/health`);
|
|
24
|
+
return res.ok;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
ipcList() {
|
|
31
|
+
return this.rpc("ipc.list");
|
|
32
|
+
}
|
|
33
|
+
ipcInvoke(channel, args) {
|
|
34
|
+
return this.rpc("ipc.invoke", { channel, args });
|
|
35
|
+
}
|
|
36
|
+
mainEval(code) {
|
|
37
|
+
return this.rpc("main.eval", { code });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.js","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAKA,2EAA2E;AAC3E,MAAM,OAAO,WAAW;IACO;IAA+B;IAA5D,YAA6B,IAAY,EAAmB,KAAa;QAA5C,SAAI,GAAJ,IAAI,CAAQ;QAAmB,UAAK,GAAL,KAAK,CAAQ;IAAG,CAAC;IAErE,KAAK,CAAC,GAAG,CAAI,MAAc,EAAE,MAAgB;QACnD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,oBAAoB,IAAI,CAAC,IAAI,MAAM,EAAE;YAC3D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,cAAc,EAAE,IAAI,CAAC,KAAK,EAAE;YAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;YACxC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;SACpC,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAgD,CAAC;QAC5E,IAAI,CAAC,CAAC,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,iBAAiB,CAAC,CAAC;QACzD,OAAO,CAAC,CAAC,MAAW,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,oBAAoB,IAAI,CAAC,IAAI,SAAS,CAAC,CAAC;YAChE,OAAO,GAAG,CAAC,EAAE,CAAC;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,GAAG,CAAmB,UAAU,CAAC,CAAC;IAChD,CAAC;IAED,SAAS,CAAC,OAAe,EAAE,IAAe;QACxC,OAAO,IAAI,CAAC,GAAG,CAAU,YAAY,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,QAAQ,CAAC,IAAY;QACnB,OAAO,IAAI,CAAC,GAAG,CAAU,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
projectPath: string;
|
|
3
|
+
port: number;
|
|
4
|
+
/** Set when a vela-agent dev probe was detected for this launch. */
|
|
5
|
+
probe?: {
|
|
6
|
+
port: number;
|
|
7
|
+
token: string;
|
|
8
|
+
};
|
|
9
|
+
/** Main-process Node inspector port (for vela_debug_attach target "main"). */
|
|
10
|
+
inspectPort?: number;
|
|
11
|
+
/** Tears down the Electron process and any CDP connections. */
|
|
12
|
+
dispose: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare class SessionRegistry {
|
|
15
|
+
private sessions;
|
|
16
|
+
private counter;
|
|
17
|
+
register(session: Session): string;
|
|
18
|
+
get(id: string): Session;
|
|
19
|
+
list(): Array<{
|
|
20
|
+
sessionId: string;
|
|
21
|
+
projectPath: string;
|
|
22
|
+
port: number;
|
|
23
|
+
}>;
|
|
24
|
+
dispose(id: string): Promise<void>;
|
|
25
|
+
disposeAll(): Promise<void>;
|
|
26
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class SessionRegistry {
|
|
2
|
+
sessions = new Map();
|
|
3
|
+
counter = 0;
|
|
4
|
+
register(session) {
|
|
5
|
+
const id = `s${++this.counter}-${session.port}`;
|
|
6
|
+
this.sessions.set(id, session);
|
|
7
|
+
return id;
|
|
8
|
+
}
|
|
9
|
+
get(id) {
|
|
10
|
+
const s = this.sessions.get(id);
|
|
11
|
+
if (!s)
|
|
12
|
+
throw new Error(`Unknown session: ${id}`);
|
|
13
|
+
return s;
|
|
14
|
+
}
|
|
15
|
+
list() {
|
|
16
|
+
return [...this.sessions.entries()].map(([sessionId, s]) => ({
|
|
17
|
+
sessionId,
|
|
18
|
+
projectPath: s.projectPath,
|
|
19
|
+
port: s.port
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
async dispose(id) {
|
|
23
|
+
const s = this.get(id);
|
|
24
|
+
await s.dispose();
|
|
25
|
+
this.sessions.delete(id);
|
|
26
|
+
}
|
|
27
|
+
async disposeAll() {
|
|
28
|
+
await Promise.all([...this.sessions.keys()].map((id) => this.dispose(id)));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAWA,MAAM,OAAO,eAAe;IAClB,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAC;IACtC,OAAO,GAAG,CAAC,CAAC;IAEpB,QAAQ,CAAC,OAAgB;QACvB,MAAM,EAAE,GAAG,IAAI,EAAE,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QAChD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QAC/B,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,GAAG,CAAC,EAAU;QACZ,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;QAClD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC3D,SAAS;YACT,WAAW,EAAE,CAAC,CAAC,WAAW;YAC1B,IAAI,EAAE,CAAC,CAAC,IAAI;SACb,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,EAAU;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvB,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC;CACF"}
|
package/dist/spec.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface Expectation {
|
|
2
|
+
equals?: unknown;
|
|
3
|
+
truthy?: boolean;
|
|
4
|
+
contains?: string;
|
|
5
|
+
matches?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SpecStep {
|
|
8
|
+
tool: "eval" | "ipc_invoke" | "ipc_list" | "targets" | "main_eval" | "screenshot";
|
|
9
|
+
args?: Record<string, unknown>;
|
|
10
|
+
expect?: Expectation;
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Spec {
|
|
14
|
+
name: string;
|
|
15
|
+
projectPath: string;
|
|
16
|
+
steps: SpecStep[];
|
|
17
|
+
}
|
|
18
|
+
export interface SpecStepResult {
|
|
19
|
+
label: string;
|
|
20
|
+
ok: boolean;
|
|
21
|
+
actual?: unknown;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SpecReport {
|
|
25
|
+
name: string;
|
|
26
|
+
passed: boolean;
|
|
27
|
+
steps: SpecStepResult[];
|
|
28
|
+
}
|
|
29
|
+
/** Evaluate an assertion against an observed value. Pure. */
|
|
30
|
+
export declare function checkExpect(actual: unknown, exp?: Expectation): {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
reason?: string;
|
|
33
|
+
};
|
|
34
|
+
/** Persist a spec as JSON; returns the file path. */
|
|
35
|
+
export declare function saveSpec(spec: Spec, root?: string): Promise<string>;
|
|
36
|
+
export declare function loadSpec(name: string, root?: string): Promise<Spec>;
|
|
37
|
+
export declare function listSpecs(root?: string): Promise<string[]>;
|
|
38
|
+
/** Launch the spec's app, run each step, assert, dispose, and report. */
|
|
39
|
+
export declare function runSpec(spec: Spec): Promise<SpecReport>;
|
package/dist/spec.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { SessionRegistry } from "./session.js";
|
|
2
|
+
import { launchTool } from "./tools/launch.js";
|
|
3
|
+
import { evalTool } from "./tools/eval.js";
|
|
4
|
+
import { ipcInvokeTool, ipcListTool } from "./tools/ipc.js";
|
|
5
|
+
import { mainEvalTool } from "./tools/main.js";
|
|
6
|
+
import { targetsTool } from "./tools/targets.js";
|
|
7
|
+
import { screenshotTool } from "./tools/screenshot.js";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { promises as fs } from "node:fs";
|
|
11
|
+
/** Evaluate an assertion against an observed value. Pure. */
|
|
12
|
+
export function checkExpect(actual, exp) {
|
|
13
|
+
if (!exp)
|
|
14
|
+
return { ok: true };
|
|
15
|
+
if ("equals" in exp) {
|
|
16
|
+
const ok = JSON.stringify(actual) === JSON.stringify(exp.equals);
|
|
17
|
+
return ok ? { ok } : { ok, reason: `expected ${JSON.stringify(exp.equals)}, got ${JSON.stringify(actual)}` };
|
|
18
|
+
}
|
|
19
|
+
if ("truthy" in exp) {
|
|
20
|
+
const ok = Boolean(actual) === exp.truthy;
|
|
21
|
+
return ok ? { ok } : { ok, reason: `expected truthy=${exp.truthy}, got ${JSON.stringify(actual)}` };
|
|
22
|
+
}
|
|
23
|
+
if ("contains" in exp) {
|
|
24
|
+
const s = JSON.stringify(actual) ?? "";
|
|
25
|
+
const ok = s.includes(exp.contains);
|
|
26
|
+
return ok ? { ok } : { ok, reason: `expected to contain "${exp.contains}", got ${s}` };
|
|
27
|
+
}
|
|
28
|
+
if ("matches" in exp) {
|
|
29
|
+
const ok = new RegExp(exp.matches).test(String(actual));
|
|
30
|
+
return ok ? { ok } : { ok, reason: `expected to match /${exp.matches}/, got ${String(actual)}` };
|
|
31
|
+
}
|
|
32
|
+
return { ok: true };
|
|
33
|
+
}
|
|
34
|
+
/** Reject spec names that could escape the specs directory. */
|
|
35
|
+
function assertSafeName(name) {
|
|
36
|
+
if (!name || /[/\\]/.test(name) || name === "." || name === "..") {
|
|
37
|
+
throw new Error(`Invalid spec name: "${name}" (no slashes or dot segments)`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function specsRoot(root) {
|
|
41
|
+
return root ?? path.join(os.homedir(), ".vela", "specs");
|
|
42
|
+
}
|
|
43
|
+
/** Persist a spec as JSON; returns the file path. */
|
|
44
|
+
export async function saveSpec(spec, root) {
|
|
45
|
+
assertSafeName(spec.name);
|
|
46
|
+
const dir = specsRoot(root);
|
|
47
|
+
await fs.mkdir(dir, { recursive: true });
|
|
48
|
+
const file = path.join(dir, `${spec.name}.json`);
|
|
49
|
+
await fs.writeFile(file, JSON.stringify(spec, null, 2));
|
|
50
|
+
return file;
|
|
51
|
+
}
|
|
52
|
+
export async function loadSpec(name, root) {
|
|
53
|
+
assertSafeName(name);
|
|
54
|
+
const file = path.join(specsRoot(root), `${name}.json`);
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(await fs.readFile(file, "utf8"));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new Error(`No spec named "${name}" in ${specsRoot(root)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function listSpecs(root) {
|
|
63
|
+
const dir = specsRoot(root);
|
|
64
|
+
try {
|
|
65
|
+
return (await fs.readdir(dir)).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// NOTE: the Parameters<> casts are intentionally unsound — spec steps are external JSON, so a
|
|
72
|
+
// malformed `args` shape surfaces as a thrown step error at run time rather than a compile error.
|
|
73
|
+
const STEP_RUNNERS = {
|
|
74
|
+
eval: (r, sessionId, a) => evalTool(r, { sessionId, ...a }).then((x) => x.value),
|
|
75
|
+
ipc_invoke: (r, sessionId, a) => ipcInvokeTool(r, { sessionId, ...a }).then((x) => x.result),
|
|
76
|
+
ipc_list: (r, sessionId) => ipcListTool(r, { sessionId }).then((x) => x.channels),
|
|
77
|
+
targets: (r, sessionId) => targetsTool(r, { sessionId }).then((x) => x.targets),
|
|
78
|
+
main_eval: (r, sessionId, a) => mainEvalTool(r, { sessionId, ...a }).then((x) => x.value),
|
|
79
|
+
screenshot: (r, sessionId, a) => screenshotTool(r, { sessionId, ...a }).then((x) => x.path)
|
|
80
|
+
};
|
|
81
|
+
/** Launch the spec's app, run each step, assert, dispose, and report. */
|
|
82
|
+
export async function runSpec(spec) {
|
|
83
|
+
const registry = new SessionRegistry();
|
|
84
|
+
const steps = [];
|
|
85
|
+
try {
|
|
86
|
+
let sessionId;
|
|
87
|
+
try {
|
|
88
|
+
({ sessionId } = await launchTool(registry, { projectPath: spec.projectPath }));
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
steps.push({ label: "launch", ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
92
|
+
return { name: spec.name, passed: false, steps };
|
|
93
|
+
}
|
|
94
|
+
for (const step of spec.steps) {
|
|
95
|
+
const label = step.label ?? step.tool;
|
|
96
|
+
const runner = STEP_RUNNERS[step.tool];
|
|
97
|
+
if (!runner) {
|
|
98
|
+
steps.push({ label, ok: false, error: `Unknown step tool "${step.tool}"` });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const actual = await runner(registry, sessionId, step.args ?? {});
|
|
103
|
+
const c = checkExpect(actual, step.expect);
|
|
104
|
+
steps.push({ label, ok: c.ok, actual, error: c.reason });
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
steps.push({ label, ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await registry.disposeAll();
|
|
113
|
+
}
|
|
114
|
+
return { name: spec.name, passed: steps.every((s) => s.ok), steps };
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=spec.js.map
|