test(medication-compliance-rollups): hook, reader, and tz parity

Unit-covers the writer's `(scheduled, taken, skipped)` upsert, the
zero-event delete, idempotency across repeated invocations, the
tz-anchored UTC window math, and the Berlin vs Los Angeles bucket
parity for an instant that lands on different local days. Reader
tests pin the trailing-window zero-fill plus the per-medication
fold-to-day. Boot-backfill discovery tests pin the singleton-keyed
enqueue. Route tests extend the existing harness with the new
coverage probe — rollup-tier read on coverage, live fallback on
miss — without touching the byte-stable response shape.
This commit is contained in:
Marc-André Bombeck committed 2026-05-18 12:30:35 +02:00
1 parent dfdf6be598
commit 7657b848f0
2 files changed
+455

No files matched your search

@@ -9,10 +9,20 @@ vi.mock("@/lib/db", () => ({
update: vi.fn(),
},
medication: { update: vi.fn() },
medicationComplianceRollup: {
findFirst: vi.fn(),
findMany: vi.fn(),
upsert: vi.fn(),
deleteMany: vi.fn(),
},
$transaction: vi.fn(),
},
}));
vi.mock("@/lib/jobs/boss-instance", () => ({
getGlobalBoss: () => null,
}));
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
vi.mock("@/lib/auth/audit", () => ({
@@ -52,6 +62,15 @@ beforeEach(() => {
vi.mocked(prisma.medicationIntakeEvent.findMany).mockResolvedValue(
[] as never,
);
// v1.4.39 W-MED — default the coverage probe to "uncovered" so the
// legacy compliance test still exercises the live-fallback branch
// and finds the legacy mocked intake events.
vi.mocked(prisma.medicationComplianceRollup.findFirst).mockResolvedValue(
null,
);
vi.mocked(prisma.medicationComplianceRollup.findMany).mockResolvedValue(
[] as never,
);
});
describe("GET /api/medications/intake", () => {
@@ -154,6 +173,7 @@ describe("POST /api/medications/intake", () => {
id: "e1",
userId: "user-1",
medicationId: "m1",
scheduledFor: new Date("2026-05-18T10:00:00.000Z"),
} as never);
vi.mocked(prisma.medicationIntakeEvent.update).mockResolvedValue({
id: "e1",
@@ -168,3 +188,56 @@ describe("POST /api/medications/intake", () => {
});
});
});
describe("v1.4.39 W-MED — compliance rollup read swap", () => {
it("reads the rollup tier when coverage is present", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
// Coverage probe returns "at least one row" so the route reads
// from the rollup tier rather than falling back to the live
// aggregator.
vi.mocked(prisma.medicationComplianceRollup.findFirst).mockResolvedValue({
day: "2026-05-18",
} as never);
vi.mocked(prisma.medicationComplianceRollup.findMany).mockResolvedValue([
{ day: "2026-05-18", scheduled: 3, taken: 2, skipped: 1 },
] as never);
const res = await GET(
new NextRequest(
"http://localhost/api/medications/intake?scope=compliance&days=7",
),
);
expect(res.status).toBe(200);
const body = (await res.json()) as {
data: Array<{ date: string; scheduled: number; taken: number }>;
};
expect(body.data).toHaveLength(7);
const today = body.data[body.data.length - 1];
expect(today.scheduled).toBe(3);
expect(today.taken).toBe(2);
// The legacy live aggregator must not have run when coverage is hot.
expect(prisma.medicationIntakeEvent.findMany).not.toHaveBeenCalled();
});
it("falls back to the live aggregator on coverage miss", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.medicationComplianceRollup.findFirst).mockResolvedValue(
null,
);
vi.mocked(prisma.medicationIntakeEvent.findMany).mockResolvedValue([
{
scheduledFor: new Date(),
takenAt: new Date(),
skipped: false,
},
] as never);
const res = await GET(
new NextRequest(
"http://localhost/api/medications/intake?scope=compliance&days=7",
),
);
expect(res.status).toBe(200);
expect(prisma.medicationIntakeEvent.findMany).toHaveBeenCalled();
});
});