Work type
Bug / regression
Observed behavior
TestGoCodeScriptStopsHarnessdOnInterrupt/wrapper-started_daemon_is_stopped fails intermittently in CI under the race suite:
--- FAIL: TestGoCodeScriptStopsHarnessdOnInterrupt (20.13s)
--- FAIL: TestGoCodeScriptStopsHarnessdOnInterrupt/wrapper-started_daemon_is_stopped (10.11s)
go_code_script_test.go:389: wrapper did not exit within 10s of SIGINT
make: *** [Makefile:21: test-race] Error 1
Observed on PR #1421, whose diff touches only cmd/harnesscli/tui/components/spinner and docs — nothing in cmd/harnesscli itself. It passed on re-run of the same commit, and passes 3/3 locally under -race on macOS. It passed CI when introduced in #1417.
Frequency: intermittent, load-dependent. Enough to red-flag unrelated PRs.
Expected behavior
The test asserts a real contract — a wrapper-started daemon must not outlive the wrapper — and should pass deterministically regardless of runner load. Its timing budget must not sit a hair above the wall-clock cost of the path it exercises.
Reproduction
go test ./cmd/harnesscli -run TestGoCodeScriptStopsHarnessdOnInterrupt -race on a loaded machine, or in CI under the full race suite. Does not reproduce reliably on an idle machine.
User and operational impact
No end-user impact; this is test-only. The operational cost is real: a flaky red check on main makes every unrelated PR look broken, and trains people to re-run rather than read failures. #1421 was merged on a re-run for exactly this reason, which is the habit worth not building.
Suspected seam and search evidence
The test itself, cmd/harnesscli/go_code_script_test.go, not the wrapper it tests. The stub daemon is written as:
#!/usr/bin/env bash
trap '' INT
echo $$ > "$DAEMON_PID_FILE"
sleep 300
Ignoring INT is deliberate and correct: it models the daemon surviving the process-group signal a real Ctrl+C delivers, so cleanup has to kill it explicitly. That part should stay.
The problem is sleep 300 in the foreground. When stop_server sends SIGTERM (scripts/go-code.sh), bash defers the signal until its foreground child finishes — which never happens. So the wrapper always falls through the full graceful-shutdown wait (25 iterations x 0.2 s = 5 s) and then force-kills.
That means every run of this test takes the 5-second worst-case path, leaving roughly 5 s of headroom inside the test's 10 s budget. Under -race on a loaded runner, that margin disappears.
Two consequences worth separating: the test is slower than it needs to be, and it exercises the force-kill path exclusively — never the graceful shutdown that real daemons actually take. The second is arguably the bigger loss, since graceful shutdown is the common case and currently has no coverage here.
Commands used: gh run view --job 102056137348 --log-failed; go test ./cmd/harnesscli -run TestGoCodeScriptStopsHarnessdOnInterrupt -race -count=1 x3 locally; reading stop_server in scripts/go-code.sh and the stub definitions in the test.
Blast-radius impact map
Callers and data flow: one test file. No production code.
Config/env/defaults: none.
API/CLI/wire formats/tools: none.
Persistence/schema/cache: none.
Concurrency/lifecycle: the point of the change — the stub becomes promptly killable, so the wrapper's graceful path is what runs.
Security/auth/permissions/privacy: none.
TUI/web/macOS/other clients: none.
Deployment/observability/runbooks: none.
Compatibility: none.
Existing tests/fixtures: the sibling case in the same test (pre-existing daemon is left alone) and TestGoCodeScriptStopsHarnessdWhenOutputPipeCloses, whose stub has the same shape and the same latent slowness, though it does not currently flake because it does not wait on the wrapper's exit the same way. Both should be treated consistently.
Documentation: engineering-log entry noting the flake and its cause.
Regression test first
This is a fix to a test, so the "test first" evidence is the failure itself plus a timing measurement rather than a new test. Before changing anything, record how long the wrapper actually takes to exit after SIGINT — expected around 5 s, dominated by the force-kill wait. After the change it should be well under 1 s, because the stub exits on TERM.
Add an assertion that the wrapper exits promptly (a budget comfortably under the old 5 s but not so tight it reintroduces flake), so a future regression that pushes cleanup back onto the force-kill path is caught as a failure rather than as a slow pass.
Fix boundaries
In scope:
- Make the stub daemon exit promptly on SIGTERM the way a real daemon does, e.g.
trap 'exit 0' TERM with sleep 300 & wait $! so bash can run the trap instead of deferring it behind a foreground sleep.
- Keep
trap '' INT: surviving the group SIGINT is the behavior under test.
- Widen the wrapper-exit budget for headroom, and assert prompt exit as described.
- Apply the same stub shape to
TestGoCodeScriptStopsHarnessdWhenOutputPipeCloses so both tests exercise graceful shutdown.
Out of scope:
scripts/go-code.sh. The wrapper is correct; its 5-second wait then force-kill is the right behavior for a daemon that will not exit.
- The
STARTED_BY_US contract and the --server detachment.
- Any change to what the tests assert about orphaning.
Diagnostic and observability evidence
Before: the wrapper takes ~5 s to exit after SIGINT in this test, all of it inside stop_server's kill -0 polling loop, ending in SIGKILL.
After: the stub exits on the first SIGTERM, stop_server's loop returns on its first or second poll, and the wrapper exits in well under a second. The measured exit time is the signal that the fix worked.
Verification plan
- Measure and record wrapper exit time before and after.
go test ./cmd/harnesscli -run TestGoCodeScript -race -count=5 — all seven tests, five times, to give the flake a chance to appear.
- Full regression:
go test ./cmd/... ./internal/....
- CI: confirm green on the PR without a re-run. A pass that needed a re-run does not count as evidence here.
Rollout and rollback
Test-only. No deployment concern. Rollback is reverting the commit.
Documentation and handoff
docs/logs/engineering-log.md: the flake, its cause (a stub whose foreground sleep makes bash defer SIGTERM, forcing the 5 s worst-case path on every run), and the durable lesson — a test stub that ignores a signal must still be killable by the mechanism under test, or the test measures the timeout rather than the behavior.
Definition of done
Work type
Bug / regression
Observed behavior
TestGoCodeScriptStopsHarnessdOnInterrupt/wrapper-started_daemon_is_stoppedfails intermittently in CI under the race suite:Observed on PR #1421, whose diff touches only
cmd/harnesscli/tui/components/spinnerand docs — nothing incmd/harnesscliitself. It passed on re-run of the same commit, and passes 3/3 locally under-raceon macOS. It passed CI when introduced in #1417.Frequency: intermittent, load-dependent. Enough to red-flag unrelated PRs.
Expected behavior
The test asserts a real contract — a wrapper-started daemon must not outlive the wrapper — and should pass deterministically regardless of runner load. Its timing budget must not sit a hair above the wall-clock cost of the path it exercises.
Reproduction
go test ./cmd/harnesscli -run TestGoCodeScriptStopsHarnessdOnInterrupt -raceon a loaded machine, or in CI under the full race suite. Does not reproduce reliably on an idle machine.User and operational impact
No end-user impact; this is test-only. The operational cost is real: a flaky red check on
mainmakes every unrelated PR look broken, and trains people to re-run rather than read failures. #1421 was merged on a re-run for exactly this reason, which is the habit worth not building.Suspected seam and search evidence
The test itself,
cmd/harnesscli/go_code_script_test.go, not the wrapper it tests. The stub daemon is written as:Ignoring INT is deliberate and correct: it models the daemon surviving the process-group signal a real Ctrl+C delivers, so cleanup has to kill it explicitly. That part should stay.
The problem is
sleep 300in the foreground. Whenstop_serversends SIGTERM (scripts/go-code.sh), bash defers the signal until its foreground child finishes — which never happens. So the wrapper always falls through the full graceful-shutdown wait (25 iterations x 0.2 s = 5 s) and then force-kills.That means every run of this test takes the 5-second worst-case path, leaving roughly 5 s of headroom inside the test's 10 s budget. Under
-raceon a loaded runner, that margin disappears.Two consequences worth separating: the test is slower than it needs to be, and it exercises the force-kill path exclusively — never the graceful shutdown that real daemons actually take. The second is arguably the bigger loss, since graceful shutdown is the common case and currently has no coverage here.
Commands used:
gh run view --job 102056137348 --log-failed;go test ./cmd/harnesscli -run TestGoCodeScriptStopsHarnessdOnInterrupt -race -count=1x3 locally; readingstop_serverinscripts/go-code.shand the stub definitions in the test.Blast-radius impact map
Callers and data flow: one test file. No production code.
Config/env/defaults: none.
API/CLI/wire formats/tools: none.
Persistence/schema/cache: none.
Concurrency/lifecycle: the point of the change — the stub becomes promptly killable, so the wrapper's graceful path is what runs.
Security/auth/permissions/privacy: none.
TUI/web/macOS/other clients: none.
Deployment/observability/runbooks: none.
Compatibility: none.
Existing tests/fixtures: the sibling case in the same test (
pre-existing daemon is left alone) andTestGoCodeScriptStopsHarnessdWhenOutputPipeCloses, whose stub has the same shape and the same latent slowness, though it does not currently flake because it does not wait on the wrapper's exit the same way. Both should be treated consistently.Documentation: engineering-log entry noting the flake and its cause.
Regression test first
This is a fix to a test, so the "test first" evidence is the failure itself plus a timing measurement rather than a new test. Before changing anything, record how long the wrapper actually takes to exit after SIGINT — expected around 5 s, dominated by the force-kill wait. After the change it should be well under 1 s, because the stub exits on TERM.
Add an assertion that the wrapper exits promptly (a budget comfortably under the old 5 s but not so tight it reintroduces flake), so a future regression that pushes cleanup back onto the force-kill path is caught as a failure rather than as a slow pass.
Fix boundaries
In scope:
trap 'exit 0' TERMwithsleep 300 & wait $!so bash can run the trap instead of deferring it behind a foreground sleep.trap '' INT: surviving the group SIGINT is the behavior under test.TestGoCodeScriptStopsHarnessdWhenOutputPipeClosesso both tests exercise graceful shutdown.Out of scope:
scripts/go-code.sh. The wrapper is correct; its 5-second wait then force-kill is the right behavior for a daemon that will not exit.STARTED_BY_UScontract and the--serverdetachment.Diagnostic and observability evidence
Before: the wrapper takes ~5 s to exit after SIGINT in this test, all of it inside
stop_server'skill -0polling loop, ending in SIGKILL.After: the stub exits on the first SIGTERM,
stop_server's loop returns on its first or second poll, and the wrapper exits in well under a second. The measured exit time is the signal that the fix worked.Verification plan
go test ./cmd/harnesscli -run TestGoCodeScript -race -count=5— all seven tests, five times, to give the flake a chance to appear.go test ./cmd/... ./internal/....Rollout and rollback
Test-only. No deployment concern. Rollback is reverting the commit.
Documentation and handoff
docs/logs/engineering-log.md: the flake, its cause (a stub whose foregroundsleepmakes bash defer SIGTERM, forcing the 5 s worst-case path on every run), and the durable lesson — a test stub that ignores a signal must still be killable by the mechanism under test, or the test measures the timeout rather than the behavior.Definition of done
-count=5race run green