From d5f473d6728dd069d185a6944903ed766212022d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 29 Jul 2026 14:19:03 +0200 Subject: [PATCH] test(01-11): specify rolling weekly overdue identity - pin the prior-day Mounjaro occurrence across local midnight and DST - cover resolution state, band end, schedule siblings, and era isolation --- .../scheduling/__tests__/next-due.test.ts | 141 ++++++++++++++++++ .../scheduling/__tests__/recurrence.test.ts | 38 +++++ 2 files changed, 179 insertions(+) diff --git a/src/lib/medications/scheduling/__tests__/next-due.test.ts b/src/lib/medications/scheduling/__tests__/next-due.test.ts index 99f7c7445..19dd77bf1 100644 --- a/src/lib/medications/scheduling/__tests__/next-due.test.ts +++ b/src/lib/medications/scheduling/__tests__/next-due.test.ts @@ -558,3 +558,144 @@ describe("as-needed (zero schedules) — never due", () => { expect(display).toBeNull(); }); }); + +describe("Mounjaro rolling weekly — canonical overdue band after day change", () => { + const lastTaken = d("2026-07-21T06:00:00.000Z"); // Jul 21 08:00 Berlin + const missedOccurrence = d("2026-07-28T06:00:00.000Z"); // Jul 28 08:00 + const nextOccurrence = d("2026-08-04T06:00:00.000Z"); // Aug 4 08:00 + const medication = makeMedication({ + startsOn: d("2026-01-01T00:00:00.000Z"), + }); + const schedule = makeRollingSchedule({ + id: "mounjaro-weekly", + timesOfDay: ["08:00"], + }); + const identifiedMark = ( + at: Date, + identity: Record, + ): ResolvedSlotMark => + ({ + ...mark(at), + ...identity, + }) as ResolvedSlotMark; + + it("keeps yesterday's unresolved occurrence overdue on the following local day", () => { + const display = computeDisplayDue({ + medication, + schedules: [schedule], + now: d("2026-07-29T10:00:00.000Z"), + userTz: BERLIN, + lastIntakeAt: lastTaken, + resolvedSlots: [], + }); + + expect(display).toMatchObject({ + at: missedOccurrence, + overdue: true, + }); + }); + + it("keeps the occurrence through the exact canonical band end, then closes it", () => { + const atBandEnd = computeDisplayDue({ + medication, + schedules: [schedule], + now: d("2026-08-02T06:00:00.000Z"), + userTz: BERLIN, + lastIntakeAt: lastTaken, + resolvedSlots: [], + }); + expect(atBandEnd).toMatchObject({ + at: missedOccurrence, + overdue: true, + }); + + const afterBandEnd = computeDisplayDue({ + medication, + schedules: [schedule], + now: d("2026-08-02T06:00:00.001Z"), + userTz: BERLIN, + lastIntakeAt: lastTaken, + resolvedSlots: [], + }); + expect(afterBandEnd).toBeNull(); + }); + + it.each(["taken", "skipped", "autoMissed"] as const)( + "an exact %s row suppresses only this occurrence and advances", + (status) => { + const display = computeDisplayDue({ + medication, + schedules: [schedule], + now: d("2026-07-29T10:00:00.000Z"), + userTz: BERLIN, + lastIntakeAt: + status === "taken" + ? d("2026-07-28T06:10:00.000Z") + : lastTaken, + resolvedSlots: [ + identifiedMark(missedOccurrence, { + scheduleId: "mounjaro-weekly", + status, + }), + ], + }); + + expect(display).toMatchObject({ + at: nextOccurrence, + overdue: false, + }); + }, + ); + + it("does not let a sibling schedule's resolved occurrence suppress this schedule", () => { + const sibling = makeRollingSchedule({ + id: "mounjaro-sibling", + timesOfDay: ["10:00"], + windowStart: "10:00", + windowEnd: "10:00", + }); + const display = computeDisplayDue({ + medication, + schedules: [schedule, sibling], + now: d("2026-07-29T10:00:00.000Z"), + userTz: BERLIN, + lastIntakeAt: lastTaken, + resolvedSlots: [ + identifiedMark(missedOccurrence, { + scheduleId: "mounjaro-weekly", + status: "skipped", + }), + ], + }); + + expect(display).toMatchObject({ + at: d("2026-07-28T08:00:00.000Z"), + overdue: true, + scheduleId: "mounjaro-sibling", + }); + }); + + it("does not let an action from the archived era suppress the live-era occurrence", () => { + const liveEraStart = d("2026-07-27T12:00:00.000Z"); + const display = computeDisplayDue({ + medication, + schedules: [schedule], + now: d("2026-07-29T10:00:00.000Z"), + userTz: BERLIN, + lastIntakeAt: lastTaken, + eraStart: liveEraStart, + resolvedSlots: [ + identifiedMark(missedOccurrence, { + scheduleId: "mounjaro-weekly", + actionAt: d("2026-07-27T11:59:59.000Z"), + }), + ], + }); + + expect(display).toMatchObject({ + at: missedOccurrence, + overdue: true, + scheduleId: "mounjaro-weekly", + }); + }); +}); diff --git a/src/lib/medications/scheduling/__tests__/recurrence.test.ts b/src/lib/medications/scheduling/__tests__/recurrence.test.ts index f81918b27..9a355870c 100644 --- a/src/lib/medications/scheduling/__tests__/recurrence.test.ts +++ b/src/lib/medications/scheduling/__tests__/recurrence.test.ts @@ -473,6 +473,44 @@ describe("occurrencesBetween — rolling", () => { }); }); +describe("rolling occurrence identity across a local-day change", () => { + it.each([ + { + label: "ordinary Berlin week", + lastIntake: "2026-07-21T06:00:00.000Z", + now: "2026-07-29T10:00:00.000Z", + expected: "2026-07-28T06:00:00.000Z", + }, + { + label: "Berlin spring DST week", + lastIntake: "2026-03-22T07:00:00.000Z", + now: "2026-03-30T10:00:00.000Z", + expected: "2026-03-29T06:00:00.000Z", + }, + ])( + "retains the unresolved prior-day slot for an $label", + ({ lastIntake, now, expected }) => { + const schedule = makeSchedule({ + id: "mounjaro-weekly", + rollingIntervalDays: 7, + timesOfDay: ["08:00"], + }); + const ctx = makeCtx({ + lastIntakeAt: d(lastIntake), + medication: { startsOn: d("2026-01-01T00:00:00.000Z") }, + }); + + const occurrence = nextOccurrenceAfter(schedule, d(now), ctx); + + expect(occurrence).toMatchObject({ + scheduleId: "mounjaro-weekly", + at: d(expected), + timeOfDay: "08:00", + }); + }, + ); +}); + // ──────────────────────────────────────────────────────────────────── // One-shot // ────────────────────────────────────────────────────────────────────