Files
HealthLog/tests/integration/local-day-keys.test.ts
T
Marc-André Bombeck 6026a28fe4 fix(dashboard): key streak and compliance days in the person's own zone
`measured_at` and `scheduled_for` are naive UTC timestamps. The dashboard
streak and the medication-compliance backfill keyed them with a single
`AT TIME ZONE tz`, which reads the value as local time and shifts it the
wrong way: in Berlin a reading at 23:30 UTC counted for the day before
instead of the day after, so a streak could break or stretch near
midnight and the backfill recomputed the wrong day.

The streak's measurement days now come from `readActivityDayKeys`: one
existence probe per local day instead of a DISTINCT over a year of
readings. The DAY rollups cannot answer this because their buckets are
UTC days. The two compliance-rollup readers had no caller left and are
removed with the coverage probe that carried the same shift. A guard
fails on any single shift of a naive column into a bound zone.
2026-10-20 03:35:19 +02:00

125 lines
3.7 KiB
TypeScript

/**
* Local day keys from naive UTC timestamps, against a real Postgres.
*
* `measured_at` and `scheduled_for` hold UTC wall-clock time in `timestamp`
* columns. The dashboard streak and the medication-compliance backfill keyed
* them with a single `AT TIME ZONE tz`, which reads the value as local time
* and shifts it the wrong way. Both now agree with `userDayKey`: a reading at
* 23:30 UTC is the next day in Berlin and still the same day in Los Angeles,
* a reading at 03:00 UTC is the previous day in Los Angeles.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
import { readActivityDayKeys } from "@/lib/dashboard/summary-reads";
import { recomputeUserMedicationCompliance } from "@/lib/rollups/medication-compliance-rollups";
import { userDayKey } from "@/lib/tz/format";
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
const prisma = getPrismaClient();
const USER = "local-day-keys";
const now = new Date();
/** UTC midnight ten days back. */
const D = Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate() - 10,
);
const LATE = new Date(D + 23.5 * 3_600_000); // 23:30 UTC on D
const EARLY = new Date(D + 3 * 3_600_000); // 03:00 UTC on D
const key = (ms: number) => new Date(ms).toISOString().slice(0, 10);
beforeEach(async () => {
await truncateAllTables(prisma);
await prisma.user.create({
data: {
id: USER,
username: USER,
email: `${USER}@example.test`,
timezone: "Europe/Berlin",
},
});
});
describe("readActivityDayKeys", () => {
beforeEach(async () => {
await prisma.measurement.createMany({
data: [
{ at: LATE, deleted: false },
{ at: EARLY, deleted: false },
{ at: new Date(D - 3 * 86_400_000), deleted: true },
].map(({ at, deleted }, i) => ({
id: `${USER}-m${i}`,
userId: USER,
type: "WEIGHT" as const,
source: "MANUAL" as const,
value: 80,
unit: "kg",
measuredAt: at,
deletedAt: deleted ? new Date() : null,
})),
});
});
const since = new Date(D - 5 * 86_400_000);
it("keys a late reading onto the next day east of UTC", async () => {
const keys = await readActivityDayKeys(
prisma,
USER,
"Europe/Berlin",
since,
now,
);
expect(keys).toEqual([key(D), key(D + 86_400_000)]);
expect(keys).toEqual(
[EARLY, LATE].map((at) => userDayKey(at, "Europe/Berlin")),
);
});
it("keys an early reading onto the previous day west of UTC", async () => {
const keys = await readActivityDayKeys(
prisma,
USER,
"America/Los_Angeles",
since,
now,
);
expect(keys).toEqual([key(D - 86_400_000), key(D)]);
expect(keys).toEqual(
[EARLY, LATE].map((at) => userDayKey(at, "America/Los_Angeles")),
);
});
});
describe("recomputeUserMedicationCompliance", () => {
it("keys a late dose onto the person's own day", async () => {
const med = await prisma.medication.create({
data: { userId: USER, name: "Ramipril", dose: "5mg", active: true },
});
await prisma.medicationIntakeEvent.create({
data: {
userId: USER,
medicationId: med.id,
scheduledFor: LATE,
takenAt: LATE,
skipped: false,
source: "API",
},
});
await recomputeUserMedicationCompliance(USER, 30, "Europe/Berlin");
const rows = await prisma.medicationComplianceRollup.findMany({
where: { userId: USER },
select: { day: true, scheduled: true, taken: true },
});
expect(rows).toEqual([
{ day: key(D + 86_400_000), scheduled: 1, taken: 1 },
]);
});
});