tui tests: the package runs in UTC from TestMain, so no test swaps time.Local under the gateway's loops

TestSessionsPage raced about 1 in 20-40 runs under -race: sessionsHome set
time.Local = time.UTC (and back in its cleanup) while goroutines an earlier
test left running read it. gateway.ListenAndServe starts its Keep* loops
(check-ins, credits, history, quotas) on its ctx and returns once the
server is shut down, without waiting for them, and those loops call
.Local() and time.Date(..., time.Local). TestMain now sets time.Local to
UTC once, before any test starts a goroutine, and sessionsHome no longer
touches it; the sessions fixtures' days are counted in UTC as before.

A probe test with a goroutine calling time.Now().Local() while
sessionsHome ran reported DATA RACE at sessions_test.go:30 on the old code
and passes -race -count=3 on the new one (probe not committed). The
reported race itself didn't reproduce here before the change (-race
-count=40 of the package, -count=150 of TestTUI*|TestSessionsPage).
go test -race -count=40 -run TestSessionsPage ./internal/tui and
go test -race -count=3 ./internal/tui pass; go vet and gofmt are clean.
This commit is contained in:
yetone committed 2026-10-07 00:32:49 +08:00
1 parent 5d8717239d
commit 1caea63804
2 files changed
+14 -7

No files matched your search

+9 -2
View File
@@ -2,9 +2,16 @@ package tui
import (
"testing"
"time"
"github.com/yetone/magpie/internal/testenv"
)
// The tests run in a home of their own, never the user's real files.
func TestMain(m *testing.M) { testenv.Main(m) }
// The tests run in a home of their own, never the user's real files, and in
// UTC, the zone the sessions fixtures' days are counted in. The zone is set
// here, before any test starts a goroutine: a test that swapped it would race
// the gateway's loops an earlier test left running, which read time.Local.
func TestMain(m *testing.M) {
time.Local = time.UTC
testenv.Main(m)
}
+5 -5
View File
@@ -14,7 +14,8 @@ import (
)
// sessionsHome puts the sessions package's fixtures where Claude Code and
// Codex keep their sessions, in a sandbox HOME, their models priced.
// Codex keep their sessions, in a sandbox HOME, their models priced. Their
// days are counted in UTC, which TestMain sets for the package.
func sessionsHome(t *testing.T) {
t.Helper()
h := home(t)
@@ -26,8 +27,7 @@ func sessionsHome(t *testing.T) {
}
t.Setenv(env, dir)
}
oldZone, oldPrice := time.Local, sessions.PriceOf
time.Local = time.UTC
oldPrice := sessions.PriceOf
sessions.PriceOf = func(_ settings.Settings, m string) (catalog.Price, bool) {
switch m {
case "claude-opus-5-5":
@@ -39,9 +39,9 @@ func sessionsHome(t *testing.T) {
}
t.Cleanup(func() {
// the page's index is written behind it: let that write finish before
// the zone it reads (a stat of the file it writes) goes back
// the prices it reads go back
sessions.Reset()
time.Local, sessions.PriceOf = oldZone, oldPrice
sessions.PriceOf = oldPrice
})
sessions.Reset()
}