diff --git a/src/app/api/admin/backups/[id]/restore/__tests__/round-trip.test.ts b/src/app/api/admin/backups/[id]/restore/__tests__/round-trip.test.ts index 228c20474..1305d3123 100644 --- a/src/app/api/admin/backups/[id]/restore/__tests__/round-trip.test.ts +++ b/src/app/api/admin/backups/[id]/restore/__tests__/round-trip.test.ts @@ -257,6 +257,29 @@ function sourceClient() { }, ]), }, + // The score as it was SHOWN on a local day. Not recomputable: today's + // number always comes from today's rows, so once the readings behind this + // day moved, nothing can reproduce what it said. + healthScoreRecord: { + findMany: vi.fn().mockResolvedValue([ + { + id: "score-day-1", + userId: OWNER, + dayKey: "2026-07-18", + timezone: "Europe/Berlin", + composite: 76, + band: "yellow", + scoreVersion: 2, + composition: ["BLOOD_PRESSURE", "SLEEP", "ADIPOSITY"], + pillarScores: { BLOOD_PRESSURE: 86, SLEEP: 100, ADIPOSITY: 42 }, + inputFingerprint: "a".repeat(64), + configVersion: null, + configChangedAt: null, + computedAt: new Date("2026-07-18T20:00:00.000Z"), + createdAt: new Date("2026-07-18T20:00:00.000Z"), + }, + ]), + }, customMetric: { findMany: vi.fn().mockResolvedValue([ { @@ -586,6 +609,44 @@ describe("backup round trip — export, wire schema, restore", () => { expect(rows[0].hourlyCumulative).toEqual(DAY_CURVE); }); + it("writes the recorded score day back with the number it showed", async () => { + const { written } = await roundTrip(); + + const write = written.find( + (w) => w.model === "healthScoreRecord" && w.op === "createMany", + ); + expect( + write, + "the recorded score day reached the file and nothing wrote it back", + ).toBeDefined(); + + const rows = write!.data as unknown as Array>; + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + id: "score-day-1", + userId: OWNER, + dayKey: "2026-07-18", + timezone: "Europe/Berlin", + composite: 76, + band: "yellow", + scoreVersion: 2, + inputFingerprint: "a".repeat(64), + }); + // The composition and the per-pillar scores are what keep the stored day + // readable across a later change to what counts. A row that came back + // with only its headline would look restored and answer nothing. + expect(rows[0].composition).toEqual([ + "BLOOD_PRESSURE", + "SLEEP", + "ADIPOSITY", + ]); + expect(rows[0].pillarScores).toEqual({ + BLOOD_PRESSURE: 86, + SLEEP: 100, + ADIPOSITY: 42, + }); + }); + it("writes the account's own cycle symptom back before resolving its links", async () => { const { written, res } = await roundTrip(); diff --git a/src/app/api/export/__tests__/per-type-routes.test.ts b/src/app/api/export/__tests__/per-type-routes.test.ts index 7bb572d87..b1823aecb 100644 --- a/src/app/api/export/__tests__/per-type-routes.test.ts +++ b/src/app/api/export/__tests__/per-type-routes.test.ts @@ -27,6 +27,7 @@ vi.mock("@/lib/db", () => ({ // The hourly shape of a cumulative day, read by // `buildIntradayProfileBackupSection`. intradayCumulativeProfile: { findMany: vi.fn().mockResolvedValue([]) }, + healthScoreRecord: { findMany: vi.fn().mockResolvedValue([]) }, // v1.15.0 — cycle tables read by the full-backup helper. cycleProfile: { findUnique: vi.fn() }, menstrualCycle: { findMany: vi.fn() }, @@ -96,6 +97,7 @@ beforeEach(() => { vi.mocked(prisma.intradayCumulativeProfile.findMany).mockResolvedValue( [] as never, ); + vi.mocked(prisma.healthScoreRecord.findMany).mockResolvedValue([] as never); }); afterEach(() => { diff --git a/src/app/api/export/encrypted/__tests__/route.test.ts b/src/app/api/export/encrypted/__tests__/route.test.ts index 4a741cf58..4ac966527 100644 --- a/src/app/api/export/encrypted/__tests__/route.test.ts +++ b/src/app/api/export/encrypted/__tests__/route.test.ts @@ -25,6 +25,7 @@ vi.mock("@/lib/db", () => ({ // The hourly shape of a cumulative day, read by // `buildIntradayProfileBackupSection`. intradayCumulativeProfile: { findMany: vi.fn().mockResolvedValue([]) }, + healthScoreRecord: { findMany: vi.fn().mockResolvedValue([]) }, cycleProfile: { findUnique: vi.fn().mockResolvedValue(null) }, menstrualCycle: { findMany: vi.fn().mockResolvedValue([]) }, cycleDayLog: { findMany: vi.fn().mockResolvedValue([]) }, diff --git a/src/lib/export/__tests__/full-backup-payload.test.ts b/src/lib/export/__tests__/full-backup-payload.test.ts index eb0e5794d..810174ed8 100644 --- a/src/lib/export/__tests__/full-backup-payload.test.ts +++ b/src/lib/export/__tests__/full-backup-payload.test.ts @@ -204,6 +204,7 @@ function makePrisma() { }, ]), }, + healthScoreRecord: { findMany: vi.fn().mockResolvedValue([]) }, // Left unmocked on purpose: `buildProfileBackupSection` runs for real // against these, so the assertions below exercise the builder rather than // a stand-in that would agree with whatever the payload happened to do.