package app import ( "testing" "time" ) func TestRefreshCoordinatorLifecycle(t *testing.T) { now := time.Date(2026, time.March, 9, 21, 0, 0, 0, time.UTC) rc := newRefreshCoordinator(10*time.Minute, 2*time.Second, 60*time.Second) if !rc.shouldRefresh(now, false, false) { t.Fatalf("expected refresh when cache is empty") } if !rc.beginRefresh(now, false, false) { t.Fatalf("expected beginRefresh=true for empty cache") } if rc.shouldRefresh(now, false, false) { t.Fatalf("must not refresh while refresh is in progress") } rc.finishSuccess(now) snap := rc.snapshot(now) if snap.RefreshInProgress { t.Fatalf("refresh must be finished after success") } if snap.Stale { t.Fatalf("fresh snapshot must not be stale") } if snap.LastError != "" || snap.NextRetryAt != "" { t.Fatalf("success must clear error and retry metadata: %#v", snap) } if rc.shouldRefresh(now.Add(5*time.Minute), false, true) { t.Fatalf("fresh cache should not refresh yet") } if !rc.shouldRefresh(now.Add(11*time.Minute), false, true) { t.Fatalf("stale cache should refresh") } } func TestRefreshCoordinatorBackoffAndReset(t *testing.T) { start := time.Date(2026, time.March, 9, 21, 5, 0, 0, time.UTC) rc := newRefreshCoordinator(10*time.Minute, 2*time.Second, 60*time.Second) type step struct { at time.Time expected time.Duration } steps := []step{ {at: start, expected: 2 * time.Second}, {at: start.Add(2 * time.Second), expected: 4 * time.Second}, {at: start.Add(6 * time.Second), expected: 8 * time.Second}, {at: start.Add(14 * time.Second), expected: 16 * time.Second}, {at: start.Add(30 * time.Second), expected: 32 * time.Second}, {at: start.Add(62 * time.Second), expected: 60 * time.Second}, } for i, st := range steps { rc.finishError("probe failed", st.at) got := rc.nextRetryAt().Sub(st.at) if got != st.expected { t.Fatalf("step=%d unexpected backoff: got=%s want=%s", i+1, got, st.expected) } if rc.lastError() == "" { t.Fatalf("step=%d expected non-empty lastError", i+1) } } rc.finishSuccess(start.Add(2 * time.Minute)) if rc.consecutiveErrors() != 0 { t.Fatalf("expected consecutiveErrors reset, got %d", rc.consecutiveErrors()) } if !rc.nextRetryAt().IsZero() { t.Fatalf("expected nextRetryAt reset") } if rc.lastError() != "" { t.Fatalf("expected lastError reset, got %q", rc.lastError()) } }