fix(tests): reap leaked pty daemons at vitest teardown (#52)
* fix(tests): reap leaked pty daemons at vitest teardown
Tests spawn pty daemons into mkdtempSync-based PTY_SESSION_DIRs. The
daemons detach by design (that's how `pty attach` after a crash works)
and vitest exits without reaping them, so every test run leaked a few
ppid=1 pty daemons + their cat leaves. Over days across pty +
pty-relay, this drove /dev/ttys past kern.tty.ptmx_max=511, blocking
new spawns entirely (`posix_spawnp failed`).
Fix: add a vitest globalSetup module that
1. mkdtempSync's a per-run root under /tmp (short prefix avoids the
macOS 103-byte AF_UNIX path limit — /var/folders/.../pv-XXX would
push nested test socket paths over),
2. exports PTY_VITEST_RUN_ROOT + overrides TMPDIR so every test's
os.tmpdir() call resolves into the run root (zero per-test changes
across the 43 files that call os.tmpdir()),
3. on teardown, `lsof +D $runRoot` enumerates every pid holding a
file or socket under the tree, SIGTERMs them, waits 2s, SIGKILLs
survivors, then rm -rfs the run root.
Verified on full suite (86 files, 1197 passed, 21 skipped, 0 failed):
daemons before = daemons after; ttys before = ttys after; teardown
reports "SIGTERM N leaked pids" for the daemons that would previously
have leaked.
* fix(tests): also sweep unix sockets in teardown — `lsof +D` silent-miss
`lsof +D <dir>` on macOS walks the filesystem tree but does not index
unix-socket-name entries. A process holding ONLY a bound listen socket
inside <dir> is invisible to that sweep. That describes the pty daemon
exactly: it holds its .sock file in the session dir, but no other open
files or cwd.
Reproducer:
mkdir -p /tmp/probe/x && node -e '
const s=require("net").createServer();
s.listen("/tmp/probe/x/leak.sock", () => setInterval(()=>{},1e9));
' &
lsof -Fpn +D /tmp/probe # → empty
lsof -Fpn -U | grep probe # → finds it
Add a second sweep: `lsof -Fpn -U` and match sockets by name prefix
(runRoot/ and /private<runRoot>/ — macOS symlinks /tmp -> /private/tmp
and the daemon's socket name resolves to the /private twin). Union the
pids with the +D sweep, then SIGTERM/SIGKILL as before.
Verified: on tests/nesting-prevention.test.ts the previous teardown
reported "SIGTERM 3 leaked pids"; with the -U sweep added it reports 6.
The 3 the +D-only sweep missed were the ones holding only sockets.
Full suite still 1197 passed / 21 skipped / 0 failed; daemon + tty
counts before == after (delta 0).
Credit: pty-relay-claude flagged the silent miss while implementing the
mirror fix in pty-relay (PR #25).