* perf(test): reuse one compiled CLI process per JavaScript application file analyzeJavaScriptDirectory CLI cases were still starting a fresh process after stdio MCP reuse. Keep one compiled process for the same child environment so later cases skip Node and CLI startup. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(test): bound shared CLI worker lifetime Retire a worker that reports a fatal failure before resolving that command, and wait for it to exit before the next command starts. Join a session frame once its bytes are complete instead of copying the unfinished buffer on every chunk. Restart the shared process after 32 commands or a reported 1 GiB resident set so one file does not keep a single heap high-water mark. Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(test): retire a shared CLI worker after a transport failure A malformed session frame rejected the command and sent SIGTERM without clearing the worker. signalCode stays unset until the process exits, so the next queued command could write to that same process. Transport failures now use the fatal-reply retirement path and the queue waits for that exit. 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>
578 lines
19 KiB
TypeScript
578 lines
19 KiB
TypeScript
import { EventEmitter, PassThrough } from "node:stream";
|
|
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
|
|
import { expect, test } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import {
|
|
cliTest,
|
|
type CliInvocation,
|
|
type CliResult,
|
|
} from "../../support/cli/cliFixture.js";
|
|
import { createCliCommandSession } from "../../support/cli/cliCommandSession.js";
|
|
import {
|
|
createCliSessionFrameReader,
|
|
encodeCliSessionFrame,
|
|
MAX_CLI_SESSION_FRAME_BYTES,
|
|
} from "../../support/cli/cliCommandSessionFrames.mjs";
|
|
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
|
import { writeFixtureFiles } from "../../support/workspace/writeFixtureFiles.js";
|
|
|
|
const requestSchema = z.object({
|
|
id: z.number(),
|
|
arguments: z.array(z.string()),
|
|
});
|
|
|
|
const command = (
|
|
arguments_: readonly string[],
|
|
environment: Readonly<Record<string, string>>,
|
|
): CliInvocation & {
|
|
cwd: string;
|
|
environment: Readonly<Record<string, string>>;
|
|
} => ({
|
|
arguments: arguments_,
|
|
cwd: process.cwd(),
|
|
environment,
|
|
});
|
|
|
|
interface FakeCliProcess extends EventEmitter {
|
|
stdin: PassThrough;
|
|
stdout: PassThrough;
|
|
stderr: PassThrough;
|
|
pid: number;
|
|
exitCode: number | null;
|
|
signalCode: NodeJS.Signals | null;
|
|
kill(signal?: NodeJS.Signals): boolean;
|
|
}
|
|
|
|
const fakeProcess = (pid: number): FakeCliProcess => {
|
|
const child = new EventEmitter() as FakeCliProcess;
|
|
child.stdin = new PassThrough();
|
|
child.stdout = new PassThrough();
|
|
child.stderr = new PassThrough();
|
|
child.pid = pid;
|
|
child.exitCode = null;
|
|
child.signalCode = null;
|
|
child.kill = (signal: NodeJS.Signals = "SIGTERM") => {
|
|
if (child.exitCode !== null || child.signalCode !== null) return true;
|
|
child.signalCode = signal;
|
|
child.emit("exit", null, signal);
|
|
child.emit("close", null, signal);
|
|
return true;
|
|
};
|
|
return child;
|
|
};
|
|
|
|
const asCliProcess = (child: FakeCliProcess): ChildProcessWithoutNullStreams =>
|
|
child as unknown as ChildProcessWithoutNullStreams;
|
|
|
|
const waitFor = async (ready: () => boolean): Promise<void> => {
|
|
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
if (ready()) return;
|
|
await new Promise((resolve) => {
|
|
setImmediate(resolve);
|
|
});
|
|
}
|
|
throw new Error("shared CLI process did not reach the expected state");
|
|
};
|
|
|
|
test("reads a CLI session frame split across chunks", () => {
|
|
const frames: unknown[] = [];
|
|
const reader = createCliSessionFrameReader((frame) => {
|
|
frames.push(frame);
|
|
});
|
|
const encoded = encodeCliSessionFrame({ ready: true, seq: 1 });
|
|
const second = encodeCliSessionFrame({ id: 2 });
|
|
const combined = Buffer.concat([encoded, second]);
|
|
reader(combined.subarray(0, 1));
|
|
expect(frames).toEqual([]);
|
|
reader(combined.subarray(1, 3));
|
|
expect(frames).toEqual([]);
|
|
reader(combined.subarray(3));
|
|
expect(frames).toEqual([{ ready: true, seq: 1 }, { id: 2 }]);
|
|
});
|
|
|
|
test("rejects an oversized CLI session frame", () => {
|
|
const reader = createCliSessionFrameReader(() => undefined);
|
|
const header = Buffer.alloc(4);
|
|
header.writeUInt32BE(MAX_CLI_SESSION_FRAME_BYTES + 1);
|
|
expect(() => reader(header)).toThrow(/limit/);
|
|
});
|
|
|
|
test("reads a CLI session frame delivered one byte at a time", () => {
|
|
const frames: unknown[] = [];
|
|
const reader = createCliSessionFrameReader((frame) => {
|
|
frames.push(frame);
|
|
});
|
|
const encoded = encodeCliSessionFrame({
|
|
id: 7,
|
|
note: "x".repeat(64),
|
|
});
|
|
for (const byte of encoded) reader(Buffer.from([byte]));
|
|
expect(frames).toEqual([{ id: 7, note: "x".repeat(64) }]);
|
|
});
|
|
|
|
test("stops an unready shared CLI process when the session closes", async () => {
|
|
let spawned: FakeCliProcess | undefined;
|
|
const session = createCliCommandSession({
|
|
spawnWorker: () => {
|
|
spawned = fakeProcess(41);
|
|
return asCliProcess(spawned);
|
|
},
|
|
});
|
|
const pending = session.run(
|
|
command(["--version"], { PATH: "/usr/bin", REA_LOG_LEVEL: "silent" }),
|
|
);
|
|
await waitFor(() => spawned !== undefined);
|
|
await session.close();
|
|
await expect(pending).rejects.toThrow("Shared CLI session is closed");
|
|
expect(spawned?.signalCode).toBe("SIGTERM");
|
|
await expect(
|
|
session.run(
|
|
command(["--version"], { PATH: "/usr/bin", REA_LOG_LEVEL: "silent" }),
|
|
),
|
|
).rejects.toThrow("Shared CLI session is closed");
|
|
await session.close();
|
|
});
|
|
|
|
test("stops an in-flight shared CLI command when the session closes", async () => {
|
|
let received = false;
|
|
let spawned: FakeCliProcess | undefined;
|
|
const session = createCliCommandSession({
|
|
spawnWorker: () => {
|
|
const child = fakeProcess(42);
|
|
spawned = child;
|
|
child.stdout.write(encodeCliSessionFrame({ ready: true }));
|
|
child.stdin.on("data", () => {
|
|
received = true;
|
|
});
|
|
return asCliProcess(child);
|
|
},
|
|
});
|
|
const pending = session.run(
|
|
command(["--version"], { PATH: "/usr/bin", REA_LOG_LEVEL: "silent" }),
|
|
);
|
|
await waitFor(() => received);
|
|
await session.close();
|
|
await expect(pending).rejects.toThrow("Shared CLI session is closed");
|
|
expect(spawned?.signalCode).toBe("SIGTERM");
|
|
});
|
|
|
|
test("reuses one process and restarts it when the environment changes", async () => {
|
|
let spawns = 0;
|
|
const session = createCliCommandSession({
|
|
spawnWorker: (environment) => {
|
|
spawns += 1;
|
|
const child = fakeProcess(50 + spawns);
|
|
const reader = createCliSessionFrameReader((value) => {
|
|
const request = requestSchema.parse(value);
|
|
child.stdout.write(
|
|
encodeCliSessionFrame({
|
|
id: request.id,
|
|
exitCode: 0,
|
|
stdout: request.arguments.join(" "),
|
|
stderr: environment.MARKER ?? "",
|
|
}),
|
|
);
|
|
});
|
|
child.stdout.write(encodeCliSessionFrame({ ready: true }));
|
|
child.stdin.on("data", (chunk) => {
|
|
reader(chunk);
|
|
});
|
|
return asCliProcess(child);
|
|
},
|
|
});
|
|
const first = await session.run(
|
|
command(["one"], { PATH: "a", MARKER: "first" }),
|
|
);
|
|
const second = await session.run(
|
|
command(["two"], { PATH: "a", MARKER: "first" }),
|
|
);
|
|
expect(spawns).toBe(1);
|
|
expect(session.processId()).toBe(51);
|
|
expect(first.stdout).toBe("one");
|
|
expect(second.stdout).toBe("two");
|
|
expect(first.stderr).toBe("first");
|
|
expect(second.stderr).toBe("first");
|
|
|
|
const changed = await session.run(
|
|
command(["three"], { PATH: "b", MARKER: "second" }),
|
|
);
|
|
expect(spawns).toBe(2);
|
|
expect(session.processId()).toBe(52);
|
|
expect(changed.stdout).toBe("three");
|
|
expect(changed.stderr).toBe("second");
|
|
await session.close();
|
|
expect(session.processId()).toBeUndefined();
|
|
});
|
|
|
|
const reply = (
|
|
child: FakeCliProcess,
|
|
id: number,
|
|
fields: {
|
|
exitCode?: number;
|
|
stdout?: string;
|
|
stderr?: string;
|
|
rss?: number;
|
|
fatal?: boolean;
|
|
},
|
|
): void => {
|
|
child.stdout.write(
|
|
encodeCliSessionFrame({
|
|
id,
|
|
exitCode: fields.exitCode ?? 0,
|
|
stdout: fields.stdout ?? "",
|
|
stderr: fields.stderr ?? "",
|
|
...(fields.rss === undefined ? {} : { rss: fields.rss }),
|
|
...(fields.fatal === undefined ? {} : { fatal: fields.fatal }),
|
|
}),
|
|
);
|
|
};
|
|
|
|
const respondingProcess = (
|
|
pid: number,
|
|
onRequest: (
|
|
request: { id: number; arguments: string[] },
|
|
child: FakeCliProcess,
|
|
) => void,
|
|
): FakeCliProcess => {
|
|
const child = fakeProcess(pid);
|
|
child.stdout.write(encodeCliSessionFrame({ ready: true }));
|
|
const reader = createCliSessionFrameReader((value) => {
|
|
onRequest(requestSchema.parse(value), child);
|
|
});
|
|
child.stdin.on("data", (chunk: Buffer) => {
|
|
reader(chunk);
|
|
});
|
|
return child;
|
|
};
|
|
|
|
test("starts another process after the shared CLI process exits", async () => {
|
|
const children: FakeCliProcess[] = [];
|
|
const session = createCliCommandSession({
|
|
spawnWorker: () => {
|
|
const child = fakeProcess(70 + children.length);
|
|
children.push(child);
|
|
child.stdout.write(encodeCliSessionFrame({ ready: true }));
|
|
if (children.length === 1) {
|
|
child.stdin.on("data", () => {
|
|
child.kill("SIGTERM");
|
|
});
|
|
} else {
|
|
const reader = createCliSessionFrameReader((value) => {
|
|
const request = requestSchema.parse(value);
|
|
child.stdout.write(
|
|
encodeCliSessionFrame({
|
|
id: request.id,
|
|
exitCode: 0,
|
|
stdout: "recovered",
|
|
stderr: "",
|
|
}),
|
|
);
|
|
});
|
|
child.stdin.on("data", (chunk) => {
|
|
reader(chunk);
|
|
});
|
|
}
|
|
return asCliProcess(child);
|
|
},
|
|
});
|
|
await expect(
|
|
session.run(command(["fail"], { PATH: "/usr/bin" })),
|
|
).rejects.toThrow(/Shared CLI process exited/);
|
|
await expect(
|
|
session.run(command(["again"], { PATH: "/usr/bin" })),
|
|
).resolves.toMatchObject({
|
|
exitCode: 0,
|
|
stdout: "recovered",
|
|
});
|
|
expect(children).toHaveLength(2);
|
|
await session.close();
|
|
});
|
|
|
|
test("retires a fatal worker before starting the next command", async () => {
|
|
const events: string[] = [];
|
|
const session = createCliCommandSession({
|
|
spawnWorker: () => {
|
|
const generation = events.filter((event) =>
|
|
event.startsWith("spawn"),
|
|
).length;
|
|
events.push(`spawn:${generation}`);
|
|
const child = respondingProcess(80 + generation, (request, current) => {
|
|
events.push(`command:${generation}`);
|
|
if (generation === 0) {
|
|
reply(current, request.id, {
|
|
exitCode: 1,
|
|
stderr: "boom",
|
|
fatal: true,
|
|
});
|
|
return;
|
|
}
|
|
reply(current, request.id, { stdout: "next" });
|
|
});
|
|
const stop = child.kill.bind(child);
|
|
child.kill = (signal?: NodeJS.Signals) => {
|
|
events.push(`kill:${generation}`);
|
|
return stop(signal);
|
|
};
|
|
return asCliProcess(child);
|
|
},
|
|
});
|
|
const fatal = session.run(command(["fail"], { PATH: "/usr/bin" }));
|
|
const next = session.run(command(["next"], { PATH: "/usr/bin" }));
|
|
await expect(fatal).resolves.toMatchObject({ exitCode: 1, stderr: "boom" });
|
|
await expect(next).resolves.toMatchObject({ exitCode: 0, stdout: "next" });
|
|
expect(events).toEqual([
|
|
"spawn:0",
|
|
"command:0",
|
|
"kill:0",
|
|
"spawn:1",
|
|
"command:1",
|
|
]);
|
|
await session.close();
|
|
});
|
|
|
|
test("retires a transport failure before the next command reuses the worker", async () => {
|
|
const events: string[] = [];
|
|
const session = createCliCommandSession({
|
|
spawnWorker: () => {
|
|
const generation = events.filter((event) =>
|
|
event.startsWith("spawn"),
|
|
).length;
|
|
events.push(`spawn:${generation}`);
|
|
const child = respondingProcess(120 + generation, (request, current) => {
|
|
events.push(`command:${generation}`);
|
|
if (generation === 0) {
|
|
current.stdout.write(encodeCliSessionFrame({ id: request.id }));
|
|
return;
|
|
}
|
|
reply(current, request.id, { stdout: "recovered" });
|
|
});
|
|
const stop = child.kill.bind(child);
|
|
child.kill = (signal: NodeJS.Signals = "SIGTERM") => {
|
|
events.push(`kill:${generation}`);
|
|
// Leave signalCode unset until a later turn. A synchronous SIGTERM
|
|
// does not update it, so reuse cannot depend on that field.
|
|
queueMicrotask(() => {
|
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
stop(signal);
|
|
});
|
|
return true;
|
|
};
|
|
return asCliProcess(child);
|
|
},
|
|
});
|
|
const failed = session.run(command(["bad"], { PATH: "/usr/bin" }));
|
|
const next = session.run(command(["next"], { PATH: "/usr/bin" }));
|
|
await expect(failed).rejects.toThrow(/exit code/);
|
|
await expect(next).resolves.toMatchObject({ stdout: "recovered" });
|
|
expect(events).toEqual([
|
|
"spawn:0",
|
|
"command:0",
|
|
"kill:0",
|
|
"spawn:1",
|
|
"command:1",
|
|
]);
|
|
await session.close();
|
|
});
|
|
|
|
test("restarts the shared CLI process after the command limit", async () => {
|
|
const children: FakeCliProcess[] = [];
|
|
const session = createCliCommandSession({
|
|
commandLimit: 2,
|
|
spawnWorker: () => {
|
|
const child = respondingProcess(
|
|
90 + children.length,
|
|
(request, current) => {
|
|
reply(current, request.id, {
|
|
stdout: String(children.indexOf(current)),
|
|
});
|
|
},
|
|
);
|
|
children.push(child);
|
|
return asCliProcess(child);
|
|
},
|
|
});
|
|
const first = await session.run(command(["one"], { PATH: "/usr/bin" }));
|
|
const second = await session.run(command(["two"], { PATH: "/usr/bin" }));
|
|
expect(children).toHaveLength(1);
|
|
expect(first.stdout).toBe("0");
|
|
expect(second.stdout).toBe("0");
|
|
const third = await session.run(command(["three"], { PATH: "/usr/bin" }));
|
|
expect(children).toHaveLength(2);
|
|
expect(children[0]?.signalCode).toBe("SIGTERM");
|
|
expect(third.stdout).toBe("1");
|
|
expect(session.processId()).toBe(91);
|
|
await session.close();
|
|
});
|
|
|
|
test("restarts the shared CLI process after the resident-set limit", async () => {
|
|
const children: FakeCliProcess[] = [];
|
|
const session = createCliCommandSession({
|
|
commandLimit: 100,
|
|
residentLimitBytes: 100,
|
|
spawnWorker: () => {
|
|
const generation = children.length;
|
|
const child = respondingProcess(100 + generation, (request, current) => {
|
|
reply(current, request.id, {
|
|
stdout: String(generation),
|
|
rss: generation === 0 ? 150 : 10,
|
|
});
|
|
});
|
|
children.push(child);
|
|
return asCliProcess(child);
|
|
},
|
|
});
|
|
await expect(
|
|
session.run(command(["one"], { PATH: "/usr/bin" })),
|
|
).resolves.toMatchObject({ stdout: "0" });
|
|
expect(session.processId()).toBe(100);
|
|
await expect(
|
|
session.run(command(["two"], { PATH: "/usr/bin" })),
|
|
).resolves.toMatchObject({ stdout: "1" });
|
|
expect(children[0]?.signalCode).toBe("SIGTERM");
|
|
expect(session.processId()).toBe(101);
|
|
await session.close();
|
|
});
|
|
|
|
test("starts a new process after the real worker reports a fatal failure", async () => {
|
|
const session = createCliCommandSession();
|
|
const failed = await session.run({
|
|
arguments: ["--version"],
|
|
cwd: "/workspace/this-directory-does-not-exist",
|
|
environment: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" },
|
|
});
|
|
expect(failed.exitCode).toBe(1);
|
|
expect(failed.stderr).toMatch(/ENOENT/);
|
|
const recovered = await session.run({
|
|
arguments: ["--version"],
|
|
cwd: process.cwd(),
|
|
environment: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" },
|
|
});
|
|
expect(recovered.exitCode).toBe(0);
|
|
expect(recovered.stdout.length).toBeGreaterThan(0);
|
|
expect(session.processId()).toEqual(expect.any(Number));
|
|
await session.close();
|
|
});
|
|
|
|
const requireSharedCli = (cli: {
|
|
sharedCliRun?: (invocation: CliInvocation) => Promise<CliResult>;
|
|
}): ((invocation: CliInvocation) => Promise<CliResult>) => {
|
|
const run = cli.sharedCliRun;
|
|
if (run === undefined)
|
|
throw new Error("cliTest did not provide a shared CLI session");
|
|
return run;
|
|
};
|
|
|
|
const normalized = (result: CliResult) =>
|
|
z
|
|
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
|
.parse(result.json).normalized_result;
|
|
|
|
const analyzeArguments = (root: string): readonly string[] => [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--format",
|
|
"json",
|
|
];
|
|
|
|
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;
|
|
};
|
|
|
|
cliTest(
|
|
"matches a fresh CLI process and reuses that process for the same environment",
|
|
async ({ cli, cliCommandSession }) => {
|
|
const sharedCliRun = requireSharedCli(cli);
|
|
const fresh = await cli.run({
|
|
arguments: ["--version"],
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
const shared = await sharedCliRun({
|
|
arguments: ["--version"],
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
expect(shared.exitCode).toBe(fresh.exitCode);
|
|
expect(shared.stdout).toBe(fresh.stdout);
|
|
expect(shared.stderr).toBe(fresh.stderr);
|
|
const pid = cliCommandSession.processId();
|
|
expect(pid).toEqual(expect.any(Number));
|
|
const again = await sharedCliRun({
|
|
arguments: ["--version"],
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
expect(again.stdout).toBe(fresh.stdout);
|
|
expect(cliCommandSession.processId()).toBe(pid);
|
|
|
|
const freshInvalid = await cli.run({
|
|
arguments: ["--version"],
|
|
environment: { REA_LOG_LEVEL: "not-a-level" },
|
|
});
|
|
const sharedInvalid = await sharedCliRun({
|
|
arguments: ["--version"],
|
|
environment: { REA_LOG_LEVEL: "not-a-level" },
|
|
});
|
|
expect(sharedInvalid.exitCode).toBe(freshInvalid.exitCode);
|
|
expect(sharedInvalid.exitCode).not.toBe(0);
|
|
expect(sharedInvalid.stdout).toBe(freshInvalid.stdout);
|
|
expect(sharedInvalid.stderr).toBe(freshInvalid.stderr);
|
|
expect(cliCommandSession.processId()).not.toBe(pid);
|
|
|
|
const restored = await sharedCliRun({
|
|
arguments: ["--version"],
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
expect(restored.exitCode).toBe(0);
|
|
expect(restored.stdout).toBe(fresh.stdout);
|
|
expect(cliCommandSession.processId()).not.toBe(pid);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"reuses one CLI process across JavaScript application directories",
|
|
async ({ cli, cliCommandSession }) => {
|
|
const sharedCliRun = requireSharedCli(cli);
|
|
const firstRoot = await writeApplication("rea-shared-cli-");
|
|
const fresh = await cli.run({
|
|
arguments: analyzeArguments(firstRoot),
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
const shared = await sharedCliRun({
|
|
arguments: analyzeArguments(firstRoot),
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
expect(shared.exitCode).toBe(0);
|
|
expect(normalized(shared)).toEqual(normalized(fresh));
|
|
const pid = cliCommandSession.processId();
|
|
expect(pid).toEqual(expect.any(Number));
|
|
|
|
const secondRoot = await writeApplication("rea-shared-cli-next-");
|
|
const freshNext = await cli.run({
|
|
arguments: analyzeArguments(secondRoot),
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
const sharedNext = await sharedCliRun({
|
|
arguments: analyzeArguments(secondRoot),
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
expect(normalized(sharedNext)).toEqual(normalized(freshNext));
|
|
expect(normalized(sharedNext).input_path).not.toBe(
|
|
normalized(shared).input_path,
|
|
);
|
|
expect(cliCommandSession.processId()).toBe(pid);
|
|
|
|
const analyzed = await analyzeJavaScriptDirectory("CLI", cli, secondRoot);
|
|
expect(analyzed.input_path).toBe(normalized(freshNext).input_path);
|
|
expect(cliCommandSession.processId()).toBe(pid);
|
|
},
|
|
);
|