Files
HealthLog/tests/integration/rollup-keyed-refold-session-tz.test.ts
T
Marc-André Bombeck 769d0334a7 fix(rollups): match the keyed refold's days in UTC whatever the session zone
The keyed refold's day filter and delete compared the naive UTC
measured_at and bucket_start with a timestamptz[] parameter, which goes
through the session time zone; in any zone but UTC neither matched and
the refold left its buckets stale. Both now bind timestamp[] literals.
Keys are refolded in (type, day) order in chunks of 500, and the
rollup-recompute queue describes the runs of buckets a job now covers.
2026-10-21 00:06:21 +02:00

77 lines
2.6 KiB
TypeScript

/**
* The keyed DAY refold does not depend on the session time zone.
*
* `measured_at` and `bucket_start` are naive UTC timestamps. The refold's day
* filter and its delete compared them with a `timestamptz[]` parameter, which
* Postgres converts through the session time zone: outside UTC neither matched
* a single day, so the refold silently left every touched bucket stale. The
* app pins its sessions to UTC, so this runs the pool in another zone on
* purpose.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/db-session", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/db-session")>();
return { ...actual, TIME_ZONE_OPTION: "-c TimeZone=Pacific/Auckland" };
});
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
import { getPrismaClient, truncateAllTables } from "./setup";
import { prisma } from "@/lib/db";
import {
recomputeBucketsForKeys,
recomputeBucketsForMeasurement,
} from "@/lib/rollups/measurement-rollups";
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
});
describe("keyed DAY refold in a non-UTC session", () => {
it("folds a new reading into its day", async () => {
const [{ zone }] = await prisma.$queryRaw<Array<{ zone: string }>>`
SELECT current_setting('TimeZone') AS zone
`;
expect(zone).toBe("Pacific/Auckland");
const user = await getPrismaClient().user.create({
data: { username: "tz-refold", email: "tz-refold@example.test" },
});
const day = new Date("2026-05-10T00:00:00.000Z");
await getPrismaClient().measurement.createMany({
data: [7, 19].map((hour, i) => ({
userId: user.id,
type: "WEIGHT" as const,
value: 80 + i,
unit: "kg",
measuredAt: new Date(day.getTime() + hour * 3_600_000),
})),
});
await recomputeBucketsForMeasurement(user.id, "WEIGHT", day);
// A third reading lands; the refold has to fold it in.
await getPrismaClient().measurement.create({
data: {
userId: user.id,
type: "WEIGHT",
value: 82,
unit: "kg",
measuredAt: new Date(day.getTime() + 21 * 3_600_000),
},
});
await recomputeBucketsForKeys(user.id, [
{ type: "WEIGHT", measuredAt: new Date(day.getTime() + 7 * 3_600_000) },
]);
const rows = await getPrismaClient().measurementRollup.findMany({
where: { userId: user.id, granularity: "DAY" },
});
expect(rows).toHaveLength(1);
expect(rows[0]!.bucketStart.toISOString()).toBe(day.toISOString());
expect(rows[0]!.count).toBe(3);
});
});