Files
HealthLog/tests/integration/doctor-report-dense-data.test.ts
T
Marc-André Bombeck 7ac81f3eed feat(vaccinations): publish the routes, classify them, gate the module
- four route files: list and create, read and edit and delete, restore,
  and the link pair; every delegable one is classified as health
  background, in each leg of the sharing guard, with its reason written
- link and unlink stay bare, consistent with the document and visit link
  routes; the three are revisited as one decision
- vaccinations becomes the nineteenth module key, default-on and
  surface-gated like medications: the data routes stay exempt so a
  restore keeps working and re-enabling finds every dose intact
- a delegate's dose entries get their own sentence in the owner's
  activity view rather than reading as a generic change
2026-08-08 14:27:36 +02:00

176 lines
5.0 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Prisma } from "@/generated/prisma/client";
import { collectDoctorReportData } from "@/lib/doctor-report-data";
import { selectionFromLeaves } from "@/lib/report-selection/selection";
import { getPrismaClient, truncateAllTables } from "./setup";
const RANGE = {
start: new Date("2026-01-01T00:00:00.000Z"),
end: new Date("2026-05-01T00:00:00.000Z"),
days: 120,
};
const MODULES = {
cycle: false,
mood: false,
sleep: false,
glucose: true,
workouts: false,
recovery: false,
labs: false,
illness: false,
achievements: true,
coach: true,
insights: true,
medications: true,
doctorReport: true,
environment: true,
mcp: true,
inboundDocuments: true,
mentalHealth: true,
nutrients: true,
vaccinations: true,
} as const;
/** The dense path is about pulse and glucose; nothing else is in scope here. */
const SELECTION = selectionFromLeaves([
"WEIGHT",
"PULSE",
"BLOOD_GLUCOSE",
"GLUCOSE_PANEL",
]);
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("doctor report — bounded dense measurement reads", () => {
it("keeps dense transfer and points bounded by local days while sparse rows remain raw", async () => {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: "doctor-report-dense",
email: "doctor-report-dense@example.test",
timezone: "America/New_York",
sourcePriorityJson: {
metricPriority: {
pulse: ["WITHINGS", "MANUAL"],
},
} as Prisma.InputJsonValue,
},
});
const sparseRows = [
{
userId: user.id,
type: "WEIGHT" as const,
value: 80,
unit: "kg",
source: "MANUAL" as const,
measuredAt: new Date("2026-01-02T04:15:00.000Z"),
},
{
userId: user.id,
type: "WEIGHT" as const,
value: 81,
unit: "kg",
source: "MANUAL" as const,
measuredAt: new Date("2026-01-02T06:15:00.000Z"),
},
];
await prisma.measurement.createMany({ data: sparseRows });
const seedDenseBatch = async (
offset: number,
samplesPerLocalDay: number,
) => {
const rows: Prisma.MeasurementCreateManyInput[] = [];
const localDayInstants = [
Date.parse("2026-01-02T04:30:00.000Z"), // Jan 1, 23:30 in New York
Date.parse("2026-01-02T05:30:00.000Z"), // Jan 2, 00:30 in New York
];
for (const [dayIndex, baseMs] of localDayInstants.entries()) {
for (let i = 0; i < samplesPerLocalDay; i += 1) {
const measuredAt = new Date(baseMs + (offset + i) * 1_000);
rows.push(
{
userId: user.id,
type: "PULSE",
value: 60 + dayIndex,
unit: "bpm",
source: "WITHINGS",
measuredAt,
},
{
userId: user.id,
type: "PULSE",
value: 160 + dayIndex,
unit: "bpm",
source: "MANUAL",
measuredAt,
},
{
userId: user.id,
type: "BLOOD_GLUCOSE",
value: 90 + dayIndex,
unit: "mg/dL",
source: "MANUAL",
glucoseContext: "FASTING",
measuredAt,
},
);
}
}
await prisma.measurement.createMany({ data: rows });
};
await seedDenseBatch(0, 4);
const findManySpy = vi.spyOn(prisma.measurement, "findMany");
const initial = await collectDoctorReportData(user.id, RANGE, SELECTION, {
moduleMap: { ...MODULES },
});
expect(initial.measurements.PULSE).toHaveLength(2);
expect(initial.measurements.BLOOD_GLUCOSE).toHaveLength(2);
expect(initial.measurements.PULSE.map((point) => point.measuredAt)).toEqual(
["2026-01-01T05:00:00.000Z", "2026-01-02T05:00:00.000Z"],
);
expect(initial.stats.PULSE).toMatchObject({
count: 8,
avg: 60.5,
min: 60,
max: 61,
});
expect(initial.measurements.WEIGHT).toEqual(
sparseRows.map((row) => ({
value: row.value,
measuredAt: row.measuredAt.toISOString(),
})),
);
const rawRows = (await findManySpy.mock.results[0]?.value) as Array<{
type: string;
}>;
expect(rawRows).toHaveLength(sparseRows.length);
expect(rawRows.map((row) => row.type)).toEqual(["WEIGHT", "WEIGHT"]);
await seedDenseBatch(10, 300);
const grown = await collectDoctorReportData(user.id, RANGE, SELECTION, {
moduleMap: { ...MODULES },
});
expect(grown.measurements.PULSE).toHaveLength(2);
expect(grown.measurements.BLOOD_GLUCOSE).toHaveLength(2);
expect(grown.stats.PULSE.count).toBe(608);
expect(grown.stats.BLOOD_GLUCOSE.count).toBe(608);
expect(grown.measurements.WEIGHT).toEqual(initial.measurements.WEIGHT);
});
});