diff --git a/scripts/ci-test-ts.test.ts b/scripts/ci-test-ts.test.ts new file mode 100644 index 000000000..2b73cb41a --- /dev/null +++ b/scripts/ci-test-ts.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from "bun:test"; +import { describeChunkFailure } from "./ci-test-ts.ts"; + +// The two ways a chunk reaches SIGKILL are indistinguishable by exit code, so +// these drive real subprocesses to produce a genuine 137 rather than asserting +// against a hand-written constant. +async function spawnExitCode(script: string): Promise { + const proc = Bun.spawn(["sh", "-c", script], { stdout: "ignore", stderr: "ignore" }); + return await proc.exited; +} + +// Re-hosts the sequential runner's failure tail: spawn, watchdog, attribute. +// `runTestCommand` itself is not injectable (it builds argv from the repo +// layout), so the decision under test is driven directly. +async function runWithWatchdog(script: string, timeoutMs: number): Promise { + const proc = Bun.spawn(["sh", "-c", script], { stdout: "ignore", stderr: "ignore" }); + let timedOut = false; + const killTimer = setTimeout(() => { + timedOut = true; + proc.kill("SIGKILL"); + }, timeoutMs); + const exitCode = await proc.exited; + clearTimeout(killTimer); + return describeChunkFailure(exitCode, timedOut); +} + +describe("describeChunkFailure", () => { + test("a real SIGKILL that the watchdog did not cause is attributed to the OOM killer", async () => { + const exitCode = await spawnExitCode("kill -9 $$"); + expect(exitCode).toBe(137); + + const message = describeChunkFailure(exitCode, false); + expect(message).toContain("OOM killer"); + expect(message).toContain("chunkSize"); + // The old wording carried no cause at all; it must not come back. + expect(message).not.toBe("failed with exit code 137"); + }); + + test("a watchdog kill is attributed to the watchdog, not to memory", async () => { + const message = await runWithWatchdog("sleep 30", 150); + expect(message).toContain("chunk watchdog"); + expect(message).toContain("OMP_TEST_CHUNK_TIMEOUT"); + expect(message).not.toContain("OOM killer"); + }); + + test("the two SIGKILL causes produce different messages from the same exit code", async () => { + const oomKilled = describeChunkFailure(137, false); + const watchdogKilled = describeChunkFailure(137, true); + expect(oomKilled).not.toBe(watchdogKilled); + }); + + test("an ordinary test failure keeps the plain wording", async () => { + const exitCode = await spawnExitCode("exit 1"); + expect(exitCode).toBe(1); + expect(describeChunkFailure(exitCode, false)).toBe("failed with exit code 1"); + }); + + test("a bun crash exit keeps the plain wording so the retry log still reads naturally", () => { + expect(describeChunkFailure(134, false)).toBe("failed with exit code 134"); + expect(describeChunkFailure(139, false)).toBe("failed with exit code 139"); + }); + + test("the watchdog message reports the configured timeout", () => { + const previous = Bun.env.OMP_TEST_CHUNK_TIMEOUT; + Bun.env.OMP_TEST_CHUNK_TIMEOUT = "42"; + try { + expect(describeChunkFailure(137, true)).toContain("42s"); + } finally { + if (previous === undefined) delete Bun.env.OMP_TEST_CHUNK_TIMEOUT; + else Bun.env.OMP_TEST_CHUNK_TIMEOUT = previous; + } + }); +});