Skip to content
27 changes: 27 additions & 0 deletions docs/changes/unreleased/1809-tz-stable-tasks-heading-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
kind: internal
title: the tasks-page test fixtures pin their clocks to the local zone, so they no longer fail east of UTC
pr: 1809
surface: [chat]
invalidates:
- >-
The tasks-page tests were believed to be deterministic and host-independent.
They built their fixture clocks in a fixed -04:00 zone while the tasks window
buckets days in the viewer's LOCAL zone (usageBucketStart, and the "THE DAY
IS THE WRITER'S" law on SpendToday), so a date the tests assert — the chat
manual's `since aug 11`, and the `shift+← aug 2 – aug 25 →` window control —
shifted by a day once the foreign offset was read as local on a host far
enough east: green on a UTC runner, red on a UTC+8 box (#1808). The
affected fixture clocks now use time.Local, fixing the fixed-offset
mismatch; a pre-existing bug at skipped-midnight DST transitions, such as
America/Santiago on 2026-09-06, remains unresolved.
---

No product behavior changed: the window following the viewer's local calendar
day is deliberate and stays. The tests were host-dependent because each fixture
injected a fixed -04:00 clock and then asserted dates that only matched in the
author's timezone band — one shared cause across tallyrows_test.go,
tasksplace_test.go, tasksparked_test.go, tasksconversation_test.go and
taskstable_test.go, two of whose tests were already red on a UTC+8 box.
Verified identical green from Pacific/Pago_Pago (UTC-11) to Pacific/Kiritimati
(UTC+14).
10 changes: 9 additions & 1 deletion internal/tui3/tallyrows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,15 @@ func TestTheTasksFootStillCountsEveryPieceByItsActualState(t *testing.T) {
// branches and the corpus is asked for what they produced, so neither heading can
// be restated as a literal that drifts.
func TestTheManualQuotesBothTasksOpeningHeadingsExactly(t *testing.T) {
loc := time.FixedZone("fixture", -4*60*60)
// The window buckets days in the viewer's LOCAL zone, on purpose: spend and
// subtask counts are attributed to the writer's own calendar day (session's
// usageBucketStart, and the "THE DAY IS THE WRITER'S" law on SpendToday). So
// the fixture clock has to live in that same zone, or the rendered `since`
// date — which the chat manual quotes verbatim — shifts by a day on any host
// where a fixed foreign offset crosses midnight once read as local. A
// `-04:00` clock did exactly that: green on a UTC runner, red on a UTC+8 box,
// `aug 11` versus `aug 12` (#1808).
loc := time.Local
now := time.Date(2026, time.August, 25, 13, 11, 0, 0, loc)
win := session.LastDays(now, 15)

Expand Down
2 changes: 1 addition & 1 deletion internal/tui3/tasksconversation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import (
// is the depth the old page could not draw. `thor clips` finished yesterday and
// has nothing under it but one row.
func tasksChatFixture() (session.World, session.UsageWindow, time.Time) {
loc := time.FixedZone("fixture", -4*60*60)
loc := time.Local // #1808: the window buckets in local time, so a fixed foreign offset would shift the dates by $TZ
now := time.Date(2026, time.September, 6, 13, 0, 0, 0, loc)
ago := func(d time.Duration) time.Time { return now.Add(-d) }

Expand Down
2 changes: 1 addition & 1 deletion internal/tui3/tasksparked_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
// one piece of work waiting on a person, and two admitted behind it that nothing
// has started.
func tasksParkedFixture() (session.World, session.UsageWindow, time.Time) {
loc := time.FixedZone("fixture", -4*60*60)
loc := time.Local // #1808: the window buckets in local time, so a fixed foreign offset would shift the dates by $TZ
now := time.Date(2026, time.September, 2, 23, 52, 0, 0, loc)
row := session.SessionRow{ID: "room-a", Title: "The Task Surface", Project: "codeaf", Open: true, Live: false}
entry := func(id, label, status string) session.TaskIndexEntry {
Expand Down
8 changes: 4 additions & 4 deletions internal/tui3/tasksplace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
var tasksWidths = []int{60, 80, 120, 200}

func tasksFixture() (session.World, session.UsageWindow, time.Time) {
loc := time.FixedZone("fixture", -4*60*60)
loc := time.Local // #1808: the window buckets in local time, so a fixed foreign offset would shift the dates by $TZ
now := time.Date(2026, time.August, 25, 13, 11, 0, 0, loc)
at := func(day, hour int) time.Time { return time.Date(2026, time.August, day, hour, 0, 0, 0, loc) }
entry := func(id, label, status string, ended time.Time, cost float64) session.TaskIndexEntry {
Expand Down Expand Up @@ -381,7 +381,7 @@ func TestTasksChangedSinceCountsOnlyLandedWorkAfterTheLook(t *testing.T) {
// that landed today with three workers under it, and a second root beside it
// that is nobody's family.
func tasksFamilyFixture() (session.World, session.UsageWindow, time.Time) {
loc := time.FixedZone("fixture", -4*60*60)
loc := time.Local // #1808: the window buckets in local time, so a fixed foreign offset would shift the dates by $TZ
now := time.Date(2026, time.August, 25, 13, 11, 0, 0, loc)
at := func(hour int) time.Time { return time.Date(2026, time.August, 25, hour, 0, 0, 0, loc) }
row := session.SessionRow{ID: "room-a", Title: "The Split", Project: "codeaf", Open: true}
Expand Down Expand Up @@ -573,7 +573,7 @@ func TestVisibleChildrenKeepTheirOwnSectionsWhenTheRootIsAbsent(t *testing.T) {
// detail beside each — and arriving OUT of time order, which is what the world
// scan hands this place.
func tasksPolishFixture() (session.World, session.UsageWindow, time.Time) {
loc := time.FixedZone("fixture", -4*60*60)
loc := time.Local // #1808: the window buckets in local time, so a fixed foreign offset would shift the dates by $TZ
now := time.Date(2026, time.September, 2, 23, 3, 0, 0, loc)
done := func(id, label, outcome string, files int, cost float64, ago time.Duration) session.TaskIndexEntry {
return session.TaskIndexEntry{
Expand Down Expand Up @@ -956,7 +956,7 @@ func TestTheTaskPageNamesTheConversationItCameOutOf(t *testing.T) {
// [homeName], whose title case has raised the id's first letter), so the
// composer keys on the trimmed title equal to the row's own id, folded.
func TestAConversationWithNoTitleIsCalledTheWordNotItsId(t *testing.T) {
loc := time.FixedZone("fixture", -4*60*60)
loc := time.Local // #1808: the window buckets in local time, so a fixed foreign offset would shift the dates by $TZ
now := time.Date(2026, time.September, 11, 14, 25, 0, 0, loc)
id := "de9ea39e6f4c18c3"
transcript := "/private/tmp/htw/codeaf/" + id + "/transcript.jsonl"
Expand Down
2 changes: 1 addition & 1 deletion internal/tui3/taskstable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// their headings, and the rows inside one of them have three different prices so
// that descending is distinguishable from arriving.
func tasksTableFixture() (session.World, session.UsageWindow, time.Time) {
loc := time.FixedZone("fixture", -4*60*60)
loc := time.Local // #1808: the window buckets in local time, so a fixed foreign offset would shift the dates by $TZ
now := time.Date(2026, time.September, 11, 12, 0, 0, 0, loc)
ago := func(d time.Duration) time.Time { return now.Add(-d) }
work := func(room, id, label, status string, ended time.Time, files int, cost float64) session.TaskIndexEntry {
Expand Down
Loading