test(security-scanner-matrix): wait for pty EOF before reading the TTY prompt output
Problem
bun-security-scanner-matrix-without-node-modules.test.tsfails on main, on every retry of build 110964 (alpine 3.23 aarch64), withExpected to contain: "Installation cancelled."and output that ends atContinue anyway? [y/N] n\r\n. The siblingwith-node-modulesfile fails the same way (build 110943, debian 13 aarch64). The exit code is correct. Only the last line of the child's output is missing.- The TTY cases in
bun-security-scanner-matrix-runner.tspass a pre-builtnew Bun.Terminal()toBun.spawnand dispose it right afterawait proc.exited.Subprocess::on_process_exitdrains the pty only for inline terminals (OWNS_TERMINAL,src/runtime/api/bun/subprocess.rs:972). When the kernel reports the pidfd before the pty master becomes readable,exitedresolves, theawait usingcloses the terminal, andTerminal::close_internaldrops the unread bytes. The test has this race since #25587 added it.
Fix
- The runner spawns with an inline terminal and waits for its
exit(EOF) callback afterproc.exited. For an inline terminal bun reads the pty to EOF inon_process_exitbefore it resolvesexited, so the last line is inerrAndOutbefore the assertions run. This is the patterntest/js/bun/terminal/terminal.test.tsuses for a fast-exiting child. - The test still drives the same prompt: it writes the answer when the prompt text arrives, and asserts on the same messages, exit code, and files.
- Verified:
USE_SYSTEM_BUN=1 bun teston both matrix files (688 pass, 32 skip, 0 fail each) andbun bd test -t "TTY:"onwithout-node-modules. A standalone probe (Notes) loses the last line in 505 of 8000 runs with a pre-built terminal and in 0 of 8000 with an inline terminal.
Background
Bun.Terminalwraps a pty.Bun.spawn({ terminal })accepts an options object (an inline terminal the subprocess owns) or an existingTerminal(the caller owns it and can reuse it). Only the owned one is drained and closed on child exit.- The
datacallback runs inside the epoll callback for the master fd.exitedresolves inside the pidfd callback, and microtasks run before the next ready poll is dispatched. So the order of the two callbacks in one epoll batch decides whether the last bytes reachdata.
Related open PRs: #35851 fixes the same race on the bun side (drain a pre-built terminal on child exit). #39956 makes the same change to the runner as part of a larger rewrite of the matrix. This PR is the minimal test-only fix so main is green again.
Notes
Probe, run with the released bun 1.4.3 on linux x64. The child is sh -c 'printf "PROMPT? [y/N] "; read x; printf "\nCANCELLED\n"'. The parent writes n\n when it sees the prompt, awaits proc.exited, then checks whether CANCELLED arrived. With a bun child the loss did not reproduce on x64 in 12000 runs. With the fast-exiting sh child, 8 parallel runs of 1000:
existing lost 59 of 1000 (pre-built Bun.Terminal, await using)
existing lost 60 of 1000
existing lost 54 of 1000
existing lost 61 of 1000
existing lost 75 of 1000
existing lost 60 of 1000
existing lost 65 of 1000
existing lost 72 of 1000
inline lost 0 of 1000 (terminal: {...}, await exit callback) x8
Why a synchronous read after exit always sees the data on Linux: pty_write pushes the bytes into the master's flip buffer and queues the flush on a workqueue, so the master becomes readable a little after the write. n_tty_read calls tty_buffer_flush_work before it reports EAGAIN or EIO, so the read that drain_and_close_slave_fd does in on_process_exit observes the bytes even when the workqueue has not run yet. An epoll readiness event does not have that guarantee.
The 5 s per-test timeouts I saw in the local ASAN debug build (host load average above 150) occur on main too (2 of 288 on main, 7 of 288 on this branch, different cases each time). Each one is a child killed by the test timeout (exit code 143), not an EOF hang.
test/cli/bun.test.ts (a tty stdout still gets colors) works around the same race with a polling loop and a comment. test/cli/init/init.test.ts and test/regression/issue/26286.test.ts also use pre-built terminals but wait for specific output before the child exits.
no test proof · iteration 0 · no src or test change; test-proof not applicable