Files
rea/tests/boundary/cli/sharedJavaScriptMcpSession.test.ts
40428da1dd perf(test): reuse one stdio MCP server per JavaScript application file (#5658)
* perf(test): reuse one stdio MCP server per JavaScript application file

Repeated analyzeJavaScriptDirectory MCP cases started a full server for
every case. Share one file-scoped server and compile cache so later cases
skip that startup while CLI cases still launch a fresh process.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(test): close in-flight shared MCP startup with the file

File teardown waits for a stdio session that is still starting, skips a
server once close has begun, and still closes one registered during that
wait. The later-case reuse test can run without the earlier case.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(test): keep a failed MCP handshake when cleanup fails

The shared session reports the startup error and carries the cleanup failure
as its cause. A cleanup that failed stays owned and is retried on close
without running a cleanup that already succeeded.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(test): fail shared MCP cleanup when the rejection has no reason

A cleanup rejection of undefined still fails the drain and stays owned.
Close caches completion only after every retained cleanup succeeds, so a
later close can release it.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(test): keep one MCP server while failed cleanup is retained

A later call retries retained cleanup before opening another server. The
new server starts only after that cleanup succeeds.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-21 03:46:14 +08:00

265 lines
8.0 KiB
TypeScript

import { expect, test } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
cliTest,
createSharedMcpSession,
type SharedMcpCall,
type TestCli,
} from "../../support/cli/cliFixture.js";
import {
analyzeJavaScriptDirectory,
resolveJavaScriptApplicationMcpCall,
} from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/workspace/writeFixtureFiles.js";
const unusedCli = {
run: async () => {
throw new Error("CLI was not requested");
},
} as TestCli;
test("prefers an explicit JavaScript MCP caller over the file session", async () => {
const explicit = (async () => ({
isError: false,
})) as unknown as SharedMcpCall;
let sharedCalls = 0;
const resolved = await resolveJavaScriptApplicationMcpCall(
{
...unusedCli,
sharedMcpCall: async () => {
sharedCalls += 1;
return explicit;
},
},
explicit,
);
expect(resolved).toBe(explicit);
expect(sharedCalls).toBe(0);
});
test("uses the file session when the case does not own a server", async () => {
const shared = (async () => ({ isError: false })) as unknown as SharedMcpCall;
const resolved = await resolveJavaScriptApplicationMcpCall(
{
...unusedCli,
sharedMcpCall: async () => shared,
},
undefined,
);
expect(resolved).toBe(shared);
});
const writeApplication = async (prefix: string): Promise<string> => {
const root = await createTestTempDirectory(prefix);
await writeFixtureFiles(root, {
"package.json": JSON.stringify({ main: "main.cjs" }),
"main.cjs":
'const { BrowserWindow } = require("electron"); new BrowserWindow().loadFile("index.html");',
"index.html": "page",
});
return root;
};
let fileSession: SharedMcpCall | undefined;
/** Record the first selected case and compare later cases when they also run. */
const useFileSession = async (cli: TestCli): Promise<SharedMcpCall> => {
const open = cli.sharedMcpCall;
if (open === undefined)
throw new Error("cliTest did not provide a shared MCP session");
const current = await open();
if (fileSession === undefined) fileSession = current;
else expect(current).toBe(fileSession);
return current;
};
const held = (): { gate: Promise<void>; release: () => void } => {
let release: () => void = () => {};
const gate = new Promise<void>((resolve) => {
release = resolve;
});
return { gate, release };
};
const fakeCall = (async () => ({
isError: false,
})) as unknown as SharedMcpCall;
test("does not open a shared MCP server after file cleanup starts", async () => {
const startup = held();
let opened = false;
const session = createSharedMcpSession(async (_registerCleanup, isClosed) => {
await startup.gate;
if (isClosed()) throw new Error("Shared MCP session is closed");
opened = true;
return fakeCall;
});
const opening = session.session.call();
const closing = session.close();
startup.release();
await closing;
await expect(opening).rejects.toThrow("Shared MCP session is closed");
expect(opened).toBe(false);
await expect(session.session.call()).rejects.toThrow(
"Shared MCP session is closed",
);
});
test("keeps a failed MCP handshake when its cleanup also fails", async () => {
const startup = new Error("handshake failed");
const cleanup = new Error("teardown failed");
let released = 0;
let retained = 0;
const session = createSharedMcpSession(async (registerCleanup) => {
registerCleanup(async () => {
retained += 1;
if (retained === 1) throw cleanup;
});
registerCleanup(async () => {
released += 1;
});
throw startup;
});
const caught = await session.session.call().then(
() => {
throw new Error("startup succeeded");
},
(error: unknown) => error,
);
expect(caught).toBeInstanceOf(Error);
if (!(caught instanceof Error)) throw new Error("expected startup failure");
expect(caught.message).toBe(startup.message);
expect(caught.cause).toBeInstanceOf(AggregateError);
const cause = caught.cause as AggregateError;
expect(cause.cause).toBe(startup);
expect([...cause.errors]).toEqual([startup, cleanup]);
expect(released).toBe(1);
expect(retained).toBe(1);
await session.close();
expect(released).toBe(1);
expect(retained).toBe(2);
});
test("retries a reasonless MCP cleanup rejection on a later close", async () => {
const startup = new Error("handshake failed");
let attempts = 0;
let fail = true;
const session = createSharedMcpSession(async (registerCleanup) => {
registerCleanup(() => {
attempts += 1;
return fail ? Promise.reject() : Promise.resolve();
});
throw startup;
});
const caught = await session.session.call().then(
() => {
throw new Error("startup succeeded");
},
(error: unknown) => error,
);
expect(caught).toBeInstanceOf(Error);
if (!(caught instanceof Error)) throw new Error("expected startup failure");
expect(caught.message).toBe(startup.message);
expect(caught.cause).toBeInstanceOf(AggregateError);
const cause = caught.cause as AggregateError;
expect(cause.cause).toBe(startup);
expect([...cause.errors]).toEqual([startup, undefined]);
await expect(session.close()).rejects.toBeUndefined();
expect(attempts).toBe(2);
fail = false;
await session.close();
expect(attempts).toBe(3);
});
test("does not open another MCP server while failed cleanup is retained", async () => {
const startup = new Error("handshake failed");
const cleanup = new Error("teardown failed");
let openings = 0;
let attempts = 0;
let failCleanup = true;
const session = createSharedMcpSession(async (registerCleanup) => {
openings += 1;
registerCleanup(async () => {
attempts += 1;
if (failCleanup) throw cleanup;
});
throw startup;
});
const first = await session.session.call().then(
() => {
throw new Error("startup succeeded");
},
(error: unknown) => error,
);
expect(first).toBeInstanceOf(Error);
if (!(first instanceof Error)) throw new Error("expected startup failure");
expect(first.message).toBe(startup.message);
expect(openings).toBe(1);
expect(attempts).toBe(1);
const blocked = await session.session.call().then(
() => {
throw new Error("startup succeeded");
},
(error: unknown) => error,
);
expect(blocked).toBe(cleanup);
expect(openings).toBe(1);
expect(attempts).toBe(2);
failCleanup = false;
const reopened = await session.session.call().then(
() => {
throw new Error("startup succeeded");
},
(error: unknown) => error,
);
expect(reopened).toBe(startup);
expect(openings).toBe(2);
expect(attempts).toBe(4);
});
test("closes a shared MCP server registered after file cleanup starts", async () => {
const startup = held();
let closedServer = false;
const session = createSharedMcpSession(async (registerCleanup) => {
await startup.gate;
registerCleanup(async () => {
closedServer = true;
});
return fakeCall;
});
const opening = session.session.call();
const closing = session.close();
startup.release();
await closing;
await expect(opening).rejects.toThrow("Shared MCP session is closed");
expect(closedServer).toBe(true);
});
cliTest(
"opens one stdio MCP session for JavaScript application cases",
async ({ cli }) => {
const first = await useFileSession(cli);
expect(await useFileSession(cli)).toBe(first);
const root = await writeApplication("rea-shared-mcp-");
const analyzed = await analyzeJavaScriptDirectory("stdio MCP", cli, root);
expect(
analyzed.graph.nodes.some(({ kind }) => kind === "electron-renderer"),
).toBe(true);
},
);
cliTest(
"reuses that stdio MCP session for a later case in the file",
async ({ cli }) => {
await useFileSession(cli);
const root = await writeApplication("rea-shared-mcp-later-");
const analyzed = await analyzeJavaScriptDirectory("stdio MCP", cli, root);
expect(
analyzed.graph.nodes.some(({ kind }) => kind === "electron-renderer"),
).toBe(true);
},
);