Files
reverse-engineering-anything/tests/boundary/mcp/deferredRuntime.test.ts
郭立 Guo Li 0b7213c890 perf(mcp): defer analysis runtime after catalog discovery (#4424)
Serve build-recorded default compact and explicit full catalogs, then materialize canonical analysis handlers on first use. Keep transport, Evidence delivery, profile identity and caller choices consistent. Share sealed MCP server ownership between eager and deferred lifecycle paths.

Validate 125 focused cases, schema/contract repeats, negative controls, builds/static/docs checks and latest-main integration. Record 72 real SDK task trials with independent Rust metadata oracles and whole-family resource bounds; disclose shifted first-RPC cost and unchanged catalog tokens. Qualify the real Rizin small-target debug lifecycle separately. Automated review findings addressed; Codex review unavailable due repository usage limits.
2026-10-22 03:07:31 +00:00

799 lines
27 KiB
TypeScript

import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { spawnSync } from "node:child_process";
import {
cp,
mkdir,
readFile,
readdir,
symlink,
writeFile,
} from "node:fs/promises";
import { resolve, join } from "node:path";
import { expect, it, onTestFinished } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../../fixtures/mcpToolCatalog.js";
import { PROMPT_CONTRACTS } from "../../../src/contracts/promptContracts.js";
import { evidenceSchema } from "../../../src/domain/evidence.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { SERVER_START_FAILED } from "../../../src/main/messages.js";
const connect = async (
mode: "legacy" | "auto",
environment: Record<string, string> = {},
nodeArguments: readonly string[] = [],
) => {
const transport = new StdioClientTransport({
command: process.execPath,
args: [...nodeArguments, resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: {
PATH: process.env.PATH ?? "",
HOME: process.env.HOME ?? "",
REA_LOG_LEVEL: "silent",
...environment,
},
stderr: "pipe",
});
let stderr = "";
transport.stderr?.on("data", (value) => {
stderr += value;
});
const client = new Client(
{ name: "deferred-runtime-boundary", version: "1" },
{ versionNegotiation: { mode } },
);
onTestFinished(async () => {
await client.close();
await transport.close();
});
await client.connect(transport);
return { client, stderr: () => stderr };
};
it.each([
{ name: "deferred", environment: {}, eager: false },
{
name: "compact deferred",
environment: { REA_MCP_INPUT_SCHEMA_PROFILE: "compact" },
eager: false,
},
{
name: "full deferred",
environment: { REA_MCP_INPUT_SCHEMA_PROFILE: "full" },
eager: false,
},
{
name: "output-off eager",
environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" },
eager: true,
},
])(
"loads compiled analysis at the intended phase for $name startup",
async ({ environment, eager }) => {
const directory = await createTestTempDirectory("rea-mcp-module-loads-");
const log = join(directory, "loaded.jsonl");
await writeFile(log, "");
const modules = [
"main/eagerRuntime.js",
"server/createServer.js",
"composition/binary.js",
"contracts/toolContracts.js",
];
const hook = `data:text/javascript,${encodeURIComponent(`
import { registerHooks } from "node:module";
import { appendFileSync } from "node:fs";
const modules = ${JSON.stringify(modules)};
registerHooks({
load(url, context, next) {
const loaded = next(url, context);
const source = loaded.source?.toString() ?? "";
const names = modules.filter((name) =>
url.endsWith("/dist/" + name) || source.includes("//#region dist/" + name + "\\n"));
if (url.endsWith("/scripts/rea.mjs")) names.push("entrypoint");
for (const name of names)
appendFileSync(${JSON.stringify(log)}, JSON.stringify(name) + "\\n");
return loaded;
},
});
`)}`;
const loadedModules = async () =>
new Set(
(await readFile(log, "utf8"))
.trim()
.split("\n")
.filter(Boolean)
.map((line): unknown => JSON.parse(line)),
);
const { client, stderr } = await connect("legacy", environment, [
`--import=${hook}`,
]);
await client.listTools();
await client.listPrompts();
expect(await loadedModules()).toEqual(
new Set(eager ? ["entrypoint", ...modules] : ["entrypoint"]),
);
const result = await client.callTool({
name: "list_unknowns",
arguments: {},
});
expect(result.isError).not.toBe(true);
expect(await loadedModules()).toEqual(new Set(["entrypoint", ...modules]));
expect(stderr()).toBe("");
},
);
it.each([
{ mode: "legacy", profile: "full" },
{ mode: "auto", profile: "full" },
{ mode: "legacy", profile: "compact" },
{ mode: "auto", profile: "compact" },
] as const)(
"serves $profile discovery and canonical handlers over $mode negotiation",
async ({ mode, profile }) => {
const recorded = JSON.parse(
await readFile(
profile === "full"
? "dist/mcp-catalog-full.json"
: "dist/mcp-catalog.json",
"utf8",
),
);
const { client, stderr } = await connect(mode, {
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
});
const before = await client.listTools();
expect(before.tools).toEqual(recorded.tools);
const contractProjection = (tool: {
name: string;
title?: string | undefined;
description?: string | undefined;
inputSchema: unknown;
outputSchema?: unknown;
annotations?: unknown;
}) => ({
name: tool.name,
title: tool.title,
description: tool.description,
inputSchema: tool.inputSchema,
outputSchema: tool.outputSchema,
annotations: tool.annotations,
});
if (profile === "full")
expect(
new Map(
before.tools.map((tool) => [tool.name, contractProjection(tool)]),
),
).toEqual(
new Map(
GENERATED_MCP_TOOL_CATALOG.map((tool) => [
tool.name,
contractProjection(tool),
]),
),
);
expect((await client.listPrompts()).prompts).toEqual(
PROMPT_CONTRACTS.map((prompt) => ({
name: prompt.name,
title: prompt.title,
description: prompt.description,
arguments: Object.entries(prompt.arguments).map(([name, argument]) => ({
name,
description: argument.description,
required: argument.required,
})),
})),
);
expect((await client.listPrompts()).prompts).toEqual(
recorded.prompts.map(
({ parameters: _parameters, ...prompt }: Record<string, unknown>) =>
prompt,
),
);
const results = await Promise.all([
client.callTool({ name: "list_unknowns", arguments: {} }),
client.callTool({ name: "list_unknowns", arguments: { limit: 1 } }),
]);
expect(results.every((result) => result.isError !== true)).toBe(true);
expect(results[0]?.structuredContent).toEqual({
result: { items: [], total: 0, offset: 0, next_offset: null },
});
expect(await client.listTools()).toEqual(before);
const invalid = await client.callTool({
name: "list_unknowns",
arguments: { limit: 1.5 },
});
expect(invalid.isError).toBe(true);
expect(invalid._meta?.provenance).toEqual({
untrusted_content: true,
source: "analysis_target",
});
expect(client.getProtocolEra()).toBe(mode === "auto" ? "modern" : "legacy");
if (mode === "auto")
expect(invalid._meta?.["io.modelcontextprotocol/serverInfo"]).toEqual(
recorded.info,
);
expect(stderr()).toBe("");
},
);
it("preserves connection creation time across deferred materialization", async () => {
const directory = await createTestTempDirectory("rea-mcp-started-at-");
const clock = join(directory, "clock");
const startedAt = "2020-01-01T00:00:00.000Z";
await writeFile(clock, startedAt);
const hook = `data:text/javascript,${encodeURIComponent(`
import { readFileSync } from "node:fs";
const NativeDate = Date;
globalThis.Date = class extends NativeDate {
constructor(...args) {
super(...(args.length === 0 ? [readFileSync(${JSON.stringify(clock)}, "utf8")] : args));
}
};
`)}`;
const { client } = await connect("legacy", {}, [`--import=${hook}`]);
await client.listTools();
await writeFile(clock, "2021-01-01T00:00:00.000Z");
const first = await client.callTool({
name: "binary_session",
arguments: {},
});
expect(first.isError).not.toBe(true);
expect(first.structuredContent).toMatchObject({
result: { server_identity: { server: { started_at: startedAt } } },
});
const again = await client.callTool({
name: "binary_session",
arguments: {},
});
expect(again.structuredContent).toMatchObject({
result: { server_identity: { server: { started_at: startedAt } } },
});
});
it.each([
{ level: "info", asynchronous: false, errorObject: true },
{ level: "info", asynchronous: true, errorObject: false },
{ level: "silent", asynchronous: false, errorObject: true },
])(
"logs deferred startup failure with $level logging and asynchronous=$asynchronous",
({ level, asynchronous, errorObject }) => {
const child = spawnSync(
process.execPath,
[
"--input-type=module",
"--eval",
`
import assert from "node:assert/strict";
import { runDeferred } from "./dist/main/deferredRuntime.js";
import { parseConfig } from "./dist/config/parseConfig.js";
const config = parseConfig({ HOME: process.env.HOME, REA_LOG_LEVEL: ${JSON.stringify(level)} });
assert.equal(config.ok, true);
const output = [];
let closeCalls = 0;
let unregistrations = 0;
const result = await runDeferred({
env: {},
writeStderr: (text) => output.push(text),
setExitCode: () => {},
registerShutdown: () => () => { unregistrations++; },
serve: (factory) => {
const server = factory({});
const close = server.server.close.bind(server.server);
server.server.close = async () => { closeCalls++; await close(); };
const cause = ${errorObject ? 'new Error("transport setup reason")' : '"transport setup reason"'};
if (${asynchronous}) return Promise.reject(cause);
throw cause;
},
}, config.value);
console.log(JSON.stringify({ result, output, closeCalls, unregistrations }));
`,
],
{ cwd: process.cwd(), encoding: "utf8", timeout: 10000 },
);
expect(child.error).toBeUndefined();
expect(child.status, child.stderr).toBe(0);
expect(JSON.parse(child.stdout)).toEqual({
result: 1,
output: [`${SERVER_START_FAILED}\n`],
closeCalls: 1,
unregistrations: 1,
});
if (level === "silent") {
expect(child.stderr).toBe("");
} else {
expect(JSON.parse(child.stderr)).toMatchObject({
level: 50,
application: "rea",
mode: "mcp",
layer: "server",
error: "transport setup reason",
msg: SERVER_START_FAILED,
});
}
},
);
it("retains process cleanup ownership after partial startup and failed cleanup", () => {
const child = spawnSync(
process.execPath,
[
"--input-type=module",
"--eval",
`
import assert from "node:assert/strict";
import { runDeferred } from "./dist/main/deferredRuntime.js";
import { parseConfig } from "./dist/config/parseConfig.js";
const config = parseConfig({ HOME: process.env.HOME, REA_LOG_LEVEL: "silent" });
assert.equal(config.ok, true);
const handlers = [];
const calls = [0, 0];
let unregistrations = 0;
let captured;
let registeredBeforeServe;
const result = await runDeferred({
env: {},
writeStderr: () => {},
setExitCode: () => {},
registerShutdown: (handler) => {
handlers.push(handler);
return () => { unregistrations++; };
},
captureShutdown: (close) => { captured = close; },
serve: (factory) => {
registeredBeforeServe = handlers.length;
for (let index = 0; index < calls.length; index++) {
const server = factory({});
const close = server.server.close.bind(server.server);
server.server.close = async () => {
calls[index]++;
if (index === 0 && calls[index] === 1) throw new Error("close denied");
await close();
};
}
throw new Error("serve failed after creating servers");
},
}, config.value).then(() => "unexpected success", (cause) => cause);
assert.ok(result instanceof AggregateError);
assert.equal(result.errors[0].message, "serve failed after creating servers");
assert.ok(result.errors[1] instanceof AggregateError);
assert.match(String(result.errors[1].errors[0].errors[0]), /close denied/);
assert.equal(registeredBeforeServe, 1);
assert.deepEqual(calls, [1, 1]);
assert.equal(unregistrations, 0);
assert.equal(typeof captured, "function");
const retry = captured();
handlers[0]();
await retry;
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(calls, [2, 1]);
assert.equal(unregistrations, 1);
await captured();
assert.deepEqual(calls, [2, 1]);
assert.equal(unregistrations, 1);
console.log("STARTUP_CLEANUP_RETRIED");
`,
],
{ cwd: process.cwd(), encoding: "utf8", timeout: 10000 },
);
expect(child.error).toBeUndefined();
expect(child.status, child.stderr).toBe(0);
expect(child.stdout).toBe("STARTUP_CLEANUP_RETRIED\n");
expect(child.stderr).toBe("");
});
it.each(["handle", "server"])(
"retries only the failed %s cleanup owner",
(failedOwner) => {
const child = spawnSync(
process.execPath,
[
"--input-type=module",
"--eval",
`
import assert from "node:assert/strict";
import { runDeferred } from "./dist/main/deferredRuntime.js";
import { parseConfig } from "./dist/config/parseConfig.js";
const config = parseConfig({ HOME: process.env.HOME });
assert.equal(config.ok, true);
const failedOwner = ${JSON.stringify(failedOwner)};
const calls = { handle: 0, server: 0 };
let captured;
let unregistrations = 0;
const result = await runDeferred({
env: {}, writeStderr: () => {}, setExitCode: () => {},
registerShutdown: () => () => { unregistrations++; },
captureShutdown: (close) => { captured = close; },
serve: (factory) => {
const server = factory({});
const close = server.server.close.bind(server.server);
server.server.close = async () => {
calls.server++;
if (failedOwner === "server" && calls.server === 1) throw new Error("server denied");
await close();
};
return { close: () => {
calls.handle++;
if (failedOwner === "handle" && calls.handle === 1) throw new Error("handle denied");
// Deliberately not idempotent: a successful handle must never be retried.
assert.ok(calls.handle <= (failedOwner === "handle" ? 2 : 1));
return Promise.resolve();
} };
},
}, config.value);
assert.equal(result, 0);
await assert.rejects(captured(), AggregateError);
assert.deepEqual(calls, { handle: 1, server: 1 });
assert.equal(unregistrations, 0);
await captured();
assert.deepEqual(calls, { handle: failedOwner === "handle" ? 2 : 1, server: failedOwner === "server" ? 2 : 1 });
assert.equal(unregistrations, 1);
await captured();
assert.equal(unregistrations, 1);
console.log("MIXED_CLEANUP_RETRIED");
`,
],
{ cwd: process.cwd(), encoding: "utf8", timeout: 10000 },
);
expect(child.error).toBeUndefined();
expect(child.status, child.stderr).toBe(0);
expect(child.stdout).toBe("MIXED_CLEANUP_RETRIED\n");
expect(child.stderr).toBe("");
},
);
it("joins shutdown with a transport and server published after shutdown starts", () => {
const child = spawnSync(
process.execPath,
[
"--input-type=module",
"--eval",
`
import assert from "node:assert/strict";
import { runDeferred } from "./dist/main/deferredRuntime.js";
import { parseConfig } from "./dist/config/parseConfig.js";
const config = parseConfig({ HOME: process.env.HOME });
assert.equal(config.ok, true);
const transport = Promise.withResolvers();
let factory;
let captured;
let signal;
let unregistrations = 0;
let handleCalls = 0;
let serverCalls = 0;
const running = runDeferred({
env: {}, writeStderr: () => {}, setExitCode: () => {},
registerShutdown: (handler) => {
signal = handler;
return () => { unregistrations++; };
},
captureShutdown: (close) => { captured = close; },
serve: (create) => { factory = create; return transport.promise; },
}, config.value);
signal();
const closing = captured();
let closed = false;
void closing.then(() => { closed = true; });
await new Promise((resolve) => setImmediate(resolve));
assert.equal(closed, false);
assert.equal(unregistrations, 0);
const server = factory({});
const close = server.server.close.bind(server.server);
server.server.close = async () => { serverCalls++; await close(); };
transport.resolve({ close: async () => { handleCalls++; } });
assert.equal(await running, 0);
await closing;
assert.deepEqual([handleCalls, serverCalls, unregistrations], [1, 1, 1]);
await captured();
assert.deepEqual([handleCalls, serverCalls, unregistrations], [1, 1, 1]);
console.log("LATE_TRANSPORT_CLOSED");
`,
],
{ cwd: process.cwd(), encoding: "utf8", timeout: 10000 },
);
expect(child.error).toBeUndefined();
expect(child.status, child.stderr).toBe(0);
expect(child.stdout).toBe("LATE_TRANSPORT_CLOSED\n");
expect(child.stderr).toBe("");
});
it("refuses server publication after the shutdown snapshot and completed close", () => {
const child = spawnSync(
process.execPath,
[
"--input-type=module",
"--eval",
`
import assert from "node:assert/strict";
import { runDeferred } from "./dist/main/deferredRuntime.js";
import { parseConfig } from "./dist/config/parseConfig.js";
const config = parseConfig({ HOME: process.env.HOME });
assert.equal(config.ok, true);
const closingHandle = Promise.withResolvers();
const releaseHandle = Promise.withResolvers();
let factory;
let captured;
let handleCalls = 0;
let serverCalls = 0;
let unregistrations = 0;
const result = await runDeferred({
env: {}, writeStderr: () => {}, setExitCode: () => {},
registerShutdown: () => () => { unregistrations++; },
captureShutdown: (close) => { captured = close; },
serve: (create) => {
factory = create;
const server = factory({});
const close = server.server.close.bind(server.server);
server.server.close = async () => { serverCalls++; await close(); };
return { close: async () => {
handleCalls++;
closingHandle.resolve();
await releaseHandle.promise;
} };
},
}, config.value);
assert.equal(result, 0);
const closing = captured();
await closingHandle.promise;
assert.throws(() => factory({}), /MCP connection is closed/);
releaseHandle.resolve();
await closing;
assert.throws(() => factory({}), /MCP connection is closed/);
await captured();
assert.deepEqual([handleCalls, serverCalls, unregistrations], [1, 1, 1]);
console.log("LATE_PUBLICATION_REFUSED");
`,
],
{ cwd: process.cwd(), encoding: "utf8", timeout: 10000 },
);
expect(child.error).toBeUndefined();
expect(child.status, child.stderr).toBe(0);
expect(child.stdout).toBe("LATE_PUBLICATION_REFUSED\n");
expect(child.stderr).toBe("");
});
it.each([
{},
{ REA_MCP_INPUT_SCHEMA_PROFILE: "compact" },
{ REA_MCP_INPUT_SCHEMA_PROFILE: "full" },
{ REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" },
])("preserves the configured catalog profile %j", async (environment) => {
const { client } = await connect("legacy", environment);
const before = await client.listTools();
const catalog = JSON.parse(
await readFile(
environment.REA_MCP_INPUT_SCHEMA_PROFILE === "full"
? "dist/mcp-catalog-full.json"
: "dist/mcp-catalog.json",
"utf8",
),
);
expect(before.tools).toHaveLength(catalog.tools.length);
const expected =
environment.REA_MCP_OUTPUT_SCHEMA_PROFILE === "off"
? catalog.tools.map(
({ outputSchema: _output, ...tool }: Record<string, unknown>) => tool,
)
: catalog.tools;
expect(before.tools).toEqual(expected);
const result = await client.callTool({
name: "list_unknowns",
arguments: {},
});
expect(result.isError).not.toBe(true);
expect(await client.listTools()).toEqual(before);
});
it("runs the real static artifact workflow after deferred initialization", async () => {
const { client } = await connect("legacy");
const opened = await client.callTool({
name: "open_binary",
arguments: { path: resolve("scripts/rea.mjs") },
});
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
const inspected = await client.callTool({
name: "inspect_artifact",
arguments: { detail: "summary" },
});
expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true);
const evidence = evidenceSchema.parse(inspected.structuredContent);
expect(evidence.operation).toBe("inspect_analysis_view");
const view = analysisViewResultSchema.parse(evidence.normalized_result);
expect(view.parent_operation).toBe("inspect_artifact");
const retained = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: {
kind: "retained-evidence",
evidence_id: view.parent_evidence_id,
},
view: { kind: "summary" },
},
});
expect(retained.isError, JSON.stringify(retained)).not.toBe(true);
const closed = await client.callTool({ name: "close_binary", arguments: {} });
expect(closed.isError, JSON.stringify(closed)).not.toBe(true);
});
const copyCompiledPackage = async (prefix: string): Promise<string> => {
const directory = await createTestTempDirectory(prefix);
await cp(resolve("dist"), join(directory, "dist"), { recursive: true });
await mkdir(join(directory, "scripts"));
await cp(resolve("scripts/rea.mjs"), join(directory, "scripts/rea.mjs"));
await cp(resolve("package.json"), join(directory, "package.json"));
await symlink(
resolve("node_modules"),
join(directory, "node_modules"),
process.platform === "win32" ? "junction" : "dir",
);
return directory;
};
it("records the canonical catalog without a host account or ambient configuration", async () => {
const directory = await copyCompiledPackage("rea-catalog-without-account-");
await cp(
resolve("scripts/record-mcp-catalog.mjs"),
join(directory, "scripts/record-mcp-catalog.mjs"),
);
const temporary = join(directory, "temporary");
await mkdir(temporary);
const hook = `data:text/javascript,${encodeURIComponent(`
import os from "node:os";
import { syncBuiltinESMExports } from "node:module";
os.userInfo = () => { throw new Error("Host account is unavailable"); };
syncBuiltinESMExports();
`)}`;
const child = spawnSync(
process.execPath,
[`--import=${hook}`, join(directory, "scripts/record-mcp-catalog.mjs")],
{
cwd: directory,
env: {
...process.env,
HOME: join(directory, "ambient-home"),
USERPROFILE: join(directory, "ambient-home"),
TMPDIR: temporary,
TMP: temporary,
TEMP: temporary,
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "off",
},
encoding: "utf8",
timeout: 30000,
},
);
expect(child.error).toBeUndefined();
expect(child.status, child.stderr).toBe(0);
expect(child.stdout).toBe("");
expect(child.stderr).toBe("");
expect(await readFile(join(directory, "dist/mcp-catalog.json"), "utf8")).toBe(
await readFile("dist/mcp-catalog.json", "utf8"),
);
expect(
await readFile(join(directory, "dist/recordedMcpCatalog.js"), "utf8"),
).toBe(await readFile("dist/recordedMcpCatalog.js", "utf8"));
expect(await readdir(temporary)).toEqual([]);
});
it.each([
{ failures: ["client"], producerFails: false },
{ failures: ["client", "server", "session"], producerFails: false },
{ failures: ["client"], producerFails: true },
{ failures: [], producerFails: true },
])(
"preserves producer failure=$producerFails and settles every recorder owner with cleanup failures=$failures",
async ({ failures, producerFails }) => {
const directory = await copyCompiledPackage("rea-catalog-cleanup-");
await cp(
resolve("scripts/record-mcp-catalog.mjs"),
join(directory, "scripts/record-mcp-catalog.mjs"),
);
const log = join(directory, "cleanup.jsonl");
const temporary = join(directory, "temporary");
await mkdir(temporary);
// Fault injection tests recorder ownership, not provider or SDK behavior.
const close = (owner: string) => `async () => {
const { appendFileSync } = await import("node:fs");
appendFileSync(${JSON.stringify(log)}, JSON.stringify(${JSON.stringify(owner)}) + "\\n");
if (${JSON.stringify(failures)}.includes(${JSON.stringify(owner)}))
throw new Error(${JSON.stringify(`${owner} cleanup failed`)});
return { ok: true };
}`;
const replacements = {
"/dist/server/createServer.js": `export const createServer = () => ({ connect: async () => {}, close: ${close("server")} });`,
"/dist/composition/binary.js": `export const createBinarySession = () => ({ close: ${close("session")} });`,
"/dist/config/parseConfig.js":
"export const parseConfig = () => ({ ok: true, value: {} });",
"/dist/logger.js": "export const silentLogger = {};",
"/dist/contracts/promptContracts.js":
"export const PROMPT_CONTRACTS = [];",
};
const clientModule = `data:text/javascript,${encodeURIComponent(`
export class Client {
async connect() {}
async listTools() {
if (${JSON.stringify(producerFails)}) throw new Error("catalog generation failed");
return { tools: [] };
}
async listPrompts() { return { prompts: [] }; }
getServerVersion() {}
getServerCapabilities() {}
getInstructions() {}
close = ${close("client")};
}
export const InMemoryTransport = { createLinkedPair: () => [null, null] };
`)}`;
const hook = `data:text/javascript,${encodeURIComponent(`
import { registerHooks } from "node:module";
const replacements = ${JSON.stringify(replacements)};
registerHooks({
resolve(specifier, context, next) {
return specifier === "@modelcontextprotocol/client"
? { url: ${JSON.stringify(clientModule)}, shortCircuit: true }
: next(specifier, context);
},
load(url, context, next) {
const replacement = Object.entries(replacements).find(([path]) => url.endsWith(path));
return replacement === undefined ? next(url, context)
: { source: replacement[1], format: "module", shortCircuit: true };
},
});
`)}`;
const child = spawnSync(
process.execPath,
[`--import=${hook}`, join(directory, "scripts/record-mcp-catalog.mjs")],
{
cwd: directory,
env: {
...process.env,
TMPDIR: temporary,
TMP: temporary,
TEMP: temporary,
},
encoding: "utf8",
timeout: 10000,
},
);
expect(child.error).toBeUndefined();
expect(child.status).toBe(1);
expect(
new Set(
(await readFile(log, "utf8"))
.trim()
.split("\n")
.map((line): unknown => JSON.parse(line)),
),
).toEqual(new Set(["client", "server", "session"]));
for (const owner of failures)
expect(child.stderr).toContain(`${owner} cleanup failed`);
if (producerFails) {
expect(child.stderr).toContain("Error: catalog generation failed");
if (failures.length > 0)
expect(
child.stderr.indexOf("Error: catalog generation failed"),
).toBeLessThan(child.stderr.indexOf(`${failures[0]} cleanup failed`));
}
if (failures.length > 0)
expect(child.stderr).toContain("AggregateError: Catalog cleanup failed");
else expect(child.stderr).not.toContain("AggregateError");
expect(await readdir(temporary)).toHaveLength(failures.length > 0 ? 1 : 0);
},
);
it("rejects a corrupted build-owned catalog before starting the connection", async () => {
const directory = await copyCompiledPackage("rea-corrupted-mcp-catalog-");
await writeFile(join(directory, "dist/mcp-catalog.json"), "{}");
const child = spawnSync(
process.execPath,
[join(directory, "scripts/rea.mjs"), "mcp"],
{ cwd: directory, input: "", encoding: "utf8", timeout: 10000 },
);
expect(child.error).toBeUndefined();
expect(child.status).toBe(1);
expect(child.stdout).toBe("");
expect(child.stderr).toBe(
"REA could not start. Run `rea doctor`, then restart REA from your MCP client.\n",
);
});