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:
Marc-André Bombeck committed 2026-07-29 14:15:48 +02:00
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);
});
});
@@ -0,0 +1,138 @@
/**
* Issue #664 — the reminder worker must carry one exact scheduled occurrence
* through recurrence, persisted intake lookup, dispatch metadata, and the
* durable push-attempt dedup ledger when its window crosses local midnight.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
vi.mock("@/lib/notifications/dispatcher", () => ({
dispatchNotification: vi.fn().mockResolvedValue({
dispatched: true,
channelsAttempted: 1,
channelsSucceeded: 1,
}),
}));
vi.mock("@/lib/rollups/medication-compliance-rollups", () => ({
recomputeMedicationComplianceForEvent: vi.fn().mockResolvedValue(undefined),
}));
const { dispatchNotification } =
await import("@/lib/notifications/dispatcher");
const { handleReminderCheck } =
await import("@/lib/jobs/reminder/medication-reminder-check");
const USER_ID = "cross-midnight-reminder-user";
const TZ = "Europe/Berlin";
const PRIOR_OCCURRENCE = new Date("2026-07-28T21:45:00.000Z"); // 23:45 CEST
async function seedMedication(): Promise<string> {
const prisma = getPrismaClient();
const medication = await prisma.medication.create({
data: {
userId: USER_ID,
name: "Cross-midnight tablet",
dose: "1 tablet",
active: true,
asNeeded: false,
notificationsEnabled: true,
startsOn: new Date("2026-07-01T00:00:00.000Z"),
schedules: {
create: {
windowStart: "23:45",
windowEnd: "00:30",
timesOfDay: ["23:45"],
rrule: "FREQ=DAILY",
scheduleType: "SCHEDULED",
},
},
},
select: { id: true },
});
return medication.id;
}
async function tickAt(instant: string): Promise<void> {
vi.setSystemTime(new Date(instant));
await handleReminderCheck([]);
}
beforeEach(async () => {
vi.useFakeTimers({ toFake: ["Date"] });
vi.mocked(dispatchNotification).mockClear();
const prisma = getPrismaClient();
await truncateAllTables(prisma);
await prisma.user.create({
data: {
id: USER_ID,
username: "cross-midnight-reminder",
email: "cross-midnight-reminder@example.test",
timezone: TZ,
locale: "en",
},
});
});
describe("medication reminder cross-midnight persistence", () => {
it("suppresses an exact taken occurrence after midnight without suppressing the next dose", async () => {
const prisma = getPrismaClient();
const medicationId = await seedMedication();
await prisma.medicationIntakeEvent.create({
data: {
userId: USER_ID,
medicationId,
scheduledFor: PRIOR_OCCURRENCE,
takenAt: new Date("2026-07-28T21:50:00.000Z"),
skipped: false,
source: "WEB",
},
});
await tickAt("2026-07-28T22:15:00.000Z"); // Jul 29 00:15 CEST
expect(dispatchNotification).not.toHaveBeenCalled();
await tickAt("2026-07-29T21:50:00.000Z"); // next distinct 23:45 dose
expect(dispatchNotification).toHaveBeenCalledTimes(1);
expect(dispatchNotification).toHaveBeenCalledWith(
expect.objectContaining({
metadata: expect.objectContaining({
medicationId,
scheduledAt: "2026-07-29T21:45:00.000Z",
}),
}),
);
});
it("dispatches an untaken prior-day occurrence once across worker retries", async () => {
const prisma = getPrismaClient();
const medicationId = await seedMedication();
await tickAt("2026-07-28T22:15:00.000Z");
await tickAt("2026-07-28T22:30:00.000Z");
expect(dispatchNotification).toHaveBeenCalledTimes(1);
expect(dispatchNotification).toHaveBeenCalledWith(
expect.objectContaining({
metadata: expect.objectContaining({
medicationId,
scheduledAt: PRIOR_OCCURRENCE.toISOString(),
}),
}),
);
const anchors = await prisma.pushAttempt.findMany({
where: {
userId: USER_ID,
channel: "DEDUP",
eventType: "MEDICATION_REMINDER",
},
select: { reason: true },
});
expect(anchors).toEqual([
{
reason: expect.stringContaining("23:45:YELLOW:2026-07-28"),
},
]);
});
});