Skip to content

fix(cli): refuse to spawn daemon from a test binary - #4

Merged
godspede merged 2 commits into
mainfrom
fix/ensure-daemon-refuse-test-binary
Apr 23, 2026
Merged

fix(cli): refuse to spawn daemon from a test binary#4
godspede merged 2 commits into
mainfrom
fix/ensure-daemon-refuse-test-binary

Conversation

@godspede

Copy link
Copy Markdown
Owner

Summary

  • Harden EnsureDaemon so it refuses to SpawnDetached(os.Executable()) when the current process is a *.test / *.test.exe binary.
  • Under go test, os.Executable() is the test runner; detaching it leaks a zombie cli.test.exe that holds handles on its t.TempDir trees. On Windows this blocks cleanup and spams %TEMP%\Test<Name>* indefinitely (one dir per run × up to 65 nested subdirs each).
  • Follow-up to fix(cli): don't spawn daemon from cap-with-yes test (Windows CI) #3, which fixed the single test that tripped this by reshaping its inputs. This PR fixes the underlying API so no future test can re-enable the zombie path.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./... (non-race, local — no cgo/gcc here; CI covers -race)
  • New TestEnsureDaemon_RefusesToSpawnTestBinary exercises the guard live — os.Executable() is a test binary, so it hits the new error path and asserts the message. Skips gracefully (with a reason) on exotic runners where the basename doesn't match.
  • TestIsTestBinary covers .test, .test.exe, mixed case, and real binary names like ghosthost / ghosthost.exe.
  • CI on Ubuntu + Windows (go vet, go test -race, smoke, govulncheck, gosec, fuzz) — must go green before merge.

🤖 Generated with Claude Code

Zack Frank added 2 commits April 22, 2026 20:59
EnsureDaemon calls daemon.SpawnDetached(os.Executable()). Under `go test`,
os.Executable() is the test runner itself, so the detached child is a
zombie cli.test.exe that survives the test run and holds open handles on
its t.TempDir trees. On Windows this blocks cleanup, spams
%TEMP%\Test<Name>* forever, and caused CI to fail with
"unlinkat cli.test.exe: Access is denied" (PR #3 fixed this by reshaping
the one test that tripped it).

Harden the boundary instead of relying on every test to avoid the call:
detect a .test / .test.exe basename in EnsureDaemon and return a
descriptive error rather than forking. Real ghosthost / ghosthost.exe
binaries are unaffected.

Includes a regression test that asserts EnsureDaemon rejects the current
process when run under `go test`, so future refactors can't quietly
re-enable the zombie path.
PR2 changed `--json share` to always emit an array (one element per file),
including in the single-file case, but the smoke test still unmarshals
into a single object. The regression has been latent on main since PR2
merged — PR3's CI stayed green only because the smoke result hit the
build cache. This PR touches internal/cli, which invalidates that cache
and surfaces the bug.

Unmarshal into a []struct, assert len == 1 for this single-file share,
and take index 0.
@godspede
godspede merged commit b337b8f into main Apr 23, 2026
2 checks passed
@godspede
godspede deleted the fix/ensure-daemon-refuse-test-binary branch April 23, 2026 03:05
godspede added a commit that referenced this pull request Jul 17, 2026
* fix(cli): refuse to spawn daemon from a test binary

EnsureDaemon calls daemon.SpawnDetached(os.Executable()). Under `go test`,
os.Executable() is the test runner itself, so the detached child is a
zombie cli.test.exe that survives the test run and holds open handles on
its t.TempDir trees. On Windows this blocks cleanup, spams
%TEMP%\Test<Name>* forever, and caused CI to fail with
"unlinkat cli.test.exe: Access is denied" (PR #3 fixed this by reshaping
the one test that tripped it).

Harden the boundary instead of relying on every test to avoid the call:
detect a .test / .test.exe basename in EnsureDaemon and return a
descriptive error rather than forking. Real ghosthost / ghosthost.exe
binaries are unaffected.

Includes a regression test that asserts EnsureDaemon rejects the current
process when run under `go test`, so future refactors can't quietly
re-enable the zombie path.

* fix(smoke): accept JSON array from `--json share`

PR2 changed `--json share` to always emit an array (one element per file),
including in the single-file case, but the smoke test still unmarshals
into a single object. The regression has been latent on main since PR2
merged — PR3's CI stayed green only because the smoke result hit the
build cache. This PR touches internal/cli, which invalidates that cache
and surfaces the bug.

Unmarshal into a []struct, assert len == 1 for this single-file share,
and take index 0.

---------

Co-authored-by: godspede <jehutheawesome@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant