summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/api/google_calendar.go19
-rw-r--r--internal/api/google_calendar_test.go44
2 files changed, 62 insertions, 1 deletions
diff --git a/internal/api/google_calendar.go b/internal/api/google_calendar.go
index 44457bf..a69f734 100644
--- a/internal/api/google_calendar.go
+++ b/internal/api/google_calendar.go
@@ -117,7 +117,24 @@ func NewGoogleCalendarClient(ctx context.Context, credentialsFile, calendarIDs,
}
func (c *GoogleCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error) {
- t := time.Now().Format(time.RFC3339)
+ // TimeMin must be the start of today in the DISPLAY timezone, not raw
+ // time.Now() (server/UTC time). Google's API filters TimeMin against
+ // each event's END time (exclusive), so this is also what makes an
+ // already-ended-today event "past" vs excluded entirely -- using
+ // time.Now() here meant, for a UTC-10 display timezone, the cutoff
+ // landed mid-afternoon the PREVIOUS Hawaii-local day (server midnight
+ // UTC is 2pm HST the day before), not midnight of the actual display
+ // day. That's what caused the 2026-08-06 report: past-today events
+ // missing entirely (silently never fetched into the cache in the first
+ // place, not a client-side bug) and the whole today/tomorrow bucketing
+ // downstream skewed by the same ~10h gap.
+ tz := c.displayTZ
+ if tz == nil {
+ tz = time.UTC
+ }
+ nowInTZ := time.Now().In(tz)
+ todayStart := time.Date(nowInTZ.Year(), nowInTZ.Month(), nowInTZ.Day(), 0, 0, 0, 0, tz)
+ t := todayStart.Format(time.RFC3339)
fetchOne := func(calendarID string) []models.CalendarEvent {
events, err := c.srv.Events.List(calendarID).ShowDeleted(false).
diff --git a/internal/api/google_calendar_test.go b/internal/api/google_calendar_test.go
index 3cf0dbd..92fdea3 100644
--- a/internal/api/google_calendar_test.go
+++ b/internal/api/google_calendar_test.go
@@ -272,3 +272,47 @@ func TestGetUpcomingEvents_APIError_ReturnsEmptyNotError(t *testing.T) {
t.Errorf("expected 0 events on API error, got %d", len(events))
}
}
+
+// Regression test for the 2026-08-06 incident: GetUpcomingEvents used
+// time.Now() (server/UTC time) as TimeMin instead of the start of today in
+// the display timezone. Google's API filters TimeMin against each event's
+// END time, so for Pacific/Honolulu (UTC-10) that cutoff landed mid-afternoon
+// the PREVIOUS Hawaii-local day, not midnight of the actual display day --
+// silently excluding every already-ended-today event from ever being
+// fetched into the cache, and skewing the whole today/tomorrow split
+// downstream by the same ~10h gap. This would have caught it: raw
+// time.Now() fails both assertions below for any negative-UTC-offset zone.
+func TestGetUpcomingEvents_TimeMinIsStartOfTodayInDisplayTimezone(t *testing.T) {
+ var capturedTimeMin string
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ capturedTimeMin = r.URL.Query().Get("timeMin")
+ w.Header().Set("Content-Type", "application/json")
+ _, _ = w.Write([]byte(`{"kind":"calendar#events","items":[]}`))
+ }))
+ defer server.Close()
+
+ tz, err := time.LoadLocation("Pacific/Honolulu")
+ if err != nil {
+ t.Fatalf("failed to load Pacific/Honolulu: %v", err)
+ }
+ client := newTestGoogleCalendarClient(t, server, []string{"primary"}, tz)
+ if _, err := client.GetUpcomingEvents(context.Background(), 10); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ got, err := time.Parse(time.RFC3339, capturedTimeMin)
+ if err != nil {
+ t.Fatalf("captured timeMin %q isn't valid RFC3339: %v", capturedTimeMin, err)
+ }
+ gotInTZ := got.In(tz)
+
+ if gotInTZ.Hour() != 0 || gotInTZ.Minute() != 0 || gotInTZ.Second() != 0 {
+ t.Errorf("timeMin = %v (in %s), want midnight", gotInTZ, tz)
+ }
+
+ nowInTZ := time.Now().In(tz)
+ if gotInTZ.Year() != nowInTZ.Year() || gotInTZ.YearDay() != nowInTZ.YearDay() {
+ t.Errorf("timeMin day = %s, want today (%s) in %s",
+ gotInTZ.Format("2006-01-02"), nowInTZ.Format("2006-01-02"), tz)
+ }
+}