bun test: only run process.on('exit') listeners when node:test APIs were used

Merged
JA
3mo ago
+205-63

What does this PR do?

Reverts the user-visible half of #34444's exit-listener change. Since that PR, bun test dispatched every test file's process.on('exit') listeners once the last file finished, so a listener calling process.exit(1) failed the run even though the summary reported everything passing (this was slated to be a documented breaking change in the 1.4 notes).

Checked jest 30 and vitest 4.1 with a passing test whose file registers process.on("exit", () => process.exit(1)):

runnerlistener ranexit code
jest (workers and -i)no0
vitest (forks, threads, vmForks, vmThreads)no0
bun 1.4 canary before this PRyes1

jest gives test files a copy of process; vitest tears its workers down itself. Since bun test runs every file in one process, one file's listener also affected the whole run.

Now the end-of-run on_exit() still runs (profilers, deferred flushes, cleanup hooks) but skips the listener dispatch unless one of these opted in:

  • a node:test API was called on the main thread — test/describe/hooks/mock.*/assert.register all reach jsFileGeneration, which marks the runner. Merely importing node:test, or a Worker using it, doesn't count.
  • BUN_TEST_DRAIN_EVENT_LOOP=1, the existing opt-in scripts/runner.node.mjs sets for vendored node tests (a few of those run under bun test without node:test and check common.mustCall() counts from an exit listener).

Both keep the vendored node tests and node:test's run() children working. A test that calls process.exit() itself still runs listeners, as before. --parallel workers now go through the same gated on_exit() instead of straight to global_exit().

test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js verified its own mustCall() wrappers from an exit listener; that check is now an afterAll.

How did you verify your code works?

  • test/cli/test/bun-test.test.ts — replaced the two tests from #34444 with eleven covering: bun:test file, globals-style file, import-only node:test, and Worker-only node:test don't run listeners and exit 0 even when the listener calls process.exit(1); explicit process.exit(3) from a test still runs them; a file registering a node:test test runs them and can fail the run; BUN_TEST_DRAIN_EVENT_LOOP=1 runs them for a bun:test file; --parallel runs for both kinds. The "not run" cases fail on the current canary and pass with this branch; the Worker case was also checked against a build with the main-thread guard removed.
  • util-inspect.test.js passes, and fails when a mustCall() wrapper is never invoked.
  • test/js/node/test_runner/node-test.test.ts passes (45/45); the vendored test-events-add-abort-listener, test-file-write-stream5, test-worker-arraybuffer-zerofill, test-runner-mocking still pass with BUN_TEST_DRAIN_EVENT_LOOP=1.
  • test/cli/test/parallel.test.ts: 34/35 locally; the one failure (unique JEST_WORKER_ID) reproduces on this debug build without the worker change and passes on the release canary — debug-build timing, unrelated.