Skip to content

Stop tui/api tests from fork-bombing as fake daemons#606

Merged
drn merged 5 commits into
masterfrom
argus/users-darrencheng-downloads
May 16, 2026
Merged

Stop tui/api tests from fork-bombing as fake daemons#606
drn merged 5 commits into
masterfrom
argus/users-darrencheng-downloads

Conversation

@drn
Copy link
Copy Markdown
Owner

@drn drn commented May 16, 2026

Two test paths reached production fork-exec helpers that re-spawn os.Executable()
with "daemon start". Under go test, that re-runs the entire test package — which
re-hits the test that forked — orphaning multiple *.test daemon start processes at
~85% CPU each, and overwriting ~/.argus/argusd to point at a temp test binary
that vanishes when the test ends.

Adds backstops at both fork sites (client.AutoStart, api.spawnSuccessorDaemon)
that refuse to fork when os.Args[0] looks like a Go test binary. Adds
App.restartDaemonFn injection so the TUI test can swap in a stub instead of
relying solely on the backstop. Splits the post-backstop fork bodies into
separate files (autostart_fork.go, spawn_fork.go) listed in coverage-ignore.txt.
Rewrites both tests to assert the new sentinel errors.

Also fixes a pre-existing race in TestRunner_KickRerender_NoLoopOnImmediateCrash
(flipping consumed=true after r.Start raced with the exit goroutine; nil-deref
panic on slow CI runners). Same panic was failing the last two master commits.

Co-Authored-By: Claude noreply@anthropic.com

drn added 5 commits May 15, 2026 20:11
Two tests reached production fork-exec helpers that re-spawn os.Executable()
with "daemon start": TestApp_HandleRestartDaemonKey_RestartChosen (via
client.AutoStart from a goroutine) and TestSpawnSuccessorDaemon (direct
call). Under `go test` os.Executable() resolves to the test binary, Go's
test framework treats "daemon start" as positional args, and m.Run() runs
the whole package again — including the test that just forked — orphaning
multiple test-binary copies at ~85% CPU each and overwriting the real
~/.argus/argusd symlink to point at a temp binary that vanishes on test
end.

- Add ErrTestBinary / errSpawnFromTestBinary backstops to AutoStart and
  spawnSuccessorDaemon. Refuse to fork when os.Args[0] looks like a Go
  test binary.
- Add App.restartDaemonFn injection so the TUI test can swap in a stub
  instead of relying solely on the backstop.
- Redirect HOME via t.Setenv in the affected TUI test as defense in depth.
- Rewrite both tests to assert errors.Is the new sentinel.
- Document the gotcha in context/knowledge/gotchas/daemon-rpc.md.
The test injected a bare pendingRestart{} (task=nil) and then flipped
consumed=true AFTER calling r.Start, so on slow runners the exit
goroutine in runner.Start.func2 read consumed=false first, called
r.Start(pending.task=nil, ...), and panicked with a nil deref at
runner.go:57 ("session already exists for task..." would otherwise
have been the assertion).

Fix: set consumed=true on the injected entry up front (before the
second Start), removing the race. Test still verifies the intended
invariant — the resumed session's exit goroutine reads consumed=true
and skips another restart — and stays green across -count=50 on
darwin and (per CI) ubuntu.
After adding the *.test backstops, TestC_AutoStart and
TestSpawnSuccessorDaemon short-circuit at the new check — they no longer
exercise the os.Executable + symlink + exec.Command + poll-timeout path
they used to reach (with a guaranteed-fail socket). That dropped the
filtered coverage from 88.0% to 87.8% on CI, tripping the 88% floor.

Those lines are now genuinely untestable under `go test`: the only way
to exercise them is to fork the test binary as a fake daemon, which is
the exact fork bomb the backstop prevents.

Move the post-backstop bodies into autostart_fork.go and spawn_fork.go,
list both in coverage-ignore.txt, and have the thin wrappers delegate
through them. Filtered coverage back to 88.2% locally.
…art call

Review surfaced two architecture warnings on the fork-bomb fix:

1. `isTestBinary` is duplicated across internal/agent/cleanup.go,
   internal/daemon/client/client.go, and internal/api/selfupdate.go. The
   `/_test/` arm is subtle and a future maintainer copying the function
   for a fourth backstop site could easily drop it. Add a cross-reference
   comment at each site so the drift risk is loud.

2. `updateArgus` calls `a.restartDaemonFn()` synchronously while the
   other three call sites use `go a.restartDaemonFn()`. The synchronous
   call is correct because `updateArgus` is itself already running in a
   goroutine, but the asymmetry was undocumented and would lure a
   future reader into "fixing" it by adding `go`. Add a one-line comment.
@github-actions
Copy link
Copy Markdown

Merging this branch will decrease overall coverage

Impacted Packages Coverage Δ 🤖
github.com/drn/argus/internal/agent 88.37% (-0.38%) 👎
github.com/drn/argus/internal/api 88.59% (-0.46%) 👎
github.com/drn/argus/internal/daemon/client 86.83% (-6.93%) 👎
github.com/drn/argus/internal/tui 81.36% (-0.07%) 👎

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/drn/argus/internal/agent/cleanup.go 95.06% (ø) 81 77 4
github.com/drn/argus/internal/api/selfupdate.go 94.12% (+4.12%) 34 (-6) 32 (-4) 2 (-2) 👍
github.com/drn/argus/internal/api/spawn_fork.go 0.00% (ø) 10 (+10) 0 10 (+10)
github.com/drn/argus/internal/daemon/client/autostart_fork.go 0.00% (ø) 23 (+23) 0 23 (+23)
github.com/drn/argus/internal/daemon/client/client.go 94.97% (+0.53%) 179 (-19) 170 (-17) 9 (-2) 👍
github.com/drn/argus/internal/tui/app.go 67.87% (-0.47%) 1628 (+1) 1105 (-7) 523 (+8) 👎

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/drn/argus/internal/agent/runner_test.go
  • github.com/drn/argus/internal/api/handlers_test.go
  • github.com/drn/argus/internal/daemon/client/client_test.go
  • github.com/drn/argus/internal/tui/app_keyhandlers_test.go

@drn drn merged commit e8c7649 into master May 16, 2026
1 check passed
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