mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 23:12:04 +00:00
test(01-11): reproduce cross-midnight reminder identity bug
- pin prior-day occurrence lookup across timezone and DST boundaries - prove real worker dedup and distinct next-dose behavior with PostgreSQL
This commit is contained in:
1 parent
8e944dbcd3
commit
81893ac972
3 files changed
+291
No files matched your search
@@ -643,3 +643,109 @@ describe("handleReminderCheck configured wall-clock phase bounds", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue #664 — exact occurrence identity survives local midnight", () => {
|
||||
function crossMidnightMedication(timezone: string) {
|
||||
const medication = medicationWithSchedule({
|
||||
windowStart: "23:45",
|
||||
windowEnd: "00:30",
|
||||
timesOfDay: ["23:45"],
|
||||
rrule: "FREQ=DAILY",
|
||||
}) as {
|
||||
user: { id: string; timezone: string; locale: string };
|
||||
};
|
||||
medication.user.timezone = timezone;
|
||||
return medication;
|
||||
}
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "UTC",
|
||||
timezone: "UTC",
|
||||
occurrence: "2026-07-28T23:45:00.000Z",
|
||||
tick: "2026-07-29T00:15:00.000Z",
|
||||
},
|
||||
{
|
||||
label: "Berlin summer time",
|
||||
timezone: "Europe/Berlin",
|
||||
occurrence: "2026-07-28T21:45:00.000Z",
|
||||
tick: "2026-07-28T22:15:00.000Z",
|
||||
},
|
||||
{
|
||||
label: "Berlin DST fall-back eve",
|
||||
timezone: "Europe/Berlin",
|
||||
occurrence: "2026-10-24T21:45:00.000Z",
|
||||
tick: "2026-10-24T22:15:00.000Z",
|
||||
},
|
||||
{
|
||||
label: "positive-offset Auckland",
|
||||
timezone: "Pacific/Auckland",
|
||||
occurrence: "2026-07-28T11:45:00.000Z",
|
||||
tick: "2026-07-28T12:15:00.000Z",
|
||||
},
|
||||
])(
|
||||
"keeps an untaken $label 23:45 occurrence actionable after midnight",
|
||||
async ({ timezone, occurrence, tick }) => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(tick));
|
||||
prismaMock.medication.findMany.mockResolvedValue([
|
||||
crossMidnightMedication(timezone),
|
||||
] as never);
|
||||
prismaMock.medicationIntakeEvent.findMany.mockResolvedValue([]);
|
||||
|
||||
await handleReminderCheck([]);
|
||||
|
||||
expect(dispatchNotification).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchNotification).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
scheduledAt: occurrence,
|
||||
timeOfDay: "23:45",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("reads and suppresses the prior-day occurrence, but not tomorrow's distinct dose", async () => {
|
||||
vi.useFakeTimers();
|
||||
const priorOccurrence = new Date("2026-07-28T21:45:00.000Z");
|
||||
const action = {
|
||||
scheduledFor: priorOccurrence,
|
||||
takenAt: new Date("2026-07-28T21:50:00.000Z"),
|
||||
skipped: false,
|
||||
attributionSource: "AUTO",
|
||||
updatedAt: new Date("2026-07-28T21:50:00.000Z"),
|
||||
};
|
||||
prismaMock.medication.findMany.mockResolvedValue([
|
||||
crossMidnightMedication("Europe/Berlin"),
|
||||
] as never);
|
||||
prismaMock.medicationIntakeEvent.findMany.mockResolvedValue([action]);
|
||||
|
||||
vi.setSystemTime(new Date("2026-07-28T22:15:00.000Z")); // 00:15 Jul 29
|
||||
await handleReminderCheck([]);
|
||||
expect(dispatchNotification).not.toHaveBeenCalled();
|
||||
|
||||
const postMidnightRead = prismaMock.medicationIntakeEvent.findMany.mock
|
||||
.calls[0][0] as {
|
||||
where: { scheduledFor: { gte: Date; lte: Date } };
|
||||
};
|
||||
expect(
|
||||
postMidnightRead.where.scheduledFor.gte.getTime(),
|
||||
).toBeLessThanOrEqual(priorOccurrence.getTime());
|
||||
expect(
|
||||
postMidnightRead.where.scheduledFor.lte.getTime(),
|
||||
).toBeGreaterThanOrEqual(priorOccurrence.getTime());
|
||||
|
||||
vi.setSystemTime(new Date("2026-07-29T21:50:00.000Z")); // 23:50 Jul 29
|
||||
await handleReminderCheck([]);
|
||||
expect(dispatchNotification).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchNotification).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
scheduledAt: "2026-07-29T21:45:00.000Z",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -312,3 +312,50 @@ describe("medication reminder — client-managed suppression is APNs-only", () =
|
||||
expect(dispatchNotification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("issue #664 — dedup follows the scheduled occurrence across midnight", () => {
|
||||
it("dispatches an untreated overnight occurrence once and anchors its own local date", async () => {
|
||||
const med = medicationFixture();
|
||||
med.schedules[0] = {
|
||||
...med.schedules[0],
|
||||
windowStart: "23:45",
|
||||
windowEnd: "00:30",
|
||||
timesOfDay: ["23:45"],
|
||||
rrule: "FREQ=DAILY",
|
||||
};
|
||||
med.user.timezone = "Europe/Berlin";
|
||||
prismaMock.medication.findMany.mockResolvedValue([med]);
|
||||
|
||||
// 00:15 and 00:30 on Jul 29 in Berlin are still the Jul 28 23:45
|
||||
// occurrence's open window.
|
||||
await tickAt("2026-07-28T22:15:00.000Z");
|
||||
await tickAt("2026-07-28T22:30:00.000Z");
|
||||
|
||||
expect(dispatchNotification).toHaveBeenCalledTimes(1);
|
||||
expect(pushAttempts).toHaveLength(1);
|
||||
expect(pushAttempts[0]?.reason).toBe(
|
||||
"med:med-1:sched-1:23:45:YELLOW:2026-07-28",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps consecutive 23:45 occurrences distinct", async () => {
|
||||
const { medicationReminderDedupKey } =
|
||||
await import("@/lib/notifications/reminder-dedup");
|
||||
const first = medicationReminderDedupKey({
|
||||
medicationId: "med-1",
|
||||
scheduleId: "sched-1",
|
||||
slotTime: "23:45",
|
||||
phase: "YELLOW",
|
||||
localDate: "2026-07-28",
|
||||
});
|
||||
const second = medicationReminderDedupKey({
|
||||
medicationId: "med-1",
|
||||
scheduleId: "sched-1",
|
||||
slotTime: "23:45",
|
||||
phase: "YELLOW",
|
||||
localDate: "2026-07-29",
|
||||
});
|
||||
|
||||
expect(first).not.toBe(second);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user