mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
393 lines
13 KiB
TypeScript
393 lines
13 KiB
TypeScript
/**
|
|
* v1.10.2 — real-Postgres integration coverage for
|
|
* `runDenseIntradayRetention()`. The mocked unit tests could not surface
|
|
* the v1.10.0.2 P2002 fold-coexistence bug: the fold mints a canonical
|
|
* daily-mean row at the user's local-noon instant, but a row may ALREADY
|
|
* sit on that `(userId, type, measuredAt, source, sleepStage)` unique
|
|
* index from another path. An externalId-keyed upsert misses the lookup
|
|
* and the INSERT then violates the measured_at index with P2002.
|
|
*
|
|
* This file seeds that exact colliding shape against a live Postgres and
|
|
* asserts the reworked fold coexists with the pre-existing daily row,
|
|
* stays idempotent, and preserves the dense-tier scope.
|
|
*/
|
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
import { runDenseIntradayRetention } from "@/lib/measurements/dense-intraday-retention";
|
|
import { canonicalDailyTimestamp } from "@/lib/measurements/consolidation-tz";
|
|
|
|
const TEST_USER_ID = "user-dense-retention";
|
|
const TZ = "Europe/Berlin";
|
|
const DAY_KEY = "2026-05-01";
|
|
// The canonical local-noon instant the fold writes for the seeded day.
|
|
const CANONICAL = canonicalDailyTimestamp(DAY_KEY, TZ);
|
|
const HRV_STATS_ID =
|
|
"stats:HKQuantityTypeIdentifierHeartRateVariabilitySDNN:2026-05-01";
|
|
|
|
beforeEach(async () => {
|
|
await truncateAllTables(getPrismaClient());
|
|
await getPrismaClient().user.create({
|
|
data: {
|
|
id: TEST_USER_ID,
|
|
username: "dense-retention",
|
|
email: "dense-retention@example.test",
|
|
timezone: TZ,
|
|
},
|
|
});
|
|
});
|
|
|
|
describe("runDenseIntradayRetention (real Postgres)", () => {
|
|
it("folds out-of-window HRV per-sample rows into one daily MEAN and soft-deletes them", async () => {
|
|
const prisma = getPrismaClient();
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 40,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-01T08:00:00.000Z"),
|
|
externalId: "hk-hrv-1",
|
|
},
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 60,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-01T09:00:00.000Z"),
|
|
externalId: "hk-hrv-2",
|
|
},
|
|
],
|
|
});
|
|
|
|
const summary = await runDenseIntradayRetention(prisma, {
|
|
userId: TEST_USER_ID,
|
|
retentionDays: 0,
|
|
log: () => {},
|
|
});
|
|
|
|
expect(summary.totals.daysConsolidated).toBe(1);
|
|
expect(summary.totals.perSampleRowsSoftDeleted).toBe(2);
|
|
|
|
const live = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
deletedAt: null,
|
|
},
|
|
});
|
|
expect(live).toHaveLength(1);
|
|
// MEAN of (40, 60) = 50.
|
|
expect(live[0].value).toBeCloseTo(50, 6);
|
|
expect(live[0].externalId).toBe(HRV_STATS_ID);
|
|
expect(live[0].measuredAt.getTime()).toBe(CANONICAL.getTime());
|
|
});
|
|
|
|
it("coexists with a pre-existing daily row on the canonical instant (the P2002 case)", async () => {
|
|
const prisma = getPrismaClient();
|
|
// A previously-collapsed daily `stats:` row ALREADY sits on the
|
|
// canonical local-noon instant under a DIFFERENT HK stats id than the
|
|
// one the fold mints (e.g. a row minted by a sibling consolidation path,
|
|
// or a legacy stats id). The scan EXCLUDES it (NOT startsWith 'stats:'),
|
|
// so it is never folded into the mean — but pre-v1.10.2 the fold's
|
|
// externalId-keyed upsert missed it on lookup and the INSERT then
|
|
// collided on (userId, type, measuredAt, source, sleepStage) with P2002.
|
|
await prisma.measurement.create({
|
|
data: {
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 999, // stale value to be overwritten by the fold
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: CANONICAL,
|
|
externalId: "stats:legacy-hrv-id:2026-05-01",
|
|
},
|
|
});
|
|
// Out-of-window per-sample rows that should fold into the canonical row.
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 40,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-01T06:00:00.000Z"),
|
|
externalId: "hk-hrv-1",
|
|
},
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 60,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-01T18:00:00.000Z"),
|
|
externalId: "hk-hrv-2",
|
|
},
|
|
],
|
|
});
|
|
|
|
// Must not throw P2002.
|
|
const summary = await runDenseIntradayRetention(prisma, {
|
|
userId: TEST_USER_ID,
|
|
retentionDays: 0,
|
|
log: () => {},
|
|
});
|
|
expect(summary.totals.daysConsolidated).toBe(1);
|
|
|
|
const live = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
deletedAt: null,
|
|
},
|
|
});
|
|
// Exactly one live daily row: the pre-existing one, adopted in place.
|
|
expect(live).toHaveLength(1);
|
|
expect(live[0].measuredAt.getTime()).toBe(CANONICAL.getTime());
|
|
// The fold refreshed the value to the per-sample MEAN (50), overwriting
|
|
// the stale 999, and stamped the canonical `stats:` externalId.
|
|
expect(live[0].value).toBeCloseTo(50, 6);
|
|
expect(live[0].externalId).toBe(HRV_STATS_ID);
|
|
|
|
// The two out-of-window samples are tombstoned.
|
|
const tombstoned = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
deletedAt: { not: null },
|
|
},
|
|
});
|
|
expect(tombstoned).toHaveLength(2);
|
|
});
|
|
|
|
it("adopts the row already carrying the target stats externalId on the canonical instant (VECTOR 1)", async () => {
|
|
const prisma = getPrismaClient();
|
|
// BOTH-present collision (VECTOR 1). A prior fold already minted the
|
|
// canonical daily row carrying the TARGET `stats:` externalId and it
|
|
// sits on the canonical local-noon instant. A fresh batch of
|
|
// out-of-window per-sample rows then arrives for the same day. The fold
|
|
// must adopt the EXISTING canonical row in place — refreshing its value
|
|
// and re-anchoring it — never INSERT a second row at local-noon (that
|
|
// would collide on the measured_at composite) and never stamp the target
|
|
// externalId onto a sibling (that would collide on the externalId
|
|
// composite with this very row). The pre-fix `findFirst` had no
|
|
// externalId discrimination and no deterministic `orderBy`, so it could
|
|
// pick the wrong row and trip P2002 on the externalId index.
|
|
await prisma.measurement.create({
|
|
data: {
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 999, // stale — the fold overwrites it with the day's mean
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: CANONICAL,
|
|
externalId: HRV_STATS_ID, // the TARGET canonical externalId
|
|
},
|
|
});
|
|
// Out-of-window per-sample rows that fold into the canonical row.
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 40,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-01T06:00:00.000Z"),
|
|
externalId: "hk-hrv-1",
|
|
},
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 60,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-01T18:00:00.000Z"),
|
|
externalId: "hk-hrv-2",
|
|
},
|
|
],
|
|
});
|
|
|
|
// Must not throw P2002.
|
|
const summary = await runDenseIntradayRetention(prisma, {
|
|
userId: TEST_USER_ID,
|
|
retentionDays: 0,
|
|
log: () => {},
|
|
});
|
|
expect(summary.totals.daysConsolidated).toBe(1);
|
|
|
|
const live = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
deletedAt: null,
|
|
},
|
|
});
|
|
// Exactly ONE live canonical row remains — the row that already carried
|
|
// the target `stats:` externalId, adopted in place and folded.
|
|
expect(live).toHaveLength(1);
|
|
expect(live[0].externalId).toBe(HRV_STATS_ID);
|
|
expect(live[0].measuredAt.getTime()).toBe(CANONICAL.getTime());
|
|
// MEAN of the two folded per-sample values (40, 60) = 50, overwriting
|
|
// the stale 999.
|
|
expect(live[0].value).toBeCloseTo(50, 6);
|
|
|
|
// Both scanned per-sample rows are tombstoned.
|
|
const tombstoned = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
deletedAt: { not: null },
|
|
},
|
|
});
|
|
expect(tombstoned).toHaveLength(2);
|
|
});
|
|
|
|
it("does not tombstone a per-sample row that happens to fall on the canonical instant", async () => {
|
|
const prisma = getPrismaClient();
|
|
// One per-sample row sits exactly on the canonical local-noon instant;
|
|
// it must become the adopted daily row, not get soft-deleted.
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 50,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: CANONICAL,
|
|
externalId: "hk-hrv-noon",
|
|
},
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 70,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-01T06:00:00.000Z"),
|
|
externalId: "hk-hrv-morning",
|
|
},
|
|
],
|
|
});
|
|
|
|
const summary = await runDenseIntradayRetention(prisma, {
|
|
userId: TEST_USER_ID,
|
|
retentionDays: 0,
|
|
log: () => {},
|
|
});
|
|
expect(summary.totals.daysConsolidated).toBe(1);
|
|
|
|
const live = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
deletedAt: null,
|
|
},
|
|
});
|
|
expect(live).toHaveLength(1);
|
|
expect(live[0].measuredAt.getTime()).toBe(CANONICAL.getTime());
|
|
// MEAN of (50, 70) = 60 — the adopted row carries the day's mean.
|
|
expect(live[0].value).toBeCloseTo(60, 6);
|
|
expect(live[0].externalId).toBe(HRV_STATS_ID);
|
|
});
|
|
|
|
it("is idempotent — a second run is a no-op and the value stays correct", async () => {
|
|
const prisma = getPrismaClient();
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "PULSE",
|
|
value: 60,
|
|
unit: "bpm",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-01T06:00:00.000Z"),
|
|
externalId: "hk-pulse-1",
|
|
},
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "PULSE",
|
|
value: 80,
|
|
unit: "bpm",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-01T18:00:00.000Z"),
|
|
externalId: "hk-pulse-2",
|
|
},
|
|
],
|
|
});
|
|
|
|
const first = await runDenseIntradayRetention(prisma, {
|
|
userId: TEST_USER_ID,
|
|
retentionDays: 0,
|
|
log: () => {},
|
|
});
|
|
expect(first.totals.daysConsolidated).toBe(1);
|
|
expect(first.totals.perSampleRowsSoftDeleted).toBe(2);
|
|
|
|
// Second run must converge to zero work and never collide on the
|
|
// canonical row it minted on the first pass.
|
|
const second = await runDenseIntradayRetention(prisma, {
|
|
userId: TEST_USER_ID,
|
|
retentionDays: 0,
|
|
log: () => {},
|
|
});
|
|
expect(second.totals.daysConsolidated).toBe(0);
|
|
expect(second.totals.perSampleRowsSoftDeleted).toBe(0);
|
|
|
|
const live = await prisma.measurement.findMany({
|
|
where: { userId: TEST_USER_ID, type: "PULSE", deletedAt: null },
|
|
});
|
|
expect(live).toHaveLength(1);
|
|
// MEAN of (60, 80) = 70.
|
|
expect(live[0].value).toBeCloseTo(70, 6);
|
|
});
|
|
|
|
it("keeps in-window per-sample rows raw (retention bound)", async () => {
|
|
const prisma = getPrismaClient();
|
|
const recent = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000); // 2 days ago
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 45,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: recent,
|
|
externalId: "hk-hrv-recent-1",
|
|
},
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
value: 55,
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date(recent.getTime() + 60 * 60 * 1000),
|
|
externalId: "hk-hrv-recent-2",
|
|
},
|
|
],
|
|
});
|
|
|
|
const summary = await runDenseIntradayRetention(prisma, {
|
|
userId: TEST_USER_ID,
|
|
// Default 14-day window — both rows are inside it.
|
|
log: () => {},
|
|
});
|
|
expect(summary.totals.daysConsolidated).toBe(0);
|
|
|
|
const live = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: TEST_USER_ID,
|
|
type: "HEART_RATE_VARIABILITY",
|
|
deletedAt: null,
|
|
},
|
|
});
|
|
// Both raw samples survive — the intra-day shape is preserved.
|
|
expect(live).toHaveLength(2);
|
|
});
|
|
});
|