mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
test(medications): make compliance and analytics tests timezone-stable
These tests passed in Europe/Berlin but failed under UTC (or against the shipped engine) because they leant on the host clock: - intake route test: the mocked user had no timezone, so slot resolution fell back to the host TZ and the 07:00 schedule slot only matched in Berlin. Pin the user to Europe/Berlin. - health-score: 30 daily takes seeded at an unaligned now-i*DAY shifted their Berlin hour with the runner TZ, dropping them out of the 08:00-10:00 on-time band. Anchor each take to a fixed 07:30 UTC. - sleep-stages: the night sat on the trailing-30-day window edge for a fixed calendar instant. Anchor it to now-5d. - weekly-compliance: not a timezone issue. The dose-window engine treats a weekly dose inside the 4-day overdue tail as still-takeable, not missed, so skipping the most-recent Monday no longer lowers the rate. Skip an older, settled-miss Monday instead; the gap assertion stands.
This commit is contained in:
1 parent
dfb6a7f298
commit
050ddbecb2
4 files changed
+32
-6
No files matched your search
@@ -96,7 +96,14 @@ import { getSession } from "@/lib/auth/session";
|
|||||||
|
|
||||||
const SESSION_OK = {
|
const SESSION_OK = {
|
||||||
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
||||||
user: { id: "user-1", username: "marc", role: "USER" as const },
|
user: {
|
||||||
|
id: "user-1",
|
||||||
|
username: "marc",
|
||||||
|
role: "USER" as const,
|
||||||
|
// Pin the user timezone so slot-instant resolution is deterministic
|
||||||
|
// regardless of the host TZ (CI runs in UTC, local in Europe/Berlin).
|
||||||
|
timezone: "Europe/Berlin",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MED_OK = { id: "med-1", userId: "user-1" };
|
const MED_OK = { id: "med-1", userId: "user-1" };
|
||||||
|
|||||||
@@ -163,7 +163,12 @@ describe("GET /api/analytics — Health Score", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
|
// Anchor the take to a fixed wall-clock instant that lands inside the
|
||||||
|
// 08:00–10:00 Berlin window (07:30 UTC ≈ 09:30 Berlin) so the slot
|
||||||
|
// matcher reads "on-time" deterministically regardless of host TZ —
|
||||||
|
// an unaligned `now - i*DAY` shifts its Berlin hour with the runner TZ.
|
||||||
const scheduledFor = new Date(now - i * DAY);
|
const scheduledFor = new Date(now - i * DAY);
|
||||||
|
scheduledFor.setUTCHours(7, 30, 0, 0);
|
||||||
await prisma.medicationIntakeEvent.create({
|
await prisma.medicationIntakeEvent.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -283,7 +288,12 @@ describe("GET /api/analytics — Health Score", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
for (let i = 0; i < 30; i++) {
|
for (let i = 0; i < 30; i++) {
|
||||||
|
// Anchor the take to a fixed wall-clock instant that lands inside the
|
||||||
|
// 08:00–10:00 Berlin window (07:30 UTC ≈ 09:30 Berlin) so the slot
|
||||||
|
// matcher reads "on-time" deterministically regardless of host TZ —
|
||||||
|
// an unaligned `now - i*DAY` shifts its Berlin hour with the runner TZ.
|
||||||
const scheduledFor = new Date(now - i * DAY);
|
const scheduledFor = new Date(now - i * DAY);
|
||||||
|
scheduledFor.setUTCHours(7, 30, 0, 0);
|
||||||
await prisma.medicationIntakeEvent.create({
|
await prisma.medicationIntakeEvent.create({
|
||||||
data: {
|
data: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|||||||
@@ -100,7 +100,11 @@ describe("GET /api/analytics — sleep-stage aggregation", () => {
|
|||||||
// One night with the four iOS-16+ stages tagged. The analytics
|
// One night with the four iOS-16+ stages tagged. The analytics
|
||||||
// route should surface a single Measurement-summary datapoint
|
// route should surface a single Measurement-summary datapoint
|
||||||
// (~480 minutes total) plus a per-stage breakdown.
|
// (~480 minutes total) plus a per-stage breakdown.
|
||||||
const baseDate = new Date("2026-05-09T01:00:00.000Z");
|
// Anchor the night comfortably inside the route's trailing 30-day
|
||||||
|
// window (relative to `now`) so the test never drifts onto the
|
||||||
|
// window edge as the calendar advances or the host TZ shifts.
|
||||||
|
const baseDate = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000);
|
||||||
|
baseDate.setUTCHours(1, 0, 0, 0);
|
||||||
const stages: Array<{
|
const stages: Array<{
|
||||||
stage: "IN_BED" | "CORE" | "DEEP" | "REM";
|
stage: "IN_BED" | "CORE" | "DEEP" | "REM";
|
||||||
min: number;
|
min: number;
|
||||||
|
|||||||
@@ -212,15 +212,20 @@ describe("weekly injectable compliance (Mounjaro 0% regression)", () => {
|
|||||||
rrule: "FREQ=WEEKLY;BYDAY=MO",
|
rrule: "FREQ=WEEKLY;BYDAY=MO",
|
||||||
windowStart: "07:30",
|
windowStart: "07:30",
|
||||||
});
|
});
|
||||||
// Log only 12 of the trailing 13 Mondays (skip the most recent).
|
// Take the most-recent Monday on time, then skip ONE older Monday
|
||||||
|
// (`lastMonday − 7d`, which sits 9 days before the pinned now — past
|
||||||
|
// the weekly 4-day overdue tail, so it counts as definitively missed
|
||||||
|
// rather than still-takeable) and take the 11 Mondays before it. The
|
||||||
|
// gap is a real, settled miss, not a head-of-window overdue slot.
|
||||||
|
await logWeeklyIntakes(id, lastMonday, 1);
|
||||||
await logWeeklyIntakes(
|
await logWeeklyIntakes(
|
||||||
id,
|
id,
|
||||||
new Date(lastMonday.getTime() - 7 * DAY_MS),
|
new Date(lastMonday.getTime() - 14 * DAY_MS),
|
||||||
12,
|
11,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { compliance90 } = await readCompliance(id);
|
const { compliance90 } = await readCompliance(id);
|
||||||
// 12 of 13 expected → ~92%, definitively non-zero and below 100.
|
// 12 taken of 13 expected → ~92%, definitively non-zero and below 100.
|
||||||
expect(compliance90.rate).toBeGreaterThan(80);
|
expect(compliance90.rate).toBeGreaterThan(80);
|
||||||
expect(compliance90.rate).toBeLessThan(100);
|
expect(compliance90.rate).toBeLessThan(100);
|
||||||
expect(compliance90.missed).toBeGreaterThanOrEqual(1);
|
expect(compliance90.missed).toBeGreaterThanOrEqual(1);
|
||||||
|
|||||||
Reference in new issue
Block a user