mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
The coverage probe called a type covered as soon as one DAY bucket existed. A write hook folds the day it writes, so a type whose history was not yet folded (an account before its backfill, or a read racing the detached refresh) had one fresh bucket, took the bucket-composed path, and its summary counted that one day: three weight readings read as one. A type is now covered when its oldest DAY bucket starts no later than its oldest live reading inside the fold window. The bucket holding a reading starts at or before it in any time zone, so the comparison holds without knowing the account's day boundaries. Both sides are one index descent per type; anything not covered falls back to the live aggregate as before.
177 lines
6.0 KiB
TypeScript
177 lines
6.0 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/db-compat", () => ({
|
|
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
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);
|
|
});
|
|
});
|