Files
rea/tests/boundary/cli/sharedJavaScriptCliSession.test.ts
T

1025 lines
33 KiB
TypeScript

import { EventEmitter, PassThrough } from "node:stream";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { fileURLToPath } from "node:url";
import { expect, onTestFinished, 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);
const rejected = expect(pending).rejects.toThrow(
"Shared CLI session is closed",
);
const closing = session.close();
spawned?.stdout.write(encodeCliSessionFrame({ ready: true }));
await closing;
await rejected;
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);
const rejected = expect(pending).rejects.toThrow(
"Shared CLI session is closed",
);
await session.close();
await rejected;
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("recovers after a shared CLI command exceeds its selected deadline", async () => {
const children: FakeCliProcess[] = [];
const session = createCliCommandSession({
requestTimeoutMs: 20,
shutdownGraceMs: 20,
spawnWorker: () => {
const child = respondingProcess(
130 + children.length,
(request, current) => {
if (children.length > 1)
reply(current, request.id, { stdout: "recovered" });
},
);
children.push(child);
return asCliProcess(child);
},
});
await expect(
session.run(command(["stuck"], { PATH: "/usr/bin" })),
).rejects.toThrow(/did not respond/);
expect(children[0]?.signalCode).not.toBeNull();
await expect(
session.run(command(["next"], { PATH: "/usr/bin" })),
).resolves.toMatchObject({ stdout: "recovered" });
expect(children).toHaveLength(2);
await session.close();
});
test("times out a shared CLI command whose stdin write callback stays pending", async () => {
const children: FakeCliProcess[] = [];
let releaseWrite: (() => void) | undefined;
let observeWrite: (() => void) | undefined;
const writing = new Promise<void>((resolve) => {
observeWrite = resolve;
});
const session = createCliCommandSession({
requestTimeoutMs: 20,
shutdownGraceMs: 20,
spawnWorker: () => {
const child = respondingProcess(
135 + children.length,
(request, current) =>
reply(current, request.id, { stdout: "recovered" }),
);
if (children.length === 0) {
child.stdin._write = (_chunk, _encoding, callback) => {
releaseWrite = () => callback();
observeWrite?.();
};
}
children.push(child);
return asCliProcess(child);
},
});
const failed = expect(
session.run(command(["stuck"], { PATH: "/usr/bin" })),
).rejects.toThrow(/did not respond/);
await writing;
expect(releaseWrite).toEqual(expect.any(Function));
await failed;
expect(children[0]?.signalCode).not.toBeNull();
releaseWrite?.();
await expect(
session.run(command(["next"], { PATH: "/usr/bin" })),
).resolves.toMatchObject({ stdout: "recovered" });
expect(children).toHaveLength(2);
await session.close();
});
test("recovers after a shared CLI worker misses its selected readiness deadline", async () => {
const children: FakeCliProcess[] = [];
const session = createCliCommandSession({
startTimeoutMs: 20,
shutdownGraceMs: 20,
spawnWorker: () => {
const child =
children.length === 0
? fakeProcess(140)
: respondingProcess(141, (request, current) =>
reply(current, request.id, { stdout: "ready" }),
);
children.push(child);
return asCliProcess(child);
},
});
await expect(
session.run(command(["stuck"], { PATH: "/usr/bin" })),
).rejects.toThrow(/did not become ready/);
expect(children[0]?.signalCode).not.toBeNull();
await expect(
session.run(command(["next"], { PATH: "/usr/bin" })),
).resolves.toMatchObject({ stdout: "ready" });
expect(children).toHaveLength(2);
await session.close();
});
test("waits for the timed-out shared CLI worker to exit before starting its replacement", async () => {
const children: FakeCliProcess[] = [];
let releaseExit: (() => void) | undefined;
let observeStop: (() => void) | undefined;
const stopping = new Promise<void>((resolve) => {
observeStop = resolve;
});
const session = createCliCommandSession({
requestTimeoutMs: 20,
shutdownGraceMs: 100,
spawnWorker: () => {
const child = respondingProcess(
150 + children.length,
(request, current) => {
if (children.length > 1)
reply(current, request.id, { stdout: "next" });
},
);
if (children.length === 0) {
const stop = child.kill.bind(child);
child.kill = (signal: NodeJS.Signals = "SIGTERM") => {
releaseExit = () => stop(signal);
observeStop?.();
return true;
};
}
children.push(child);
return asCliProcess(child);
},
});
const stalled = expect(
session.run(command(["stuck"], { PATH: "/usr/bin" })),
).rejects.toThrow(/did not respond/);
const next = session.run(command(["next"], { PATH: "/usr/bin" }));
await stopping;
expect(children).toHaveLength(1);
releaseExit?.();
await stalled;
await expect(next).resolves.toMatchObject({ stdout: "next" });
expect(children).toHaveLength(2);
await session.close();
});
test("retains a shared CLI worker that survives SIGKILL and retries its exact owner on close", async () => {
const children: FakeCliProcess[] = [];
const signals: NodeJS.Signals[] = [];
const session = createCliCommandSession({
shutdownGraceMs: 20,
spawnWorker: () => {
const child = respondingProcess(
160 + children.length,
(request, current) => {
reply(current, request.id, {
exitCode: 1,
stderr: "fatal",
fatal: true,
});
},
);
child.kill = (signal: NodeJS.Signals = "SIGTERM") => {
signals.push(signal);
return true;
};
children.push(child);
return asCliProcess(child);
},
});
await expect(
session.run(command(["fatal"], { PATH: "/usr/bin" })),
).rejects.toThrow(/did not exit after SIGKILL/);
await expect(
session.run(command(["blocked"], { PATH: "/usr/bin" })),
).rejects.toThrow(/did not exit after SIGKILL/);
expect(children).toHaveLength(1);
await expect(session.close()).rejects.toThrow(/did not exit after SIGKILL/);
const retained = children[0];
if (retained === undefined) throw new Error("missing retained worker");
const stopsBeforeRecovery = signals.length;
retained.kill = (signal: NodeJS.Signals = "SIGTERM") => {
signals.push(signal);
retained.signalCode = signal;
retained.emit("exit", null, signal);
retained.emit("close", null, signal);
return true;
};
await session.close();
expect(signals.length).toBeGreaterThan(stopsBeforeRecovery);
expect(retained.signalCode).not.toBeNull();
expect(children).toHaveLength(1);
await session.close();
});
test.each([
{
phase: "command",
reason: /did not respond/,
},
{
phase: "startup",
reason: /did not become ready/,
},
])(
"preserves the $phase timeout when shared CLI cleanup also fails",
async ({ phase, reason }) => {
const children: FakeCliProcess[] = [];
const signals: NodeJS.Signals[] = [];
const session = createCliCommandSession({
...(phase === "command"
? { requestTimeoutMs: 20 }
: { startTimeoutMs: 20 }),
shutdownGraceMs: 20,
spawnWorker: () => {
const child =
phase === "command"
? respondingProcess(190, () => undefined)
: fakeProcess(190);
child.kill = (signal: NodeJS.Signals = "SIGTERM") => {
signals.push(signal);
return true;
};
children.push(child);
return asCliProcess(child);
},
});
const failure: unknown = await session
.run(command(["stuck"], { PATH: "/usr/bin" }))
.catch((error: unknown) => error);
expect(failure).toBeInstanceOf(AggregateError);
if (!(failure instanceof AggregateError))
throw new Error("missing combined command and cleanup failure", {
cause: failure,
});
expect(failure.message).toMatch(reason);
expect(failure.message).toMatch(/did not exit after SIGKILL/);
expect(failure.errors).toHaveLength(2);
expect(failure.errors[0]).toBeInstanceOf(Error);
expect(failure.errors[0]).toHaveProperty(
"message",
expect.stringMatching(reason),
);
expect(failure.errors[1]).toHaveProperty(
"message",
expect.stringMatching(/did not exit after SIGKILL/),
);
expect(failure.cause).toBe(failure.errors[0]);
expect(signals).toEqual(["SIGTERM", "SIGKILL"]);
await expect(
session.run(command(["blocked"], { PATH: "/usr/bin" })),
).rejects.toThrow(/did not exit after SIGKILL/);
expect(children).toHaveLength(1);
await expect(session.close()).rejects.toThrow(/did not exit after SIGKILL/);
const retained = children[0];
if (retained === undefined) throw new Error("missing retained worker");
retained.kill = (signal: NodeJS.Signals = "SIGTERM") => {
signals.push(signal);
retained.signalCode = signal;
retained.emit("exit", null, signal);
retained.emit("close", null, signal);
return true;
};
await session.close();
expect(retained.signalCode).not.toBeNull();
expect(children).toHaveLength(1);
},
);
test("confirms shared CLI shutdown after escalating an ignored SIGTERM", async () => {
let child: FakeCliProcess | undefined;
const signals: NodeJS.Signals[] = [];
const session = createCliCommandSession({
shutdownGraceMs: 20,
spawnWorker: () => {
const started = respondingProcess(170, (request, current) =>
reply(current, request.id, { stdout: "done" }),
);
const stop = started.kill.bind(started);
started.kill = (signal: NodeJS.Signals = "SIGTERM") => {
signals.push(signal);
return signal === "SIGKILL" ? stop(signal) : true;
};
child = started;
return asCliProcess(started);
},
});
await expect(
session.run(command(["once"], { PATH: "/usr/bin" })),
).resolves.toMatchObject({ stdout: "done" });
await session.close();
expect(signals).toEqual(["SIGTERM", "SIGKILL"]);
expect(child?.signalCode).toBe("SIGKILL");
await session.close();
});
test("lets the shared CLI worker finish its resource cleanup before signaling", async () => {
const events: string[] = [];
const session = createCliCommandSession({
spawnWorker: () => {
const child = respondingProcess(180, (request, current) =>
reply(current, request.id, { stdout: "done" }),
);
child.stdin.once("finish", () => {
events.push("stdin closed", "resources released");
child.exitCode = 0;
child.emit("exit", 0, null);
child.emit("close", 0, null);
});
const stop = child.kill.bind(child);
child.kill = (signal: NodeJS.Signals = "SIGTERM") => {
events.push(signal);
return stop(signal);
};
return asCliProcess(child);
},
});
await expect(
session.run(command(["once"], { PATH: "/usr/bin" })),
).resolves.toMatchObject({ stdout: "done" });
await session.close();
expect(events).toEqual(["stdin closed", "resources released"]);
await session.close();
expect(events).toHaveLength(2);
});
test("shares one shared CLI cleanup attempt across concurrent close calls", async () => {
let cleanupStarts = 0;
let releaseExit: (() => void) | undefined;
let observeCleanup: (() => void) | undefined;
const cleaning = new Promise<void>((resolve) => {
observeCleanup = resolve;
});
const signals: NodeJS.Signals[] = [];
const session = createCliCommandSession({
spawnWorker: () => {
const child = respondingProcess(185, (request, current) =>
reply(current, request.id, { stdout: "done" }),
);
child.stdin.once("finish", () => {
cleanupStarts += 1;
releaseExit = () => {
child.exitCode = 0;
child.emit("exit", 0, null);
child.emit("close", 0, null);
};
observeCleanup?.();
});
const stop = child.kill.bind(child);
child.kill = (signal: NodeJS.Signals = "SIGTERM") => {
signals.push(signal);
return stop(signal);
};
return asCliProcess(child);
},
});
await session.run(command(["once"], { PATH: "/usr/bin" }));
const first = session.close();
const second = session.close();
expect(second).toBe(first);
await cleaning;
expect(cleanupStarts).toBe(1);
expect(signals).toEqual([]);
releaseExit?.();
await Promise.all([first, second]);
await session.close();
expect(cleanupStarts).toBe(1);
expect(signals).toEqual([]);
});
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();
});
test("keeps a malformed JSON command's failure status and serves the next command", async () => {
const session = createCliCommandSession();
onTestFinished(() => session.close());
const environment = {
PATH: process.env.PATH ?? "",
REA_LOG_LEVEL: "silent",
};
const failed = await session.run(
command(["compare-javascript-export-shapes", "{", "--json"], environment),
);
expect(failed.exitCode).toBe(1);
expect(failed.stdout).toContain("invalid_request");
const worker = session.processId();
expect(worker).toEqual(expect.any(Number));
const root = await createTestTempDirectory("rea-warm-cli-status-");
await writeFixtureFiles(root, {
"app.mjs": "export function current() { return 1; }\n",
});
const recovered = await session.run(
command(["analyze-javascript-application", root, "--json"], environment),
);
expect(recovered.exitCode, recovered.stderr).toBe(0);
expect(recovered.json).toMatchObject({
normalized_result: expect.any(Object),
});
expect(session.processId()).toBe(worker);
await session.close();
});
test("closes the real JavaScript CLI worker through its resource cleanup", async () => {
let worker: ChildProcessWithoutNullStreams | undefined;
const session = createCliCommandSession({
spawnWorker: (environment) => {
worker = spawn(
process.execPath,
[
fileURLToPath(
new URL(
"../../support/cli/cliCommandSessionWorker.mjs",
import.meta.url,
),
),
],
{
cwd: process.cwd(),
env: environment,
stdio: ["pipe", "pipe", "pipe"],
},
);
return worker;
},
});
onTestFinished(() => session.close());
const root = await createTestTempDirectory("rea-warm-cli-graceful-");
await writeFixtureFiles(root, {
"app.mjs": "export function current() { return 1; }\n",
});
const analyzed = await session.run(
command(["analyze-javascript-application", root, "--json"], {
PATH: process.env.PATH ?? "",
REA_LOG_LEVEL: "silent",
}),
);
expect(analyzed.exitCode, analyzed.stderr).toBe(0);
expect(analyzed.json).toMatchObject({
normalized_result: expect.any(Object),
});
expect(worker?.exitCode).toBeNull();
expect(worker?.signalCode).toBeNull();
await session.close();
expect(worker?.exitCode).toBe(0);
expect(worker?.signalCode).toBeNull();
expect(session.processId()).toBeUndefined();
});
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);
},
);