Files
rea/tests/boundary/cli/compiledCliSession.test.ts
T
71b696582b perf(test): reuse one compiled CLI for export-binding parity (#5679)
* perf(test): reuse one compiled CLI for export-binding parity

The slow export-binding cases each spawned the compiled CLI three times.
One warm process now serves those JSON commands from a single command
instance, so startup and retained registrations are paid once per file.

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

* style(test): format the warm CLI session

oxfmt --check rejected the session and its worker.

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

* fix(test): report warm CLI shutdown, deadlines, and exit status

A compiled CLI session that needed a signal after stdin closed reported
success. A command that never answered left later checks waiting, and a
failure recorded only on process.exitCode was returned as status 0.

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

* fix(test): close the warm CLI worker when a status check fails

The failure-then-success case only closed the compiled worker after both
assertions passed. Register the close with the test so a failed check still
stops the child.

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

* fix(test): retain a timed-out compiled CLI worker until it exits

A request deadline cleared the child before SIGKILL was observed. The next
command could start a replacement, and close could miss the original process.
Retired workers stay owned until exit, and a failed stop remains for a later
close to retry.

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

* fix(test): reject a compiled CLI launch that close interrupts

Closing during the ready handshake detached the child before the launch
promise settled. The queued command then waited on the startup timer.
Close now rejects that startup before the child is detached.

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 12:57:04 +08:00

267 lines
8.6 KiB
TypeScript

import { EventEmitter, PassThrough } from "node:stream";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, onTestFinished, test } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { startCompiledCliSession } from "../../support/cli/compiledCliSession.js";
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 ready = (child: FakeCliProcess): void => {
child.stdout.write(`${JSON.stringify({ ready: true })}\n`);
};
const exitWhenStdinCloses = (child: FakeCliProcess): void => {
child.stdin.on("finish", () => {
if (child.exitCode !== null || child.signalCode !== null) return;
child.exitCode = 0;
child.emit("exit", 0, null);
child.emit("close", 0, null);
});
};
const replyWhenAsked = (
child: FakeCliProcess,
stdout: string,
exitCode = 0,
): void => {
let buffer = "";
child.stdin.setEncoding("utf8");
child.stdin.on("data", (chunk: string) => {
buffer += chunk;
let newline = buffer.indexOf("\n");
while (newline !== -1) {
const line = buffer.slice(0, newline);
buffer = buffer.slice(newline + 1);
if (line.length > 0) {
const request = JSON.parse(line) as { id: number };
child.stdout.write(
`${JSON.stringify({ id: request.id, exitCode, stdout })}\n`,
);
}
newline = buffer.indexOf("\n");
}
});
};
test("rejects a compiled CLI command when close wins the ready handshake", async () => {
let child: FakeCliProcess | undefined;
let releaseReady: (() => void) | undefined;
const session = startCompiledCliSession({
stopTimeoutMs: 50,
spawnWorker: () => {
const started = fakeProcess(60);
child = started;
exitWhenStdinCloses(started);
releaseReady = () => {
ready(started);
};
return asCliProcess(started);
},
});
const pending = session.run(["early"]);
for (let attempt = 0; child === undefined && attempt < 30; attempt += 1)
await new Promise((resolve) => {
setImmediate(resolve);
});
if (child === undefined) throw new Error("compiled CLI worker did not start");
const closing = session.close();
releaseReady?.();
await expect(pending).rejects.toThrow("Compiled CLI session is closed");
await closing;
});
test("rejects a compiled CLI command that never replies and serves the next one", async () => {
const children: FakeCliProcess[] = [];
const session = startCompiledCliSession({
requestTimeoutMs: 30,
spawnWorker: () => {
const child = fakeProcess(70 + children.length);
children.push(child);
exitWhenStdinCloses(child);
ready(child);
if (children.length > 1)
replyWhenAsked(child, JSON.stringify({ ok: true }));
return asCliProcess(child);
},
});
await expect(session.run(["stuck"])).rejects.toThrow(/did not respond/);
expect(children[0]?.signalCode).toBe("SIGKILL");
await expect(session.run(["next"])).resolves.toEqual({ ok: true });
expect(children).toHaveLength(2);
await session.close();
});
test("keeps a timed-out compiled CLI worker until it exits", async () => {
const children: FakeCliProcess[] = [];
let releaseExit: (() => void) | undefined;
const session = startCompiledCliSession({
requestTimeoutMs: 20,
stopTimeoutMs: 40,
spawnWorker: () => {
const child = fakeProcess(90 + children.length);
children.push(child);
if (children.length === 1) {
child.kill = () => {
releaseExit = () => {
if (child.exitCode !== null || child.signalCode !== null) return;
child.signalCode = "SIGKILL";
child.emit("exit", null, "SIGKILL");
child.emit("close", null, "SIGKILL");
};
return true;
};
} else {
exitWhenStdinCloses(child);
replyWhenAsked(child, JSON.stringify({ ok: true }));
}
ready(child);
return asCliProcess(child);
},
});
const stalled = session.run(["stuck"]);
const next = session.run(["next"]);
await expect(stalled).rejects.toThrow(/did not respond/);
expect(children).toHaveLength(1);
expect(children[0]?.signalCode).toBeNull();
releaseExit?.();
await expect(next).resolves.toEqual({ ok: true });
expect(children).toHaveLength(2);
await session.close();
});
test("retries a timed-out compiled CLI worker that survives SIGKILL", async () => {
let child: FakeCliProcess | undefined;
const signals: NodeJS.Signals[] = [];
const session = startCompiledCliSession({
requestTimeoutMs: 20,
stopTimeoutMs: 20,
spawnWorker: () => {
child = fakeProcess(91);
child.kill = (signal: NodeJS.Signals = "SIGTERM") => {
signals.push(signal);
return true;
};
ready(child);
return asCliProcess(child);
},
});
await expect(session.run(["stuck"])).rejects.toThrow(/did not respond/);
const replacement = session.run(["next"]);
await expect(replacement).rejects.toThrow(/stayed running after SIGKILL/);
await expect(session.close()).rejects.toThrow(/stayed running after SIGKILL/);
expect(signals).toEqual(["SIGKILL", "SIGKILL"]);
const stalledChild = child;
if (stalledChild === undefined) throw new Error("missing stalled worker");
stalledChild.kill = (signal: NodeJS.Signals = "SIGKILL") => {
signals.push(signal);
stalledChild.signalCode = signal;
stalledChild.emit("exit", null, signal);
stalledChild.emit("close", null, signal);
return true;
};
await session.close();
expect(stalledChild.signalCode).toBe("SIGKILL");
});
test("reports a compiled CLI session that needed a signal after stdin closed", async () => {
let child: FakeCliProcess | undefined;
const signals: NodeJS.Signals[] = [];
const session = startCompiledCliSession({
stopTimeoutMs: 20,
spawnWorker: () => {
child = fakeProcess(81);
child.kill = (signal: NodeJS.Signals = "SIGTERM") => {
signals.push(signal);
if (signal !== "SIGKILL") return true;
if (child?.exitCode !== null || child?.signalCode !== null) return true;
child.signalCode = signal;
child.emit("exit", null, signal);
child.emit("close", null, signal);
return true;
};
ready(child);
replyWhenAsked(child, JSON.stringify({ ok: true }));
return asCliProcess(child);
},
});
await expect(session.run(["once"])).resolves.toEqual({ ok: true });
await expect(session.close()).rejects.toThrow(
"Compiled CLI session did not exit after stdin closed",
);
expect(signals).toEqual(["SIGTERM", "SIGKILL"]);
expect(child?.signalCode).toBe("SIGKILL");
});
test("closes a compiled CLI session that exits after stdin closes", async () => {
const session = startCompiledCliSession({
spawnWorker: () => {
const child = fakeProcess(82);
exitWhenStdinCloses(child);
ready(child);
replyWhenAsked(child, JSON.stringify({ ok: true }));
return asCliProcess(child);
},
});
await expect(session.run(["once"])).resolves.toEqual({ ok: true });
await session.close();
});
test("keeps a compiled CLI failure status and serves the next command", async () => {
const session = startCompiledCliSession();
onTestFinished(() => session.close());
const root = await createTestTempDirectory("rea-warm-cli-status-");
await writeFile(
join(root, "app.mjs"),
"export function current() { return 1; }\n",
);
await expect(
session.run(["compare-javascript-export-shapes", "{", "--json"]),
).rejects.toThrow(/status 1:.*invalid_request/);
await expect(
session.run(["analyze-javascript-application", root, "--json"]),
).resolves.toMatchObject({
normalized_result: expect.any(Object),
});
await session.close();
});