From ee5c44d9d710e0fded7e92b7d81947f673f049f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 27 Sep 2026 13:27:51 +0200 Subject: [PATCH] fix(coach): name a night in the prose by the key the sleep table uses The Coach's recent nights carried the wake instant in UTC as their date, while the sleep table under the answer keys a night by the local day it ends on. East of UTC the two named different days for the same night. The snapshot now gives the night's own key. --- .../__tests__/sleep-block-night-key.test.ts | 44 +++++++++++++++++++ .../ai/coach/snapshot-blocks/sleep-block.ts | 6 ++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/lib/ai/coach/snapshot-blocks/__tests__/sleep-block-night-key.test.ts diff --git a/src/lib/ai/coach/snapshot-blocks/__tests__/sleep-block-night-key.test.ts b/src/lib/ai/coach/snapshot-blocks/__tests__/sleep-block-night-key.test.ts new file mode 100644 index 000000000..6bac1aa00 --- /dev/null +++ b/src/lib/ai/coach/snapshot-blocks/__tests__/sleep-block-night-key.test.ts @@ -0,0 +1,44 @@ +/** + * v1.39.4 — the Coach names a night by the same key as the sleep table + * under its answer: the local day the night ends on. + */ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/logging/context", () => ({ annotate: vi.fn() })); + +import { reconstructSleepNights } from "@/lib/analytics/sleep-night"; +import { buildSleepTimelineBlock } from "../sleep-block"; + +const TZ = "Pacific/Auckland"; + +describe("buildSleepTimelineBlock — night keys", () => { + it("keys a night east of UTC by its local wake day, as the table does", () => { + // Woke at 07:00 on 27 September in Auckland: still the 26th in UTC. + const rows = [ + { + value: 450, + measuredAt: new Date("2026-09-26T19:00:00.000Z"), + sleepStage: null, + source: "APPLE_HEALTH" as const, + }, + ]; + const snapshot: Record = {}; + buildSleepTimelineBlock({ + sleepRows: rows, + sourcePriorityJson: null, + userTz: TZ, + recentCutoff: new Date("2026-09-01T00:00:00.000Z"), + snapshot, + metrics: new Set(), + counts: {}, + registerBlock: () => {}, + groundingValues: new Map(), + }); + const recent = ( + snapshot.sleep as { timeline: { recent: Array<{ date: string }> } } + ).timeline.recent; + const tableKey = reconstructSleepNights(rows, TZ, null)[0].night; + expect(tableKey).toBe("2026-09-27"); + expect(recent[0].date).toBe(tableKey); + }); +}); diff --git a/src/lib/ai/coach/snapshot-blocks/sleep-block.ts b/src/lib/ai/coach/snapshot-blocks/sleep-block.ts index b67802d55..c90da3d84 100644 --- a/src/lib/ai/coach/snapshot-blocks/sleep-block.ts +++ b/src/lib/ai/coach/snapshot-blocks/sleep-block.ts @@ -97,7 +97,11 @@ export function buildSleepTimelineBlock( .filter((n) => n.measuredAt >= recentCutoff) .map((n) => { const row: Record = { - date: n.measuredAt, + // The night's own key, the local day it ends on: the key the + // sleep table under the answer and every sleep surface use. The + // wake instant in UTC named the day before east of UTC, so the + // prose and the table disagreed about which night it was. + date: n.night, weekday: tzWeekday(n.measuredAt, userTz), minutes: Math.round(n.asleepMinutes), };