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), };