mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
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.
174 lines
5.9 KiB
TypeScript
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);
|
|
});
|
|
});
|