test(mcp): add bridge integration probes
This commit is contained in:
parent
a4a098e923
commit
b0cc1c75c8
@ -18,11 +18,11 @@ The bridge also needs future end-to-end coverage that spans both runtimes: a hos
|
|||||||
|
|
||||||
Keep the Node package scripts simple and one-purpose:
|
Keep the Node package scripts simple and one-purpose:
|
||||||
|
|
||||||
- `npm run build` emits TypeScript into `dist/`.
|
- `npm run build` emits TypeScript into `dist/` and acts as the type-check gate.
|
||||||
- `npm run test` runs only the already-built Node unit tests.
|
- `npm run test` runs the Node unit tests directly from TypeScript with `tsx --test test/*.test.ts`. No prior build is required.
|
||||||
- `npm run check` runs ESLint. TypeScript compilation is covered by `npm run build` and `npm run ci` to avoid duplicate compiler runs in repository CI.
|
- `npm run check` runs ESLint. TypeScript compilation is covered by `npm run build` and `npm run ci` to avoid duplicate compiler runs in repository CI.
|
||||||
- `npm run ci` runs `npm run build && npm run test`.
|
- `npm run ci` runs `npm run check && npm run build && npm run test`, so type errors and lint failures both surface even though `test` itself no longer compiles.
|
||||||
- `npm run test-integration` is reserved for bridge-to-CraftOS integration coverage and currently prints an explicit TODO.
|
- `npm run test-integration` runs the bridge-to-CraftOS integration suite with `tsx --test --test-concurrency=1 test-integration/*.test.ts`. Each case boots the bridge in-process on fixed loopback ports (`127.0.0.1:2000` for MCP HTTP, `127.0.0.1:2001` for the CraftOS link), spawns a CraftOS-PC headless computer that connects back, exercises `tools/call probe-computers`, and tears everything down. `--test-concurrency=1` keeps the fixed ports collision-free.
|
||||||
|
|
||||||
Expose matching repository recipes for the Node lifecycle:
|
Expose matching repository recipes for the Node lifecycle:
|
||||||
|
|
||||||
@ -34,12 +34,12 @@ Expose matching repository recipes for the Node lifecycle:
|
|||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
- `npm run test` no longer hides a build step. Callers that need a fresh build must use `npm run ci` or `just test`.
|
- `npm run test` no longer needs `dist/`. Both unit and integration tests load TypeScript directly through `tsx`, so test iteration is faster and `dist/` is only required for `npm start`.
|
||||||
|
- TypeScript errors are no longer caught by `npm run test`; they are caught by `npm run build`, which stays wired into `npm run ci`, `just build`, `just test`, and `just ci`.
|
||||||
- `just ci` avoids duplicating Node unit tests by calling `npm run ci` directly and then invoking only the CraftOS-side test body.
|
- `just ci` avoids duplicating Node unit tests by calling `npm run ci` directly and then invoking only the CraftOS-side test body.
|
||||||
- ESLint failures are part of `just check`, so they are covered by the same pre-commit and pre-push hooks as Lua and Markdown checks. TypeScript compiler failures are covered by `just build`, `just test`, and `just ci`.
|
- ESLint failures are part of `just check`, so they are covered by the same pre-commit and pre-push hooks as Lua and Markdown checks.
|
||||||
- Future bridge integration tests have a named home before they exist, reducing the chance that slow end-to-end behavior is mixed into fast unit tests.
|
- Integration tests live in `tools/mcp-bridge/test-integration/`, with Lua client fixtures under `test-integration/lua/`. Slow end-to-end behavior stays out of the fast unit-test path.
|
||||||
|
|
||||||
## Future Work
|
## Future Work
|
||||||
|
|
||||||
- Replace the `npm run test-integration` placeholder with a harness that launches the bridge and CraftOS-PC headless, connects a websocket client from CraftOS, probes through the MCP surface, and tears down both processes reliably.
|
- Extend the integration harness with disconnect and reconnect scenarios (computer drops mid-probe; bridge restarts while a computer is connected) once those failure modes need regression coverage.
|
||||||
- Give the bridge integration harness host-level timeout handling and readable diagnostics modelled on the CraftOS-PC test recipes.
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export default tseslint.config(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["test/**/*.ts"],
|
files: ["test/**/*.ts", "test-integration/**/*.ts"],
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-floating-promises": "off",
|
"@typescript-eslint/no-floating-promises": "off",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,11 +6,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --noEmit false",
|
"build": "tsc --noEmit false",
|
||||||
"check": "eslint .",
|
"check": "eslint .",
|
||||||
"ci": "npm run build && npm run test",
|
"ci": "npm run check && npm run build && npm run test",
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "tsx src/index.ts",
|
||||||
"start": "node dist/src/index.js",
|
"start": "node dist/src/index.js",
|
||||||
"test": "node --test dist/test/*.test.js",
|
"test": "tsx --test test/*.test.ts",
|
||||||
"test-integration": "node -e \"console.log('TODO: add mcp-bridge integration tests that launch CraftOS-PC headless, start the bridge, connect a ComputerCraft websocket client from inside CraftOS, and exercise the MCP probe path end-to-end. The intended harness should verify startup ordering, websocket hello/response framing, timeout behavior when a computer is silent, and cleanup of both the CraftOS process and Node bridge server. Keep this separate from unit tests because it will need host ports, a CraftOS watchdog, and careful failure diagnostics similar to the existing Lua timeout fixtures.')\""
|
"test-integration": "tsx --test --test-concurrency=1 test-integration/*.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.17.1"
|
"ws": "^8.17.1"
|
||||||
|
|||||||
151
tools/mcp-bridge/test-integration/harness.ts
Normal file
151
tools/mcp-bridge/test-integration/harness.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { mkdtemp, rm } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import type { Server } from "node:http";
|
||||||
|
import type { WebSocketServer } from "ws";
|
||||||
|
import { LinkRegistry, startLinkServer } from "../src/link-server.js";
|
||||||
|
import { startMcpServer } from "../src/mcp-server.js";
|
||||||
|
|
||||||
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const LUA_DIR = join(HERE, "lua");
|
||||||
|
|
||||||
|
export const MCP_PORT = 2000;
|
||||||
|
export const LINK_PORT = 2001;
|
||||||
|
export const MCP_URL = `http://127.0.0.1:${MCP_PORT}`;
|
||||||
|
|
||||||
|
export type Bridge = {
|
||||||
|
registry: LinkRegistry;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function startBridge(probeTimeoutMs = 500): Promise<Bridge> {
|
||||||
|
const registry = new LinkRegistry();
|
||||||
|
const mcpServer = startMcpServer({ host: "127.0.0.1", port: MCP_PORT, probeTimeoutMs, registry });
|
||||||
|
const linkServer = startLinkServer({ host: "127.0.0.1", port: LINK_PORT, registry });
|
||||||
|
await Promise.all([waitForListening(mcpServer), waitForListening(linkServer)]);
|
||||||
|
return {
|
||||||
|
registry,
|
||||||
|
close: () => closeBridge(mcpServer, linkServer),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForComputers(registry: LinkRegistry, count: number, timeoutMs: number): Promise<void> {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (registry.count() >= count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await sleep(50);
|
||||||
|
}
|
||||||
|
throw new Error(`waitForComputers timed out (expected ${count}, got ${registry.count()})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function callProbeComputers(): Promise<string> {
|
||||||
|
const body = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 1,
|
||||||
|
method: "tools/call",
|
||||||
|
params: { name: "probe-computers", arguments: {} },
|
||||||
|
};
|
||||||
|
const response = await fetch(MCP_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const payload = (await response.json()) as {
|
||||||
|
result?: { content?: { type: string; text: string }[] };
|
||||||
|
};
|
||||||
|
const text = payload.result?.content?.[0]?.text;
|
||||||
|
if (typeof text !== "string") {
|
||||||
|
throw new Error(`Unexpected MCP response: ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CraftosResult = {
|
||||||
|
status: number | null;
|
||||||
|
signal: NodeJS.Signals | null;
|
||||||
|
output: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CraftosHandle = {
|
||||||
|
done: Promise<CraftosResult>;
|
||||||
|
abort: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function startCraftos(
|
||||||
|
luaName: string,
|
||||||
|
opts: { shellArgs?: string[]; timeoutMs?: number } = {},
|
||||||
|
): CraftosHandle {
|
||||||
|
const timeoutMs = opts.timeoutMs ?? 15_000;
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const done = (async (): Promise<CraftosResult> => {
|
||||||
|
const dataDir = await mkdtemp(join(tmpdir(), "mcp-bridge-it-"));
|
||||||
|
const watchdog = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
try {
|
||||||
|
const args: string[] = ["--directory", dataDir, "--headless", "--mount-ro", `/staging=${LUA_DIR}`];
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
args.push("--rom", "/Applications/CraftOS-PC.app/Contents/Resources");
|
||||||
|
}
|
||||||
|
args.push("--exec", buildExecCode(luaName, opts.shellArgs ?? []));
|
||||||
|
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
const child = spawn("craftos", args, { signal: controller.signal });
|
||||||
|
child.stdout.on("data", (d: Buffer) => chunks.push(d));
|
||||||
|
child.stderr.on("data", (d: Buffer) => chunks.push(d));
|
||||||
|
|
||||||
|
const result = await new Promise<{ status: number | null; signal: NodeJS.Signals | null }>((resolve) => {
|
||||||
|
child.once("close", (code, signal) => resolve({ status: code, signal }));
|
||||||
|
child.once("error", () => resolve({ status: null, signal: null }));
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status: result.status, signal: result.signal, output: Buffer.concat(chunks).toString("utf8") };
|
||||||
|
} finally {
|
||||||
|
clearTimeout(watchdog);
|
||||||
|
await rm(dataDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return { done, abort: () => controller.abort() };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFailure(message: string, craftosOutput: string): string {
|
||||||
|
const lines = ["\x1b[31mFAIL\x1b[0m " + message, "--- craftos output ---", craftosOutput.trimEnd(), "----------------------"];
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExecCode(luaName: string, shellArgs: string[]): string {
|
||||||
|
const parts = [`'/staging/${luaName}'`, ...shellArgs.map(luaQuote)];
|
||||||
|
return `shell.run(${parts.join(", ")})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function luaQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForListening(server: Server | WebSocketServer): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const addr = "address" in server ? server.address() : null;
|
||||||
|
if (addr) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
server.once("listening", () => resolve());
|
||||||
|
server.once("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeBridge(mcpServer: Server, linkServer: WebSocketServer): Promise<void> {
|
||||||
|
for (const client of linkServer.clients) {
|
||||||
|
client.terminate();
|
||||||
|
}
|
||||||
|
await new Promise<void>((resolve) => linkServer.close(() => resolve()));
|
||||||
|
await new Promise<void>((resolve) => mcpServer.close(() => resolve()));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
42
tools/mcp-bridge/test-integration/lua/echo-client.lua
Normal file
42
tools/mcp-bridge/test-integration/lua/echo-client.lua
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
-- Integration-test helper: connect to the mcp-bridge link server, send a hello
|
||||||
|
-- frame, then reply to every `request` with a `pong` response until a deadline
|
||||||
|
-- expires. Args (via shell.run): id, label, urlBase, durationSeconds.
|
||||||
|
local args = {...};
|
||||||
|
|
||||||
|
local id = tonumber(args[1]) or 1;
|
||||||
|
local label = args[2] or "echo";
|
||||||
|
local urlBase = args[3] or "ws://127.0.0.1:2001";
|
||||||
|
local duration = tonumber(args[4]) or 5;
|
||||||
|
|
||||||
|
local url = urlBase .. "/?id=" .. id;
|
||||||
|
local ws, err = http.websocket(url);
|
||||||
|
if not ws then
|
||||||
|
print("websocket failed: " .. tostring(err));
|
||||||
|
os.shutdown();
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
ws.send(textutils.serializeJSON({
|
||||||
|
type = "hello",
|
||||||
|
computerId = id,
|
||||||
|
computerLabel = label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
local deadline = os.epoch("utc") + duration * 1000;
|
||||||
|
while os.epoch("utc") < deadline do
|
||||||
|
local msg = ws.receive(0.5);
|
||||||
|
if msg then
|
||||||
|
local frame = textutils.unserializeJSON(msg);
|
||||||
|
if frame and frame.type == "request" then
|
||||||
|
ws.send(textutils.serializeJSON({
|
||||||
|
type = "response",
|
||||||
|
id = frame.id,
|
||||||
|
ok = true,
|
||||||
|
result = "pong from " .. id .. " (Label: " .. label .. ")",
|
||||||
|
}));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
pcall(function() ws.close(); end);
|
||||||
|
os.shutdown();
|
||||||
43
tools/mcp-bridge/test-integration/lua/multi-echo-client.lua
Normal file
43
tools/mcp-bridge/test-integration/lua/multi-echo-client.lua
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
-- Integration-test helper: open two websocket connections in parallel, each
|
||||||
|
-- presenting itself as a distinct logical computer. Used by probe-multi.
|
||||||
|
local urlBase = "ws://127.0.0.1:2001";
|
||||||
|
|
||||||
|
local function connect(id, label, duration)
|
||||||
|
local url = urlBase .. "/?id=" .. id;
|
||||||
|
local ws, err = http.websocket(url);
|
||||||
|
if not ws then
|
||||||
|
print("websocket failed (" .. id .. "): " .. tostring(err));
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
ws.send(textutils.serializeJSON({
|
||||||
|
type = "hello",
|
||||||
|
computerId = id,
|
||||||
|
computerLabel = label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
local deadline = os.epoch("utc") + duration * 1000;
|
||||||
|
while os.epoch("utc") < deadline do
|
||||||
|
local msg = ws.receive(0.3);
|
||||||
|
if msg then
|
||||||
|
local frame = textutils.unserializeJSON(msg);
|
||||||
|
if frame and frame.type == "request" then
|
||||||
|
ws.send(textutils.serializeJSON({
|
||||||
|
type = "response",
|
||||||
|
id = frame.id,
|
||||||
|
ok = true,
|
||||||
|
result = "pong from " .. id .. " (Label: " .. label .. ")",
|
||||||
|
}));
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
pcall(function() ws.close(); end);
|
||||||
|
end
|
||||||
|
|
||||||
|
parallel.waitForAll(
|
||||||
|
function() connect(1001, "echo-A", 5); end,
|
||||||
|
function() connect(1002, "echo-B", 5); end
|
||||||
|
);
|
||||||
|
|
||||||
|
os.shutdown();
|
||||||
27
tools/mcp-bridge/test-integration/lua/silent-client.lua
Normal file
27
tools/mcp-bridge/test-integration/lua/silent-client.lua
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
-- Integration-test helper: connect, send a hello frame, then ignore every
|
||||||
|
-- request until the deadline expires. Used to prove the bridge's per-computer
|
||||||
|
-- probe timeout path.
|
||||||
|
local args = {...};
|
||||||
|
|
||||||
|
local id = tonumber(args[1]) or 99;
|
||||||
|
local label = args[2] or "silent";
|
||||||
|
local duration = tonumber(args[3]) or 5;
|
||||||
|
|
||||||
|
local url = "ws://127.0.0.1:2001/?id=" .. id;
|
||||||
|
local ws, err = http.websocket(url);
|
||||||
|
if not ws then
|
||||||
|
print("websocket failed: " .. tostring(err));
|
||||||
|
os.shutdown();
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
ws.send(textutils.serializeJSON({
|
||||||
|
type = "hello",
|
||||||
|
computerId = id,
|
||||||
|
computerLabel = label,
|
||||||
|
}));
|
||||||
|
|
||||||
|
sleep(duration);
|
||||||
|
|
||||||
|
pcall(function() ws.close(); end);
|
||||||
|
os.shutdown();
|
||||||
12
tools/mcp-bridge/test-integration/probe-empty.test.ts
Normal file
12
tools/mcp-bridge/test-integration/probe-empty.test.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { callProbeComputers, startBridge } from "./harness.js";
|
||||||
|
|
||||||
|
test("probe-computers returns the no-computers message when nothing is connected", async () => {
|
||||||
|
const bridge = await startBridge();
|
||||||
|
try {
|
||||||
|
assert.equal(await callProbeComputers(), "No computers connected.");
|
||||||
|
} finally {
|
||||||
|
await bridge.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
21
tools/mcp-bridge/test-integration/probe-happy.test.ts
Normal file
21
tools/mcp-bridge/test-integration/probe-happy.test.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { callProbeComputers, formatFailure, startBridge, startCraftos, waitForComputers } from "./harness.js";
|
||||||
|
|
||||||
|
test("probe-computers aggregates a single CraftOS echo computer", async () => {
|
||||||
|
const bridge = await startBridge();
|
||||||
|
const craftos = startCraftos("echo-client.lua", { shellArgs: ["1", "echo-1", "ws://127.0.0.1:2001", "8"] });
|
||||||
|
try {
|
||||||
|
await waitForComputers(bridge.registry, 1, 12_000);
|
||||||
|
const text = await callProbeComputers();
|
||||||
|
assert.equal(text, "pong from 1 (Label: echo-1)");
|
||||||
|
} catch (error) {
|
||||||
|
craftos.abort();
|
||||||
|
const result = await craftos.done;
|
||||||
|
throw new Error(formatFailure(error instanceof Error ? error.message : String(error), result.output), { cause: error });
|
||||||
|
} finally {
|
||||||
|
craftos.abort();
|
||||||
|
await craftos.done;
|
||||||
|
await bridge.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
25
tools/mcp-bridge/test-integration/probe-multi.test.ts
Normal file
25
tools/mcp-bridge/test-integration/probe-multi.test.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { callProbeComputers, formatFailure, startBridge, startCraftos, waitForComputers } from "./harness.js";
|
||||||
|
|
||||||
|
test("probe-computers aggregates two CraftOS computers from the same headless process", async () => {
|
||||||
|
const bridge = await startBridge();
|
||||||
|
const craftos = startCraftos("multi-echo-client.lua", { timeoutMs: 15_000 });
|
||||||
|
try {
|
||||||
|
await waitForComputers(bridge.registry, 2, 12_000);
|
||||||
|
const text = await callProbeComputers();
|
||||||
|
const lines = text.split("\n").sort();
|
||||||
|
assert.deepEqual(lines, [
|
||||||
|
"pong from 1001 (Label: echo-A)",
|
||||||
|
"pong from 1002 (Label: echo-B)",
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
craftos.abort();
|
||||||
|
const result = await craftos.done;
|
||||||
|
throw new Error(formatFailure(error instanceof Error ? error.message : String(error), result.output), { cause: error });
|
||||||
|
} finally {
|
||||||
|
craftos.abort();
|
||||||
|
await craftos.done;
|
||||||
|
await bridge.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
21
tools/mcp-bridge/test-integration/probe-silent.test.ts
Normal file
21
tools/mcp-bridge/test-integration/probe-silent.test.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { callProbeComputers, formatFailure, startBridge, startCraftos, waitForComputers } from "./harness.js";
|
||||||
|
|
||||||
|
test("probe-computers reports timeout for a connected computer that never replies", async () => {
|
||||||
|
const bridge = await startBridge(300);
|
||||||
|
const craftos = startCraftos("silent-client.lua", { shellArgs: ["7", "silent-7", "8"] });
|
||||||
|
try {
|
||||||
|
await waitForComputers(bridge.registry, 1, 12_000);
|
||||||
|
const text = await callProbeComputers();
|
||||||
|
assert.equal(text, "timeout from 7 (Label: silent-7)");
|
||||||
|
} catch (error) {
|
||||||
|
craftos.abort();
|
||||||
|
const result = await craftos.done;
|
||||||
|
throw new Error(formatFailure(error instanceof Error ? error.message : String(error), result.output), { cause: error });
|
||||||
|
} finally {
|
||||||
|
craftos.abort();
|
||||||
|
await craftos.done;
|
||||||
|
await bridge.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -11,5 +11,5 @@
|
|||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"declaration": true
|
"declaration": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
"include": ["src/**/*.ts", "test/**/*.ts", "test-integration/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user