test(cli): pre-set NODE_OPTIONS heap cap so ensureHeap can't re-exec (#2264 CI)

Root cause of the ubuntu-coverage-only failure (3/3 CI runs, passing locally):
analyzeCommand calls ensureHeap() (analyze.ts:715), which RE-EXECS the process —
spawning `node <heap-flags> <argv>` with vitest's argv — unless NODE_OPTIONS
already carries --max-old-space-size (analyze.ts:498). That re-exec killed the
forked vitest worker ("Worker exited unexpectedly" → the assertion never ran).
It only reproduced on the memory-constrained CI runner because locally a high V8
heap-size-limit also short-circuits ensureHeap (analyze.ts:501).

Reproduced locally with NODE_OPTIONS="--no-warnings" (no heap cap) → same failure;
fixed by pre-setting --max-old-space-size in beforeAll (restored in afterAll), the
same workaround cli-e2e uses. Verified: passes under the repro condition, normally,
and with --coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JBJomjoTdBV2eveDVq4JMm
This commit is contained in:
Gergo Magyar 2026-06-21 11:40:06 +00:00
parent e404beea18
commit bb0c59209b

View file

@ -85,8 +85,18 @@ describe('analyzeCommand — finalize-failure must terminate, not hang (#2264 P1
const baselineUnhandled = process.listeners('unhandledRejection');
const baselineUncaught = process.listeners('uncaughtException');
let exitSpy: MockInstance<typeof process.exit>;
let savedNodeOptions: string | undefined;
beforeAll(() => {
// analyzeCommand calls ensureHeap(), which RE-EXECS the process — spawning
// `node <heap-flags> <argv>` where argv is vitest's, killing the forked
// worker — UNLESS NODE_OPTIONS already carries a heap cap (analyze.ts:498).
// Locally a high V8 heap-size-limit also short-circuits it (analyze.ts:501),
// which is why this only crashed on the memory-constrained CI runner. Pre-set
// the cap so ensureHeap returns early — the same workaround cli-e2e uses
// (#2264 CI). Restored in afterAll so a reused worker's later files are clean.
savedNodeOptions = process.env.NODE_OPTIONS;
process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --max-old-space-size=8192`.trim();
// Mock process.exit for the WHOLE file — a fatal handler firing between tests
// (after a per-test spy would have been restored) can't really exit.
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
@ -105,6 +115,7 @@ describe('analyzeCommand — finalize-failure must terminate, not hang (#2264 P1
.filter((l) => !baselineUncaught.includes(l))
.forEach((l) => process.removeListener('uncaughtException', l));
exitSpy.mockRestore();
process.env.NODE_OPTIONS = savedNodeOptions ?? '';
process.exitCode = 0;
});