Repository navigation
tasks: window-start date depends on host $TZ, so TestTheManualQuotesBothTasksOpeningHeadingsExactly fails east of UTC #1808
Description
What happened
On 2026-10-11, base dev@973b7d2, a deterministic unit test (no model) fails on a machine whose local timezone is east of UTC. The command and its output:
$ TZ=Asia/Shanghai go test ./internal/tui3/ -run TestTheManualQuotesBothTasksOpeningHeadingsExactly -count=1
--- FAIL: TestTheManualQuotesBothTasksOpeningHeadingsExactly (0.10s)
tallyrows_test.go:244: the tasks page opens with "1 chat · 148 subtasks since aug 12 · $34.10", which the chat manual does not quote
FAIL
The chat manual quotes aug 11, not aug 12:
$ grep -n "subtasks since" internal/manual/chat/tasks.md
2641:`1 chat · 148 subtasks since aug 11 · $34.10`. The count is every row the time window
The test fixes now to 2026-08-25 13:11 in time.FixedZone("fixture", -4h) and builds a 15-day window with session.LastDays(now, 15). The window-start it renders depends on the host timezone, not the fixture zone, so the same source produces a different date on different machines — green on a UTC CI runner, red on a UTC+8 developer box.
Replication
Deterministic (no model). Runs at a reviewer's desk this minute; the only variable is $TZ:
# red — window start renders as "aug 12"
TZ=Asia/Shanghai go test ./internal/tui3/ -run TestTheManualQuotesBothTasksOpeningHeadingsExactly -count=1
# green — window start renders as "aug 11", which is what the manual quotes
TZ=UTC go test ./internal/tui3/ -run TestTheManualQuotesBothTasksOpeningHeadingsExactly -count=1
TZ=America/New_York go test ./internal/tui3/ -run TestTheManualQuotesBothTasksOpeningHeadingsExactly -count=1
The band that passes is roughly the author's own: a fixture now of 2026-08-25 13:11 -04:00 stays on Aug 25 after converting to a local zone at or west of UTC, so the 15-day window starts Aug 11; converted into UTC+8 it rolls to Aug 26, and the window starts Aug 12.
Field (real models). None needed — the defect is a pure, deterministic rendering path with a fixed clock. The product-facing shadow of the same cause: on a host east of UTC the /tasks page header N subtasks since <date> (and the usage/spend window it shares) is anchored to the host's local calendar day rather than to the time it was handed, so "last 15 days" can start a day later than the window actually filtered on.
Where
TestTheManualQuotesBothTasksOpeningHeadingsExactly—internal/tui3/tallyrows_test.go(searchsubtasks since). It builds the heading with a fixed-zonenowand asserts the chat manual quotes it verbatim.tasksWindowStart—internal/tui3/tasksplace.go(searchsince/win.Normalized()), which formatswin.FromasJan 2lowercased.LastDaysand the root causeusageBucketStart—internal/session/usage_spend.go(searchat = at.Local()).usageBucketStarttruncates to midnight inat.Local(), i.e. the host timezone, discarding the location the passed-intime.Timealready carries.LastDaysbuilds its window off that, so the window start is host-timezone-dependent.- The quoted value —
internal/manual/chat/tasks.md(searchsubtasks since), which hardcodesaug 11. Bothtasks.mdand the window reading were introduced together in senior-dev: read its window from codeaf offline, refuse tiny models, keep progress on compaction #1791.
The fix
The window start a given now produces should be a function of that now and nothing else, so the same inputs render the same date on every machine. Make usageBucketStart truncate in the given time's own location (at.Location()) rather than at.Local(): a function handed an explicit now time.Time should honor the zone that time already carries, and a caller that genuinely wants the host's wall-clock day can pass now.Local(). The test then reads the same aug 11 under every $TZ, and the manual's quoted aug 11 is correct as written.
(If instead the product deliberately wants the viewer's host-local calendar day, then the test is the thing that is wrong — it must construct now and assert in one explicit zone rather than leaking $TZ. Either way the current state is a test that is green only in the author's timezone, which is the bug.)
Acceptance
- Unit:
TestTheManualQuotesBothTasksOpeningHeadingsExactlyrenders the identical heading —1 chat · 148 subtasks since aug 11 · $34.10— underTZ=UTC,TZ=Asia/ShanghaiandTZ=America/New_York, so the pass no longer depends on$TZ. (One deterministic test, no model: this behavior shows fully at the unit boundary, so no e2e door is involved.) - Unit: a direct check that
session.LastDays(now, 15).From, for anowcarrying a fixed non-UTC zone, is the same calendar day regardless of the host$TZunder which the test runs. - The manual page
internal/manual/chat/tasks.mdquotes the one deterministic value, and the change entry'sinvalidatesnames the belief that the tasks-pagesince <date>is host-timezone-independent today.
Activity
tasks: pin every tasks-page fixture clock to local zone, not just one (…