Files
rea/tests/boundary/mcp/mainErrors.test.ts
T
Edgar Arakelyanandgensokyo-dev 60bd646478 feat(mcp): add opt-in streamable HTTP transport (#4955)
Add an explicit Streamable HTTP entry for environment/gateway hosts while keeping stdio registrations and default behavior. Preserve the shared provider/session owner across DELETE and replacement; fence and drain tool/completion work before handover or teardown.

Bound HTTP input and plain-response admission, cancel refused bodies, join whole exchanges and asynchronous response cleanup before provider hooks, gate startup readiness, and reject queued HTTP/1.1 pipelining while retaining sequential keep-alive.

Validation: 8 static/build tasks, 83 adapter tests and registry metadata pass. A real installed gh binary produces seven equivalent CLI/stdio/HTTP reports with DELETE/replacement and clean signal shutdown. Broader local native-provider/ownership failures reproduce on clean main and are documented with limitations in the PR.

Co-authored-by: Edgar Arakelyan <edgar.arakelyan@scale.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-19 23:58:51 +08:00

226 lines
6.5 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
import { run, runEntrypoint } from "../../../src/main.js";
import { MCP_HTTP_LISTEN_FAILED } from "../../../src/main/messages.js";
import type { McpServeOptions } from "../../../src/main/types.js";
import { StreamableHttpListenError } from "../../../src/server/http/streamableHttpListenError.js";
type RuntimeDependencies = NonNullable<Parameters<typeof run>[0]>;
const nextTurn = (): Promise<void> =>
new Promise((resolve) => setImmediate(resolve));
const dependencies = ({
serve,
output,
shutdown,
exitCodes,
shutdownUnregistrations = [],
loadOptionalProviders,
}: {
readonly serve: RuntimeDependencies["serve"];
readonly output: string[];
readonly shutdown: Array<() => void>;
readonly exitCodes: number[];
readonly shutdownUnregistrations?: string[];
readonly loadOptionalProviders?: RuntimeDependencies["loadOptionalProviders"];
}): RuntimeDependencies => ({
env: {},
serve,
writeStderr: (text) => output.push(text),
setExitCode: (code) => exitCodes.push(code),
registerShutdown: (handler) => {
shutdown.push(handler);
return () => shutdownUnregistrations.push("shutdown");
},
...(loadOptionalProviders === undefined ? {} : { loadOptionalProviders }),
});
describe("MCP runtime errors", () => {
it("imports the compiled runtime when argv[1] is a missing synthetic path", () => {
const runtimeUrl = pathToFileURL(resolve("dist/main.js")).href;
const script = [
'process.argv[1] = "/rea-test-missing-entrypoint/main.js";',
`await import(${JSON.stringify(runtimeUrl)});`,
'console.log("IMPORT_OK");',
].join("\n");
const child = spawnSync(
process.execPath,
["--input-type=module", "--eval", script],
{ cwd: process.cwd(), encoding: "utf8", timeout: 5_000 },
);
expect(child.error).toBeUndefined();
expect(child.status).toBe(0);
expect(child.stdout).toBe("IMPORT_OK\n");
expect(child.stderr).toBe("");
});
it("reports connection loss without the transport cause", async () => {
const output: string[] = [];
const shutdown: Array<() => void> = [];
const shutdownUnregistrations: string[] = [];
let options: McpServeOptions | undefined;
let closeCalls = 0;
const close = (): Promise<void> => {
closeCalls += 1;
return Promise.resolve();
};
const exitCodes: number[] = [];
expect(
await run(
dependencies({
serve: (_factory, received) => {
options = received;
return { close };
},
output,
shutdown,
exitCodes,
shutdownUnregistrations,
}),
),
).toBe(0);
options?.onerror?.(new Error("SECRET transport stack"));
expect(output).toEqual([
"REA lost its MCP client connection. Restart REA from your MCP client.\n",
]);
shutdown[0]?.();
await nextTurn();
expect(closeCalls).toBe(1);
expect(shutdownUnregistrations).toEqual(["shutdown"]);
});
it("unregisters every process-lifetime handler during idempotent shutdown", async () => {
const shutdown: Array<() => void> = [];
const unregistrations: string[] = [];
let closeCalls = 0;
const runtime: RuntimeDependencies = {
...dependencies({
serve: () => ({
close: async () => {
closeCalls += 1;
},
}),
output: [],
shutdown,
exitCodes: [],
shutdownUnregistrations: unregistrations,
}),
};
expect(await run(runtime)).toBe(0);
expect(shutdown).toHaveLength(1);
shutdown[0]?.();
shutdown[0]?.();
await nextTurn();
expect(closeCalls).toBe(1);
expect(unregistrations).toEqual(["shutdown"]);
});
it("reports transport startup failure without its cause", async () => {
const output: string[] = [];
expect(
await run(
dependencies({
serve: () => {
throw new Error("SECRET startup stack");
},
output,
shutdown: [],
exitCodes: [],
}),
),
).toBe(1);
expect(output).toEqual([
"REA could not start its MCP connection. Restart REA from your MCP client; run `rea doctor` if it fails again.\n",
]);
});
it("reports shutdown failure and sets a failing exit code", async () => {
const output: string[] = [];
const shutdown: Array<() => void> = [];
const exitCodes: number[] = [];
await run(
dependencies({
serve: () => ({
close: () => Promise.reject(new Error("SECRET close")),
}),
output,
shutdown,
exitCodes,
}),
);
shutdown[0]?.();
await nextTurn();
expect(exitCodes).toEqual([1]);
expect(output).toEqual([
"REA could not close cleanly. End the REA process before starting it again.\n",
]);
});
it("reports an unexpected entrypoint failure without its cause", async () => {
const output: string[] = [];
const exitCodes: number[] = [];
await runEntrypoint(
() => Promise.reject(new Error("SECRET entrypoint stack")),
(text) => output.push(text),
(code) => exitCodes.push(code),
);
expect(output).toEqual([
"REA could not start. Run `rea doctor`, then restart REA from your MCP client.\n",
]);
expect(exitCodes).toEqual([1]);
});
});
describe("MCP runtime HTTP transport errors", () => {
it("awaits an asynchronous transport start before installing shutdown", async () => {
const shutdown: Array<() => void> = [];
let closeCalls = 0;
expect(
await run(
dependencies({
serve: async () => ({
close: async () => {
closeCalls += 1;
},
}),
output: [],
shutdown,
exitCodes: [],
}),
),
).toBe(0);
expect(shutdown).toHaveLength(1);
shutdown[0]?.();
await nextTurn();
expect(closeCalls).toBe(1);
});
it("reports an HTTP listener that cannot bind without its cause", async () => {
const output: string[] = [];
expect(
await run(
dependencies({
serve: () =>
Promise.reject(
new StreamableHttpListenError(
new Error("SECRET EADDRINUSE 127.0.0.1:1"),
),
),
output,
shutdown: [],
exitCodes: [],
}),
),
).toBe(1);
expect(output).toEqual([`${MCP_HTTP_LISTEN_FAILED}\n`]);
});
});