mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 03:45:39 +00:00
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>
This commit is contained in:
4 files changed
+473
-7
No files matched your search
@@ -0,0 +1,264 @@
|
||||
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);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user