fix(test): don't kill the deno process on top-level Deno.exit() (#34564)
## Summary
Calling `Deno.exit()` from outside any test function — at module top
level, in an `unload` listener, or from async work that escaped a test —
used to kill the entire `deno test` process with the requested exit
code. That silently dropped buffered test results and made failing tests
"pass" with exit code 0.
Reproducer from #12888:
```ts
Deno.test("fail", () => { throw new Error("fail"); });
self.onunload = () => { Deno.exit(0); };
```
Before this PR:
```
$ deno test test.ts
Check file:///.../test.ts
running 1 test from file:///.../test.ts
test fail ... FAILED (9ms)
$ echo $?
0 # failed test, but zero exit code
```
After this PR:
```
$ deno test test.ts
running 1 test from ./test.ts
fail ... FAILED (1ms)
note ./test.ts called `Deno.exit(0)` from outside any test. The isolate was terminated; remaining test files will continue.
ERRORS
fail => ./test.ts:1:6
error: Error: fail
...
FAILED | 0 passed | 1 failed (24ms)
error: Test failed
$ echo $?
1
```
## How it works
Each test specifier now installs a default exit handler (in addition to
the per-test `assertExit` handlers) that, when `Deno.exit(code)` is
called outside of any test function, calls a new `op_test_isolate_exit`
op:
- records the exit code in `OpState` (`IsolateExitInfo`)
- sends a new `TestEvent::IsolateExit(origin, exit_code)` to the
reporter
- asks V8 to terminate execution of just this isolate (uncatchable)
The test runner detects the termination via the `IsolateExitInfo` flag,
calls `cancel_terminate_execution`, and continues with any remaining
specifiers. A zero exit code is treated as a clean isolate teardown; a
non-zero exit code fails the run.
The existing `sanitizeExit: false` path (which kills the whole process
via `std::process::exit`) is preserved for `Deno.exit()` calls from
inside a test function — only the *outside-a-test* path changes here,
since that's where the silent-process-kill bug lived.
Fixes #12888.
Closes denoland/orchid#325
## Test plan
- [x] Existing `exit_sanitizer`, `exit_sanitizer_disabled`,
`exit_code{,2,3}`, `before_unload_prevent_default` spec tests still pass
- [x] New `exit_in_isolate_unload` spec test (the literal #12888
reproducer): test fails, isolate-exit note is printed, process exits 1
- [x] New `exit_in_isolate_top_level::exit_zero_alone`: top-level
`Deno.exit(0)` -> exit 0, no phantom tests
- [x] New `exit_in_isolate_top_level::exit_nonzero_alone`: top-level
`Deno.exit(7)` -> exit 1, failure reported
- [x] New
`exit_in_isolate_top_level::mixed_run_passing_alongside_isolate_exit`: a
passing test file run alongside an isolate-exit file still completes
normally
- [x] `cargo clippy --bin deno` clean
---------
Co-authored-by: divybot <divybot@users.noreply.github.com>
Co-authored-by: Divy Srivastava <me@littledivy.com> E
em committed
5c6094ef7f8a0d213c2d2ee9afc8d263c025fad3
Parent: 902ba73
Committed by GitHub <noreply@github.com>
on 5/31/2026, 4:29:27 AM