From 3f11219dc1a68d576b93f5a299595126671aa810 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Wed, 6 May 2026 18:24:38 +0100 Subject: [PATCH] test: drain healthcheck goroutine + lock read in HealthCallback test CI on Linux flagged a race in TestStartWorker_HealthCallback_StatusChange: the test read ui.updates while the healthchecker's per-port goroutine (spawned by Register at checker.go:164) was still running and could fire UpdateStatus through notifyStatusChange. The earlier mutex on MockStatusUpdater protected the writes; the read side was unprotected, and the goroutine had not finished by the time the test started ranging over the slice on slower runners. Fix: - call healthChecker.Unregister(fwd.ID()) to drain the per-port goroutine before reading - hold ui.mu around the slice read for belt-and-suspenders happens- before, regardless of goroutine timing Verified locally with go test -race -count=20 on the targeted test and -count=3 on the full forward package. --- internal/forward/coverage_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/forward/coverage_test.go b/internal/forward/coverage_test.go index 3b83665..7b2f2ea 100644 --- a/internal/forward/coverage_test.go +++ b/internal/forward/coverage_test.go @@ -721,7 +721,15 @@ func TestStartWorker_HealthCallback_StatusChange(t *testing.T) { // but MarkConnected spawns a goroutine; MarkReconnecting calls markStatus directly). time.Sleep(20 * time.Millisecond) - // The callback should have updated status. + // Stop the healthchecker so its background per-port goroutine drains + // before we read the mock — establishes happens-before for the read and + // keeps the race detector quiet on slower CI runners. + m.healthChecker.Unregister(fwd.ID()) + + // The callback should have updated status. Hold the mock's lock during + // the read because background goroutines may still be unwinding. + ui.mu.Lock() + defer ui.mu.Unlock() var sawUpdate bool for _, u := range ui.updates { if u.ID == fwd.ID() {