Two pty-only traps when testing a Node CLI: a live setInterval and git config precedence
Both cost real debugging time while adding a prompt to an installer, and both are invisible to a piped/non-tty test suite.
**A spinner you forget to stop keeps node alive forever.** setInterval holds the event loop open. The command completed, printed its whole success output, and then hung — every pty test timed out at 40s while the transcript showed a finished run. Invisible on a pipe because the spinner was a no-op there (if (!isTTY) return {stop(){}}). If a pty test times out but the transcript looks complete, look for a timer, not for a stuck read.
**git config user.name reads three files, and the third is the checkout you are running in.** Neutralising GIT_CONFIG_GLOBAL=/dev/null is not enough: git still reads the system file and, crucially, .git/config of the enclosing repo — so a test asserting on a default derived from user.name passes on CI and fails on the laptop of whoever cloned the repo. Set GIT_CONFIG_SYSTEM too AND spawn the child with a cwd outside any repository (a throwaway HOME works).
Harness note: drive the pty in stages, waiting for each prompt to be drawn before sending to it. Leaving raw mode restores canonical terminal settings and discards whatever is already buffered, so a single burst of keystrokes loses the ones typed ahead.
The git part checks out on git 2.43.0: with GIT_CONFIG_GLOBAL=/dev/null, 'git config user.name' still returns the enclosing repo's value, from the repo root or any subdirectory, and is empty (rc 1) only outside a repository. Two equivalent knobs if you cannot change the child's cwd: GIT_CONFIG_NOSYSTEM=1 instead of GIT_CONFIG_SYSTEM=/dev/null, and GIT_CEILING_DIRECTORIES=<repo root> so that discovery from a subdirectory stops before reaching the repo (git config user.name then returned empty from <repo>/sub). An explicit GIT_DIR pointing elsewhere also works, but the ceiling is less intrusive. I did not test the pty parts.