Files
HealthLog/tests/integration/drain-per-sample-cumulative.test.ts
T
Marc-André Bombeck 3ece95f192 chore(release): v1.4.30
Server-side prep for the next iOS TestFlight build. Five surfaces land together so the iOS engineer can pick them up in one cut-over: a locked externalId shape for daily-aggregated cumulative HealthKit rows, the SyncMode foundation columns + handshake + bulk-backfill endpoints, a first-class MoodEntry.note column that replaces the legacy tag-based workaround, a cross-source workout dedup helper, and two new MeasurementType enums plus the shared category overlay that drives the iOS permission picker and the future Insights nav.
2026-05-16 12:37:55 +02:00

250 lines
7.3 KiB
TypeScript

/**
* v1.4.30 — real-Postgres integration coverage for
* `drainPerSampleCumulative()`. Mirrors the
* `measurements-aggregate-daily` fixture: the testcontainer is hot
* before this file loads, so the body just truncates + seeds + drains.
*
* The drain logic operates against `prisma.$transaction` with an upsert
* inside; mocking that surface would re-introduce the v1.4.29 lesson
* (mocks hide real-SQL behaviour). The container test pins the
* idempotent-re-run contract end-to-end.
*/
import { beforeEach, describe, expect, it } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
import { drainPerSampleCumulative } from "@/lib/measurements/drain-per-sample-cumulative";
const TEST_USER_ID = "user-drain-cumulative";
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
await getPrismaClient().user.create({
data: {
id: TEST_USER_ID,
username: "drain-cumulative",
email: "drain-cumulative@example.test",
timezone: "Europe/Berlin",
},
});
});
describe("drainPerSampleCumulative (real Postgres)", () => {
it("collapses three per-sample step rows on the same day into one daily row", async () => {
const prisma = getPrismaClient();
await prisma.measurement.createMany({
data: [
{
userId: TEST_USER_ID,
type: "ACTIVITY_STEPS",
value: 1200,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T08:00:00.000Z"),
externalId: "hk-uuid-1",
},
{
userId: TEST_USER_ID,
type: "ACTIVITY_STEPS",
value: 3400,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T14:00:00.000Z"),
externalId: "hk-uuid-2",
},
{
userId: TEST_USER_ID,
type: "ACTIVITY_STEPS",
value: 800,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T20:00:00.000Z"),
externalId: "hk-uuid-3",
},
],
});
const summary = await drainPerSampleCumulative(prisma, {
userId: TEST_USER_ID,
dryRun: false,
log: () => {},
});
expect(summary.dryRun).toBe(false);
expect(summary.totals.bucketsCollapsed).toBe(1);
expect(summary.totals.perSampleRowsDeleted).toBe(3);
expect(summary.totals.dailyRowsUpserted).toBe(1);
const remaining = await prisma.measurement.findMany({
where: {
userId: TEST_USER_ID,
type: "ACTIVITY_STEPS",
source: "APPLE_HEALTH",
},
orderBy: { measuredAt: "asc" },
});
expect(remaining).toHaveLength(1);
expect(remaining[0].value).toBe(5400);
expect(remaining[0].externalId).toBe(
"stats:HKQuantityTypeIdentifierStepCount:2026-05-16",
);
// 12:00 Berlin during DST = 10:00 UTC.
expect(remaining[0].measuredAt.toISOString()).toBe(
"2026-05-16T10:00:00.000Z",
);
});
it("is idempotent — a second run after a successful drain is a no-op", async () => {
const prisma = getPrismaClient();
await prisma.measurement.createMany({
data: [
{
userId: TEST_USER_ID,
type: "ACTIVITY_STEPS",
value: 1200,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T08:00:00.000Z"),
externalId: "hk-uuid-1",
},
{
userId: TEST_USER_ID,
type: "ACTIVITY_STEPS",
value: 800,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T14:00:00.000Z"),
externalId: "hk-uuid-2",
},
],
});
await drainPerSampleCumulative(prisma, {
userId: TEST_USER_ID,
dryRun: false,
log: () => {},
});
const second = await drainPerSampleCumulative(prisma, {
userId: TEST_USER_ID,
dryRun: false,
log: () => {},
});
expect(second.totals.bucketsCollapsed).toBe(0);
expect(second.totals.perSampleRowsDeleted).toBe(0);
expect(second.totals.dailyRowsUpserted).toBe(0);
});
it("dry-run reports the plan without touching the DB", async () => {
const prisma = getPrismaClient();
await prisma.measurement.createMany({
data: [
{
userId: TEST_USER_ID,
type: "FLIGHTS_CLIMBED",
value: 5,
unit: "flights",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T08:00:00.000Z"),
externalId: "hk-flights-1",
},
{
userId: TEST_USER_ID,
type: "FLIGHTS_CLIMBED",
value: 3,
unit: "flights",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T14:00:00.000Z"),
externalId: "hk-flights-2",
},
],
});
const summary = await drainPerSampleCumulative(prisma, {
userId: TEST_USER_ID,
dryRun: true,
log: () => {},
});
expect(summary.dryRun).toBe(true);
expect(summary.totals.bucketsCollapsed).toBe(1);
expect(summary.buckets[0].sumValue).toBe(8);
expect(summary.buckets[0].externalId).toBe(
"stats:HKQuantityTypeIdentifierFlightsClimbed:2026-05-16",
);
// DB unchanged.
const remaining = await prisma.measurement.findMany({
where: { userId: TEST_USER_ID, type: "FLIGHTS_CLIMBED" },
});
expect(remaining).toHaveLength(2);
});
it("ignores spot metrics (PULSE) — only cumulative types collapse", async () => {
const prisma = getPrismaClient();
await prisma.measurement.createMany({
data: [
{
userId: TEST_USER_ID,
type: "PULSE",
value: 64,
unit: "bpm",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T08:00:00.000Z"),
externalId: "hk-pulse-1",
},
{
userId: TEST_USER_ID,
type: "PULSE",
value: 72,
unit: "bpm",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T14:00:00.000Z"),
externalId: "hk-pulse-2",
},
],
});
const summary = await drainPerSampleCumulative(prisma, {
userId: TEST_USER_ID,
dryRun: false,
log: () => {},
});
expect(summary.totals.bucketsCollapsed).toBe(0);
const remaining = await prisma.measurement.findMany({
where: { userId: TEST_USER_ID, type: "PULSE" },
});
expect(remaining).toHaveLength(2);
});
it("skips rows already in stats:... shape (re-running after an earlier drain)", async () => {
const prisma = getPrismaClient();
await prisma.measurement.create({
data: {
userId: TEST_USER_ID,
type: "ACTIVITY_STEPS",
value: 5400,
unit: "steps",
source: "APPLE_HEALTH",
measuredAt: new Date("2026-05-16T10:00:00.000Z"),
externalId: "stats:HKQuantityTypeIdentifierStepCount:2026-05-16",
},
});
const summary = await drainPerSampleCumulative(prisma, {
userId: TEST_USER_ID,
dryRun: false,
log: () => {},
});
expect(summary.totals.bucketsCollapsed).toBe(0);
const remaining = await prisma.measurement.findMany({
where: { userId: TEST_USER_ID, type: "ACTIVITY_STEPS" },
});
expect(remaining).toHaveLength(1);
expect(remaining[0].value).toBe(5400);
});
});