Files
HealthLog/tests/integration/pr-detection-end-to-end.test.ts
T
Marc-André Bombeck 17b8d8d2c2 chore(release): v1.4.25
Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites).

See CHANGELOG.md for the full per-section delta.
2026-05-14 23:49:32 +02:00

189 lines
6.4 KiB
TypeScript

/**
* v1.4.25 W16c — PR detection end-to-end.
*
* The handler runs against a real Postgres testcontainer + real
* Measurement / Workout rows. We assert two flows:
*
* 1. After enough measurements land, the detector writes a
* PersonalRecord row at the all-time best and tags the
* `sourceMeasurementId` to the winning row.
* 2. The detector is idempotent — re-running the same handler over
* the same dataset produces no duplicate rows because the
* `(userId, metricType, metricSlot, achievedAt)` unique index
* absorbs the second insert.
*
* The HTTP-layer enqueue is covered by the per-route unit suites
* (`src/app/api/measurements/batch/__tests__/pr-detection-hook.test.ts`
* and the workouts equivalent). This file walks the handler instead
* of going through pg-boss so the integration suite stays free of
* out-of-process job-queue plumbing.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
const TEST_USER_ID = "user-pr-detection-e2e";
const { detectPersonalRecordsForUser, PR_DETECTION_WARMUP_THRESHOLD } =
await import("@/lib/personal-records/pr-detection-worker");
const { isPRTrackable } = await import("@/lib/personal-records/pr-direction");
import type { MeasurementType } from "@/generated/prisma/client";
const { measurementTypeEnum } = await import("@/lib/validations/measurement");
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
await getPrismaClient().user.create({
data: {
id: TEST_USER_ID,
username: "pr-detection",
email: "pr-detection@example.test",
},
});
});
describe("PR detection — measurement-driven (real DB)", () => {
it("writes one PersonalRecord row for the all-time best after the warm-up gate", async () => {
const prisma = getPrismaClient();
// Seed enough samples to clear the warm-up gate plus one obvious
// all-time best. Days are stepped backwards from today so the
// best row's `measuredAt` lands at a deterministic, unique
// timestamp.
const baseDate = new Date("2026-05-14T00:00:00Z");
const samples = [];
for (let i = 0; i < PR_DETECTION_WARMUP_THRESHOLD; i++) {
samples.push({
userId: TEST_USER_ID,
type: "ACTIVITY_STEPS" as const,
value: 8000 + i * 100,
unit: "count",
source: "APPLE_HEALTH" as const,
measuredAt: new Date(baseDate.getTime() - (i + 1) * 86_400_000),
externalId: `seed-${i}`,
});
}
samples.push({
userId: TEST_USER_ID,
type: "ACTIVITY_STEPS" as const,
value: 18234,
unit: "count",
source: "APPLE_HEALTH" as const,
measuredAt: baseDate,
externalId: "all-time-best",
});
await prisma.measurement.createMany({ data: samples });
const result = await detectPersonalRecordsForUser(TEST_USER_ID);
expect(result.inserted).toBeGreaterThanOrEqual(1);
const stepsPR = await prisma.personalRecord.findFirst({
where: {
userId: TEST_USER_ID,
metricType: "ACTIVITY_STEPS",
metricSlot: null,
},
});
expect(stepsPR).not.toBeNull();
expect(stepsPR?.value).toBe(18234);
expect(stepsPR?.direction).toBe("MAX");
expect(stepsPR?.sourceMeasurementId).toBeDefined();
});
it("is idempotent — re-running the handler does not duplicate the row", async () => {
const prisma = getPrismaClient();
const baseDate = new Date("2026-05-14T00:00:00Z");
const samples = [];
for (let i = 0; i < PR_DETECTION_WARMUP_THRESHOLD; i++) {
samples.push({
userId: TEST_USER_ID,
type: "RESTING_HEART_RATE" as const,
value: 65 + i,
unit: "bpm",
source: "APPLE_HEALTH" as const,
measuredAt: new Date(baseDate.getTime() - (i + 1) * 86_400_000),
externalId: `rhr-seed-${i}`,
});
}
samples.push({
userId: TEST_USER_ID,
type: "RESTING_HEART_RATE" as const,
value: 47,
unit: "bpm",
source: "APPLE_HEALTH" as const,
measuredAt: baseDate,
externalId: "rhr-best",
});
await prisma.measurement.createMany({ data: samples });
await detectPersonalRecordsForUser(TEST_USER_ID);
const after1 = await prisma.personalRecord.count({
where: { userId: TEST_USER_ID, metricType: "RESTING_HEART_RATE" },
});
await detectPersonalRecordsForUser(TEST_USER_ID);
const after2 = await prisma.personalRecord.count({
where: { userId: TEST_USER_ID, metricType: "RESTING_HEART_RATE" },
});
expect(after1).toBe(1);
expect(after2).toBe(1);
});
it("propagates the silent flag without affecting the row written", async () => {
const prisma = getPrismaClient();
const baseDate = new Date("2026-05-14T00:00:00Z");
const samples = [];
for (let i = 0; i < PR_DETECTION_WARMUP_THRESHOLD; i++) {
samples.push({
userId: TEST_USER_ID,
type: "VO2_MAX" as const,
value: 38 + i * 0.5,
unit: "ml/kg/min",
source: "APPLE_HEALTH" as const,
measuredAt: new Date(baseDate.getTime() - (i + 1) * 86_400_000),
externalId: `vo2-${i}`,
});
}
samples.push({
userId: TEST_USER_ID,
type: "VO2_MAX" as const,
value: 52.5,
unit: "ml/kg/min",
source: "APPLE_HEALTH" as const,
measuredAt: baseDate,
externalId: "vo2-best",
});
await prisma.measurement.createMany({ data: samples });
const result = await detectPersonalRecordsForUser(TEST_USER_ID, {
silent: true,
});
expect(result.silent).toBe(true);
const pr = await prisma.personalRecord.findFirst({
where: { userId: TEST_USER_ID, metricType: "VO2_MAX" },
});
expect(pr).not.toBeNull();
expect(pr?.value).toBeCloseTo(52.5);
});
});
describe("PR detection — drift guard (real DB)", () => {
it("scans every PR-trackable metric type when no row exists", async () => {
// Drift assertion — `scanned` should match the count of PR-trackable
// metrics (and at least every direction enum case must appear in
// either the worker or the suppress branch via `getPRDirection`).
const result = await detectPersonalRecordsForUser(TEST_USER_ID);
const trackable = measurementTypeEnum.options.filter((t) =>
isPRTrackable(t as MeasurementType),
);
expect(result.scanned).toBeGreaterThanOrEqual(trackable.length);
});
});