Cause: TestClaudeAccountBackOnceItsWindowRenews (a2c39053) failed the
ubuntu race shard of run 37942751214. Its Cleanup's LoginUsageVia(nil)
wrote the package-level stand-in while a reading Allowances had started
(routing.go:419) was still reading it in loginUsageAt. Allowances reads
in the background on purpose and returns without waiting (only the very
first ask waits, and not for long), so a reading can outlast the caller,
and the test. The window: Allowances deletes c.loading before it tells
the renewed hooks, so the test's next poll can start another reading
just as the rest it waits on is lifted; the test ends with that reading
in flight. In production the stand-in is never set, so there is no
production race; the goroutine outliving its caller is the design (it is
bounded by its 30 s context).
Change: the stand-in is held for reading while it runs, and
LoginUsageVia takes it for writing, so it returns once no reading is
running through the one it replaces and none starts through it after.
That fixes every test that swaps it (gateway claude_renewed and
allowance_stale, provider routing, allowance_renewed,
codex_reset_deadline, window_renewed), not only the one that failed.
UsageClaudeVia's claudeCLIUsage is the same shape (tests swap it, and
Allowances' background reading of a Claude account reaches it through
readClaudeUsage), so it gets the same treatment; claude_usage_test's
restore goes through UsageClaudeVia. ProbeClaudeVia is left: a Claude
account test calls it synchronously, nothing reads it in the background.
Tests: login_usage_via_test.go holds a call through each stand-in and
checks the swap waits for it.
Run:
- Red before: with a 30 ms delay injected into loginUsageAt and before
Allowances tells the renewed hooks (not committed),
go test -race -count=20 -run TestClaudeAccountBackOnceItsWindowRenews
./internal/gateway/ printed 7 "WARNING: DATA RACE", the write at
login_usage.go:43 from the test's Cleanup and the read at
login_usage.go:63 from Allowances.func1, as on CI. Same injection with
this change: ok, no race. (-race -count=50 without the injection
passed on the old code too; the window is too narrow on this Mac.)
- The new tests with the swap made unsynchronised again: both FAIL
"swapped while a call was still running through the stand-in it
replaced"; with the change: PASS, -race -count=5.
- gofmt -l: nothing. go vet (darwin) ./internal/provider ./internal/gateway
./internal/gui; GOOS=linux and GOOS=windows go vet -tags nogui on the
same: clean.
- Under a temp HOME with pinned caches: go test -tags nogui
./internal/provider ./internal/gateway ok; go test -race
./internal/provider ok; go test -race -count=20 -run
'^(TestClaudeAccountBackOnceItsWindowRenews|TestAllowance.*)$'
./internal/gateway ok.
- go test -race -tags nogui ./internal/gateway (whole package, temp HOME):
ok in 635 s, no race.