test_runner: gate event-loop drain and on_exit to node:test, drop BUN_TEST_DRAIN_EVENT_LOOP - #35401
Open
robobun wants to merge 5 commits into
Open
test_runner: gate event-loop drain and on_exit to node:test, drop BUN_TEST_DRAIN_EVENT_LOOP#35401robobun wants to merge 5 commits into
robobun wants to merge 5 commits into
Claude / Claude Code Review
completed
Jul 24, 2026 in 28m 37s
Code review found 1 important issue
Found 4 candidates, confirmed 2. See review comments for details.
Details
| Severity | Count |
|---|---|
| 🔴 Important | 1 |
| 🟡 Nit | 1 |
| 🟣 Pre-existing | 0 |
| Severity | File:Line | Issue |
|---|---|---|
| 🔴 Important | src/runtime/cli/test_command.rs:3384-3386 |
Per-file on_before_exit() drain is unsound in multi-file bun test runs |
| 🟡 Nit | src/js/node/test.ts:479 |
run() children that don't import node:test lose drain and exit handlers |
Annotations
Check failure on line 3386 in src/runtime/cli/test_command.rs
claude / Claude Code Review
Per-file on_before_exit() drain is unsound in multi-file bun test runs
The per-file `on_before_exit()` drain at line 3384 is unsound in multi-file `bun test` runs: `node_test_module_used` is process-lifetime state that is never reset between files, and even when correctly set for a node:test file, the drain's `while is_event_loop_alive()` loop sees ref'd handles left by *earlier* bun:test files (a leaked `setInterval`, an unclosed `Bun.serve`) and hangs the whole process. The removed env var was explicitly "Opt-in; one file per process" — its only setters (`runner.
Check warning on line 479 in src/js/node/test.ts
claude / Claude Code Review
run() children that don't import node:test lose drain and exit handlers
Dropping `BUN_TEST_DRAIN_EVENT_LOOP` from the `runOneFile` env means a `run()` child whose target file never imports `node:test` (e.g. a plain `common.mustCall()` + async-callback script) no longer drains its event loop or dispatches `process.on('exit')` — `node_test_module_used` is only set via `getRootNode()`, which such a file never reaches. Node's `run({isolation:'process'})` runs each file as a full node process that always drains and fires `'exit'` regardless of whether it imported `node:t
Loading