From 422adf29861db26b56398c46a87895e4d4c8364e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 19:00:54 +0200 Subject: [PATCH] test(measurements): pin that a stats re-post with a new source version, device or sleep stage still writes The unchanged-stats shortcut skips the write when every overwritten column matches the stored row. The kit test covered a new value and a new device; it now also re-posts a step total that only names a new HealthKit source version and a sleep row that only changes its stage, and checks each is written with its syncVersion bumped and the tail started. --- ...measurements-batch-unchanged-stats.test.ts | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/tests/integration/routes/measurements-batch-unchanged-stats.test.ts b/tests/integration/routes/measurements-batch-unchanged-stats.test.ts index d4a0b6cd2..367c40dd4 100644 --- a/tests/integration/routes/measurements-batch-unchanged-stats.test.ts +++ b/tests/integration/routes/measurements-batch-unchanged-stats.test.ts @@ -9,8 +9,9 @@ * `updatedAt`), and starts nothing: no rollup refold, no cache * invalidation, no PR detection; * - goes the old way as soon as one column differs (the value, the unit, - * the device), and for a tombstoned row (a resurrection), so those are - * written and start the tail as before; + * the device, the source version, the sleep stage), and for a tombstoned + * row (a resurrection), so those are written and start the tail as + * before; * - costs a handful of statements for a batch of them, instead of a * reconciler transaction each. */ @@ -72,8 +73,14 @@ const prisma = getPrismaClient(); const ROUTE = "/api/measurements/batch"; const DAYS = ["2026-10-01", "2026-10-02", "2026-10-03", "2026-10-04"]; -function stepsEntry(day: string, value: number, deviceType = "watch") { +function stepsEntry( + day: string, + value: number, + deviceType = "watch", + externalSourceVersion?: string, +) { return { + ...(externalSourceVersion ? { externalSourceVersion } : {}), hkIdentifier: "HKQuantityTypeIdentifierStepCount", value, unit: "count", @@ -166,6 +173,65 @@ describe("an unchanged stats: re-post", () => { expect(tail.prs).toBe(1); }); + it("writes a re-post that changes only the source version, the device or the sleep stage", async () => { + // The overwrite contract of a `stats:` row covers these columns as much as + // the value: a re-post that only names a new HealthKit source version, a + // new device or a new sleep stage replaces the stored one. + const night = { + hkIdentifier: "HKCategoryTypeIdentifierSleepAnalysis", + value: 95, + unit: "category", + startDate: "2026-10-05T01:00:00.000Z", + endDate: "2026-10-05T02:35:00.000Z", + externalId: "stats:HKCategoryTypeIdentifierSleepAnalysis:2026-10-05", + sleepStage: 3, + }; + const seeded = await post([night]); + expect(seeded.data.inserted).toBe(1); + const sleepRow = () => + prisma.measurement.findFirstOrThrow({ + where: { userId, externalId: night.externalId }, + select: { syncVersion: true, sleepStage: true }, + }); + const sleepBefore = await sleepRow(); + const before = (await storedVersions()).map((r) => r.syncVersion); + resetTail(); + + const res = await post([ + stepsEntry(DAYS[0]!, 1000, "watch", "17.1"), // new source version + stepsEntry(DAYS[1]!, 1001, "phone"), // new device + { ...night, sleepStage: 4 }, // new sleep stage + ]); + expect(res.status).toBe(200); + expect(res.data.entries.map((e) => e.status)).toEqual([ + "updated", + "updated", + "updated", + ]); + + const after = await prisma.measurement.findMany({ + where: { userId, externalId: { startsWith: "stats:HKQuantity" } }, + orderBy: { externalId: "asc" }, + select: { + syncVersion: true, + externalSourceVersion: true, + deviceType: true, + }, + }); + expect(after.map((r) => r.syncVersion)).toEqual([ + before[0]! + 1, + before[1]! + 1, + before[2], + before[3], + ]); + expect(after[0]!.externalSourceVersion).toBe("17.1"); + expect(after[1]!.deviceType).toBe("phone"); + const sleepAfter = await sleepRow(); + expect(sleepAfter.sleepStage).toBe("DEEP"); + expect(sleepAfter.syncVersion).toBe(sleepBefore.syncVersion + 1); + expect(tail.rollups).toBe(1); + }); + it("resurrects a tombstoned row the old way", async () => { await prisma.measurement.updateMany({ where: {