Files
HealthLog/tests/integration/rollup-coverage-probe.test.ts
Marc-André Bombeck b6da360856 refactor(db): drop the runtime schema mirror
ensureDbCompatibility ran ALTER TABLE / CREATE TABLE IF NOT EXISTS on the
login, register, token and session paths, mirroring columns every
migration under prisma/migrations already creates. The entrypoint applies
those migrations before the server listens and stops the boot when one
fails, so the mirror never had work to do, and DDL on a request path
needs the table's owner: a connection that may only read and write rows
fails every login on it. Drop the module, its call sites and the mocks
that stubbed it out.
2026-11-03 20:32:35 +01:00

174 lines
5.9 KiB
TypeScript

/**
* The rollup coverage probe reads the distinct live types as a loose index
* scan. The previous shape (a DISTINCT over every live row joined to a COUNT
* over every DAY bucket) is kept here as the oracle for which types appear
* and for the edges that decide the read-swap: tombstoned types, types with
* only coarser buckets, another account's buckets, and an account with
* nothing. Coverage itself is stricter than the oracle's "any DAY bucket":
* the buckets must reach back to the type's oldest live reading, which the
* second case pins.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
import { probeRollupCoverage } from "@/lib/rollups/measurement-coverage";
import type { MeasurementType } from "@/generated/prisma/client";
vi.mock("@/lib/jobs/boss-instance", () => ({
getGlobalBoss: vi.fn(() => null),
}));
const USER = "user-coverage-probe";
const OTHER = "user-coverage-probe-other";
const EMPTY = "user-coverage-probe-empty";
async function oracle(userId: string): Promise<Map<string, boolean>> {
const rows = await getPrismaClient().$queryRaw<
Array<{ type: string; has_buckets: boolean }>
>`
SELECT m."type"::text AS type, COUNT(r.*) > 0 AS has_buckets
FROM (
SELECT DISTINCT "type" FROM measurements
WHERE user_id = ${userId} AND "deleted_at" IS NULL
) m
LEFT JOIN measurement_rollups r
ON r.user_id = ${userId} AND r."type" = m."type" AND r.granularity = 'DAY'
GROUP BY m."type"
`;
return new Map(rows.map((r) => [r.type, Boolean(r.has_buckets)]));
}
beforeEach(async () => {
const prisma = getPrismaClient();
await truncateAllTables(prisma);
for (const id of [USER, OTHER, EMPTY]) {
await prisma.user.create({
data: { id, username: id, email: `${id}@example.test`, timezone: "UTC" },
});
}
});
describe("probeRollupCoverage", () => {
it("matches the full-scan oracle across tombstones, granularities and accounts", async () => {
const prisma = getPrismaClient();
const day = 86_400_000;
const base = Date.UTC(2026, 0, 10);
const reading = (
userId: string,
type: MeasurementType,
i: number,
deleted = false,
) => ({
userId,
type,
value: 60 + i,
unit: "x",
measuredAt: new Date(base + i * day),
deletedAt: deleted ? new Date(base) : null,
});
await prisma.measurement.createMany({
data: [
// Live rows with DAY buckets → covered.
...[0, 1, 2].map((i) => reading(USER, "PULSE", i)),
// Live rows, no buckets at all → uncovered.
...[0, 1].map((i) => reading(USER, "WEIGHT", i)),
// Live rows with only a WEEK bucket → uncovered.
reading(USER, "BLOOD_PRESSURE_SYS", 0),
// Only tombstones → absent from the map, even with a DAY bucket.
...[0, 1].map((i) => reading(USER, "SLEEP_DURATION", i, true)),
// A mix of tombstoned and live rows → present.
reading(USER, "ACTIVITY_STEPS", 0, true),
reading(USER, "ACTIVITY_STEPS", 1),
// Another account's rows and buckets never leak in.
reading(OTHER, "WEIGHT", 0),
reading(OTHER, "OXYGEN_SATURATION", 0),
],
});
const bucket = (
userId: string,
type: MeasurementType,
granularity: "DAY" | "WEEK",
) => ({
userId,
type,
granularity,
bucketStart: new Date(base),
source: "MANUAL" as const,
count: 1,
mean: 1,
minValue: 1,
maxValue: 1,
});
await prisma.measurementRollup.createMany({
data: [
bucket(USER, "PULSE", "DAY"),
bucket(USER, "BLOOD_PRESSURE_SYS", "WEEK"),
bucket(USER, "SLEEP_DURATION", "DAY"),
bucket(USER, "ACTIVITY_STEPS", "DAY"),
bucket(OTHER, "WEIGHT", "DAY"),
bucket(USER, "OXYGEN_SATURATION", "DAY"),
],
});
const coverage = await probeRollupCoverage(USER);
expect(Object.fromEntries(coverage)).toEqual({
ACTIVITY_STEPS: true,
BLOOD_PRESSURE_SYS: false,
PULSE: true,
WEIGHT: false,
});
expect(coverage).toEqual(await oracle(USER));
expect(await probeRollupCoverage(OTHER)).toEqual(await oracle(OTHER));
const empty = await probeRollupCoverage(EMPTY);
expect(empty.size).toBe(0);
expect(empty).toEqual(await oracle(EMPTY));
});
it("does not count a type as covered by buckets for its newest days only", async () => {
const prisma = getPrismaClient();
const day = 86_400_000;
const base = Date.UTC(2026, 0, 10);
// Three weight readings, but only the newest day folded: what a write
// hook leaves when it runs before the history was folded.
await prisma.measurement.createMany({
data: [0, 1, 2].map((i) => ({
userId: USER,
type: "WEIGHT" as const,
value: 80 + i,
unit: "kg",
measuredAt: new Date(base + i * day + 9 * 3_600_000),
})),
});
const bucket = (i: number) => ({
userId: USER,
type: "WEIGHT" as const,
granularity: "DAY" as const,
bucketStart: new Date(base + i * day),
source: "MANUAL" as const,
count: 1,
mean: 80 + i,
minValue: 80 + i,
maxValue: 80 + i,
});
await prisma.measurementRollup.create({ data: bucket(2) });
expect((await probeRollupCoverage(USER)).get("WEIGHT")).toBe(false);
// Once the fold reaches the oldest reading's day, the type is covered.
await prisma.measurementRollup.createMany({ data: [bucket(0), bucket(1)] });
expect((await probeRollupCoverage(USER)).get("WEIGHT")).toBe(true);
// A reading older than the oldest bucket uncovers it again.
await prisma.measurement.create({
data: {
userId: USER,
type: "WEIGHT",
value: 79,
unit: "kg",
measuredAt: new Date(base - day),
},
});
expect((await probeRollupCoverage(USER)).get("WEIGHT")).toBe(false);
});
});