fix(test): remove all 8 delayed process.exit teardown bombs — the tier-1 gate can finally fail

bun test runs every file in ONE process, so a 500ms setTimeout(process.exit(0))
armed in afterAll fired mid-way through a LATER file and killed the entire
suite with exit 0 and no summary — only ~16 of 434 files ran, and every
downstream failure was invisible (observed live throughout this wave's
enumeration). Changes, all guarded by fault injection:

- Replace every delayed-exit teardown with a time-boxed close of the file's
  own browser (8 files across browse/ and design/); stub the daemon
  /shutdown timer instead of letting its unconditional process.exit tear
  the runner down.
- test/no-suicide-exit.test.ts: static tripwire — no *.test.ts may schedule
  a delayed process.exit again.
- test/exit-propagation.test.ts + fixtures: fault injection with REAL bun
  output proves the truncation shape (exit 0, no summary) and that
  scripts/test-free-shards.ts now detects it: a shard exiting 0 WITHOUT
  bun's final summary line is treated as FAILED (exit code alone is not
  evidence of completion).
- handoff: the three headed-mode integration tests are darwin-skipped with
  a pointer to the known macOS headed-launch breakage (#2242/#2554); they
  keep running on Linux CI. Un-skip in the browse-daemon wave.
- feedback-roundtrip: repair the handler call sites unmasked by the fix —
  handlers take (command, args, session, bm); passing the manager where a
  session belongs broke all six tests.
- user-slug-fallback: HOME isolation makes endpoint_hash deterministic.

Fixes #2421, #2435.

Contributed by @sneakygriff (PR #2172) with repairs from @time-attack
(PR #2230 feedback-roundtrip hunks); supersedes PR #2252 by @whd4 (same
defect, credited).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-14 20:20:51 -07:00
co-authored by Claude Fable 5
parent 3f176d2226
commit e0bfc8fff5
16 changed files with 322 additions and 77 deletions
+21 -10
View File
@@ -361,16 +361,27 @@ describe("daemon /shutdown", () => {
await fetchHandler(
req("POST", `/boards/${board.id}/api/feedback`, { regenerated: false }),
);
// Now non-done count is 0 — handler should return shuttingDown:true.
// We DON'T let the real gracefulShutdown timer fire (it calls process.exit
// after 50ms which would tear down the test runner); instead we just
// observe the immediate response.
const r = await fetchHandler(req("POST", "/shutdown"));
expect(r.status).toBe(200);
const body = (await r.json()) as any;
expect(body.shuttingDown).toBe(true);
// Reset state for subsequent tests; the shutdown timer will be a no-op
// because the next resetForTest flips shuttingDown back to false.
// The handler arms setTimeout(gracefulShutdown, 50), and gracefulShutdown
// arms setTimeout(process.exit, 50). bun test runs ALL files in one
// process, so letting that exit fire would kill the whole suite ~100ms
// later (exit 0, no summary — see test/no-suicide-exit.test.ts). Stub
// process.exit, wait past both timers so they fire harmlessly while
// stubbed, then restore. (resetForTest does NOT defuse the timers: the
// exit callback is unconditional.)
const origExit = process.exit;
(process as any).exit = (() => undefined) as any;
try {
const r = await fetchHandler(req("POST", "/shutdown"));
expect(r.status).toBe(200);
const body = (await r.json()) as any;
expect(body.shuttingDown).toBe(true);
// Let both 50ms timers (gracefulShutdown, then its process.exit) fire
// against the stub before restoring the real process.exit.
await new Promise((resolve) => setTimeout(resolve, 200));
} finally {
(process as any).exit = origExit;
}
// Reset state for subsequent tests (gracefulShutdown set shuttingDown).
resetDaemon();
});
});