fix(opencode): stop CLI test fixtures leaking processes on Windows - #42990
fix(opencode): stop CLI test fixtures leaking processes on Windows#42990NamedIdentity wants to merge 1 commit into
Conversation
cli-process.ts spawns CLI children with a bare "bun" argv[0]. Under bunx, PATH resolves that to the node_modules/.bin/bun.exe shim, which starts the real bun as a separate process and waits on it. The fixture only ever holds the shim, so kill() + await exited completes as soon as the shim dies while the OpenCode process keeps running with no parent. I had 35 of these alive, the oldest 46 hours, holding ports and file locks. Use process.execPath instead. Bun resolves it via selfExePath() rather than PATH, so the handle is the process the fixture means to control. Only serve() and startRun() were leaking. acp() is safe by accident (stdin EOF exits the child first) and the generic spawn() goes through CrossSpawnSpawner, which tree-kills on Windows. Changed all four anyway so the fixture never picks a different bun than the test runner. Also two test changes so this cannot silently come back: the serve teardown assertion was `typeof code === "number" || code === null`, true for every possible value - it now checks the server actually stops answering. And the SIGINT test body gets a timeout so it fails on its own terms instead of running out the test runner's clock.
Overall: focused, well-commented fix for real Windows CI pain, with the serve-dead verification being a genuinely better invariant than what it replaces. Nothing blocking. Thanks! |
Issue for this PR
Closes #42989
Type of change
What does this PR do?
Changes the four places
packages/opencode/test/lib/cli-process.tspicks the bun executable, from the string"bun"toprocess.execPath."bun"gets resolved through PATH. Under bunx that finds thenode_modules\.bin\bun.exeshim, which starts the real bun as a separate process and waits on it — so the fixture is holding the shim, not the process it thinks it owns.kill()+await exitedthen completes as soon as the shim dies, while the actual OpenCode process keeps running with no parent. On my machine that had accumulated 35 leftover processes.process.execPathis the bun already running the tests (bun resolves it viaselfExePath(), not PATH), so the handle points at the process we actually want to control.Only
serve()andstartRun()were leaking.acp()is safe by accident (stdin EOF exits the child first) and the genericspawn()is safe because it goes throughCrossSpawnSpawner, which tree-kills on Windows. I changed all four anyway so the fixture never picks a different bun than the test runner.Two test changes so this can't silently come back: the serve teardown assertion was
typeof code === "number" || code === null, true for every possible value — it now checks the server actually stops answering. And the SIGINT test body gets a timeout so it fails on its own terms instead of running out the test runner's clock.How did you verify your code works?
I wrote both tests first and ran them against the unchanged code to confirm they fail:
exitedhad already resolvedrun.resultnever settled, ran to the 30s timeoutThen changed the four selectors and reran the same test files unmodified:
Full
test/clirun afterwards: 398 tests, zero leftover processes. The same run left 3 behind before the change. Fullbun testfrompackages/opencode: 16 failures, all of which were already failing on my machine before I touched anything (symlink tests and one Zed path test). Typecheck clean.This branch is the change applied to current
dev(5a0e07e), and I reran both tests there as well — serve passes in 2.1s, SIGINT in 2.8s, no leftover processes, typecheck clean.To confirm it yourself on Windows: run any CLI subprocess test through bunx, then look for leftover
bun.exeprocesses whose parent no longer exists.One limitation worth flagging — this only reproduces where bunx puts the shim on PATH. The Windows CI job installs bun through
setup-bun, so no shim is generated there and CI wouldn't catch a regression in this.Screenshots / recordings
N/A — not a UI change.
Checklist