mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
341 lines
9.9 KiB
TypeScript
341 lines
9.9 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",
|
|
);
|
|
expect(remaining[0]).toMatchObject({
|
|
aggregationProvenance: "LEGACY_UNKNOWN",
|
|
syncVersion: 1,
|
|
});
|
|
});
|
|
|
|
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.each(["HEALTHKIT_STATISTICS", "EXPORT_XML_SOURCE_MAX"] as const)(
|
|
"drains source rows without mutating an authoritative %s total",
|
|
async (aggregationProvenance) => {
|
|
const prisma = getPrismaClient();
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 5_400,
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-16T10:00:00.000Z"),
|
|
externalId: "stats:HKQuantityTypeIdentifierStepCount:2026-05-16",
|
|
aggregationProvenance,
|
|
},
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 500,
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-16T20:00:00.000Z"),
|
|
externalId: `late-${aggregationProvenance}`,
|
|
},
|
|
],
|
|
});
|
|
|
|
await drainPerSampleCumulative(prisma, {
|
|
userId: TEST_USER_ID,
|
|
log: () => {},
|
|
});
|
|
|
|
const remaining = await prisma.measurement.findMany({
|
|
where: { userId: TEST_USER_ID, type: "ACTIVITY_STEPS" },
|
|
});
|
|
expect(remaining).toHaveLength(1);
|
|
expect(remaining[0]).toMatchObject({
|
|
value: 5_400,
|
|
aggregationProvenance,
|
|
syncVersion: 1,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("keeps legacy late-increment semantics and bumps syncVersion", async () => {
|
|
const prisma = getPrismaClient();
|
|
await prisma.measurement.createMany({
|
|
data: [
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 5_400,
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-16T10:00:00.000Z"),
|
|
externalId: "stats:HKQuantityTypeIdentifierStepCount:2026-05-16",
|
|
aggregationProvenance: "LEGACY_UNKNOWN",
|
|
},
|
|
{
|
|
userId: TEST_USER_ID,
|
|
type: "ACTIVITY_STEPS",
|
|
value: 500,
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
measuredAt: new Date("2026-05-16T20:00:00.000Z"),
|
|
externalId: "late-legacy",
|
|
},
|
|
],
|
|
});
|
|
|
|
await drainPerSampleCumulative(prisma, {
|
|
userId: TEST_USER_ID,
|
|
log: () => {},
|
|
});
|
|
|
|
const remaining = await prisma.measurement.findMany({
|
|
where: { userId: TEST_USER_ID, type: "ACTIVITY_STEPS" },
|
|
});
|
|
expect(remaining).toHaveLength(1);
|
|
expect(remaining[0]).toMatchObject({
|
|
value: 5_900,
|
|
aggregationProvenance: "LEGACY_UNKNOWN",
|
|
syncVersion: 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);
|
|
});
|
|
});
|