Version
v22.23.3, v24.21.0
Platform
Linux x64: official node:22-bookworm and node:24-bookworm Docker images. Not reproducible on Windows.
Subsystem
test_runner
What steps will reproduce the bug?
// leak.test.mjs
import { test } from 'node:test';
import { spawn } from 'node:child_process';
test('leaks a child with inherited stdio', () => {
spawn(process.execPath, ['-e', 'setTimeout(() => {}, 20000)'], { stdio: 'inherit' });
});
time node --test --test-force-exit leak.test.mjs
How often does it reproduce?
Every run we measured.
What is the expected behavior?
The run ends shortly after the test passes, as it does on Windows (about 1 s). --test-force-exit is documented to exit the process once all known tests have finished, even if the event loop would otherwise stay active.
What do you see instead?
The test passes immediately, but the run does not end until the leaked child exits: 20 s for a child that lives 20 s. That happened on both versions above, with --test-force-exit and with or without a preload that makes stdout blocking.
Our reading: the per-file test process does exit, but the child still holds a copy of that process's stdout pipe (inherited), and the parent runner waits for the pipe to close. Nothing inside the test process can close another process's copy of the pipe, so the test file itself cannot work around it.
Additional information
Version
v22.23.3, v24.21.0
Platform
Linux x64: official
node:22-bookwormandnode:24-bookwormDocker images. Not reproducible on Windows.Subsystem
test_runner
What steps will reproduce the bug?
How often does it reproduce?
Every run we measured.
What is the expected behavior?
The run ends shortly after the test passes, as it does on Windows (about 1 s).
--test-force-exitis documented to exit the process once all known tests have finished, even if the event loop would otherwise stay active.What do you see instead?
The test passes immediately, but the run does not end until the leaked child exits: 20 s for a child that lives 20 s. That happened on both versions above, with
--test-force-exitand with or without a preload that makes stdout blocking.Our reading: the per-file test process does exit, but the child still holds a copy of that process's stdout pipe (inherited), and the parent runner waits for the pipe to close. Nothing inside the test process can close another process's copy of the pipe, so the test file itself cannot work around it.
Additional information
--test-timeout=15000 --test-force-exitexits in about 1.1 s.--test-force-exitdropping queued test reports on POSIX asynchronous stdout pipes). A blocking-stdout preload fixes that one, but not this hang.