From 162514d976933b1edda3c7ce32fd71b8886b3773 Mon Sep 17 00:00:00 2001 From: Musa Musa Date: Sat, 29 Aug 2026 21:08:14 +0100 Subject: [PATCH] fix: retry a stalled opossum file once before failing the build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I made test:compat a required CI check, and it is timing-sensitive. It failed on two pull requests and passed on a rerun of the same commit with no changes. A flaky required check is worse than no check, because it teaches you to ignore red — and that is a problem I introduced. The stall diagnostic added earlier is what made this diagnosable at all. Instead of "0 0 STALLED" it now prints the child's exit and output, which showed the real failure: an unhandled EOPENBREAKER rejection crashing the process at test.js:778. The race is in opossum's own test. It opens a breaker, waits exactly resetTimeout using a setTimeout of the same duration, then fires and expects a half-open probe. That is a dead heat and a loaded runner decides it; when the fire lands early the breaker is still open, and the .then() chain at that line has no .catch(), so the rejection is unhandled and the process dies. A single retry keeps the signal — a genuine break fails twice — without the noise. The report distinguishes "one attempt" from "two attempts" so a real regression still reads as one. Not a root cause, and the comment says so. Matching another library's exact timer ordering is not something a compatibility layer can guarantee from outside, and the alternative — dropping the check — loses the 362-of-362 guarantee that is the whole basis of the migration claim. --- scripts/opossum-compat.mjs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/scripts/opossum-compat.mjs b/scripts/opossum-compat.mjs index 96818ad..f80a88b 100644 --- a/scripts/opossum-compat.mjs +++ b/scripts/opossum-compat.mjs @@ -128,8 +128,33 @@ const main = async () => { execFileSync("npm", ["run", "build"], { cwd: ROOT, stdio: "ignore" }); } + // Retry a file that produced no TAP summary, once, before believing it. + // + // opossum's test.js is timing-sensitive: it opens a breaker, waits exactly + // `resetTimeout` with a setTimeout of the same duration, then fires and expects + // a half-open probe. That is a dead heat, and a loaded CI runner decides it — a + // late fire rejects with EOPENBREAKER on a `.then()` chain that has no + // `.catch()`, so the process dies and the file reports 0 of 0. + // + // Observed failing on two pull requests and passing on a rerun of the SAME + // commit with no changes. This suite is a REQUIRED check, and a flaky required + // check is worse than none: it teaches you to ignore red. A single retry keeps + // the signal (a genuine break fails twice) without the noise. + // + // This is not a root cause. The race is in opossum's test, not in the shim, and + // matching their exact timer ordering is not something a compatibility layer + // can guarantee from the outside. const results = []; - for (const f of TESTS.filter((f) => f !== "common.js")) results.push(await run(f)); + for (const f of TESTS.filter((f) => f !== "common.js")) { + let result = await run(f); + if (result.stalled) { + console.log(` ${f} produced no summary — retrying once`); + const second = await run(f); + if (!second.stalled) result = second; + else result = { ...second, retried: true }; + } + results.push(result); + } let pass = 0; let fail = 0; @@ -155,7 +180,10 @@ const main = async () => { // and the cause had to be guessed at. Print the exit status and the tail of what it actually // said. for (const r of stalls) { - console.error(`\n${r.file} produced no TAP summary (exit=${r.code} signal=${r.signal})`); + console.error( + `\n${r.file} produced no TAP summary on ${r.retried ? "two attempts" : "one attempt"}` + + ` (exit=${r.code} signal=${r.signal})`, + ); const tail = r.out.trimEnd().split("\n").slice(-12); for (const line of tail) console.error(` ${line}`); if (r.out.trim() === "") console.error(" (no output at all)");