test(sync): pin the delta feed before moving its domain pages

This commit is contained in:
Marc-André Bombeck committed 2026-10-23 14:40:37 +02:00
1 parent 3e9ed8e2e3
commit 29620ffa49
9 files changed
+1136

No files matched your search

@@ -0,0 +1,230 @@
{
"status": 200,
"body": {
"data": {
"serverNow": "<now>",
"cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDQ4MjAwMDAwLCJpIjoibS0wMiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzAwMDAwMCwiaSI6ImluLTAyIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU2NjAwMDAwLCJpIjoiY2QtMDIifSwiY3ljbGVzIjp7InUiOjE3OTIwNTQ4MDAwMDAsImkiOiJjeS0wMiJ9fX0",
"hasMore": true,
"cursorExpired": false,
"changes": {
"measurements": {
"upserts": [
{
"id": "m-01",
"externalId": null,
"type": "WEIGHT",
"value": 81.2,
"unit": "kg",
"measuredAt": "2026-10-14T06:30:00.000Z",
"source": "MANUAL",
"notes": "after the run",
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
},
{
"id": "m-02",
"externalId": "w-pulse-1",
"type": "PULSE",
"value": 61,
"unit": "bpm",
"measuredAt": "2026-10-14T07:00:00.000Z",
"source": "WITHINGS",
"notes": null,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
}
],
"tombstones": []
},
"mood": {
"upserts": [
{
"id": "mood-01",
"date": "2026-10-14",
"mood": "GUT",
"score": 4,
"a1": 7,
"a2": 3,
"a3": null,
"a4": null,
"a5": null,
"context": {
"workStatus": "regular",
"workMinutes": 480,
"overtimeMinutes": null,
"workLoad": null,
"workSatisfaction": null,
"contactCircles": [],
"contactForm": null,
"contactExtent": null,
"contactQuality": null,
"contactSupport": null,
"leisureCategories": [],
"leisureMinutes": null,
"leisureJoy": null,
"leisureRecovery": null,
"eventType": null,
"eventValence": null,
"eventAt": null,
"note": null
},
"tags": "[\"slept well\"]",
"note": "a calm morning",
"moodLoggedAt": "2026-10-14T07:00:00.000Z",
"source": "MANUAL",
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
}
],
"tombstones": [
{
"id": "mood-02",
"syncVersion": "<syncVersion>",
"deletedAt": "2026-10-15T11:30:00.000Z",
"updatedAt": "<updatedAt>"
}
]
},
"intakes": {
"upserts": [
{
"id": "in-01",
"medicationId": "<id:1>",
"scheduledFor": "2026-10-14T06:00:00.000Z",
"takenAt": "2026-10-14T06:10:00.000Z",
"skipped": false,
"source": "WEB",
"autoRecorded": false,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
},
{
"id": "in-02",
"medicationId": "<id:1>",
"scheduledFor": "2026-10-13T06:00:00.000Z",
"takenAt": "2026-10-13T06:00:00.000Z",
"skipped": false,
"source": "WEB",
"autoRecorded": true,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
}
],
"tombstones": []
},
"cycleDays": {
"upserts": [
{
"id": "cd-01",
"date": "2026-10-10",
"cycleId": "cy-02",
"flow": "MEDIUM",
"intermenstrualBleeding": false,
"basalBodyTempC": null,
"temperatureExcluded": false,
"ovulationTest": null,
"cervicalMucus": null,
"cervixPosition": null,
"cervixFirmness": null,
"cervixOpening": null,
"sexualActivity": false,
"protectedSex": null,
"pregnancyTest": null,
"progesteroneTest": null,
"contraceptive": null,
"symptoms": [],
"note": null,
"source": "MANUAL",
"externalId": null,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>",
"deletedAt": null
},
{
"id": "cd-02",
"date": "2026-10-11",
"cycleId": "cy-02",
"flow": "LIGHT",
"intermenstrualBleeding": false,
"basalBodyTempC": 36.4,
"temperatureExcluded": false,
"ovulationTest": null,
"cervicalMucus": null,
"cervixPosition": null,
"cervixFirmness": null,
"cervixOpening": null,
"sexualActivity": false,
"protectedSex": null,
"pregnancyTest": null,
"progesteroneTest": null,
"contraceptive": null,
"symptoms": [],
"note": null,
"source": "MANUAL",
"externalId": "hk-cycle-1",
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>",
"deletedAt": null
}
],
"tombstones": []
},
"cycles": {
"upserts": [
{
"id": "cy-01",
"startDate": "2026-09-12",
"endDate": "2026-10-09",
"periodEndDate": "2026-09-16",
"lengthDays": 28,
"ovulationDate": null,
"ovulationConfirmed": false,
"isPredicted": false,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
},
{
"id": "cy-02",
"startDate": "2026-10-10",
"endDate": null,
"periodEndDate": null,
"lengthDays": null,
"ovulationDate": null,
"ovulationConfirmed": false,
"isPredicted": false,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
}
],
"tombstones": []
}
}
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "sync.changes.pull"
},
"meta": {
"measurement_upserts": 2,
"measurement_tombstones": 0,
"mood_upserts": 1,
"mood_tombstones": 1,
"intake_upserts": 2,
"intake_tombstones": 0,
"cycle_day_upserts": 2,
"cycle_day_tombstones": 0,
"cycle_upserts": 2,
"cycle_tombstones": 0,
"cycle_module_enabled": true,
"has_more": true,
"cursor_present": false
},
"warnings": null,
"error": null
}
]
}
@@ -0,0 +1,102 @@
{
"status": 200,
"body": {
"data": {
"serverNow": "<now>",
"cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDQ5NDAwMDAwLCJpIjoibS0wNCJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzYwMDAwMCwiaSI6ImluLTAzIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU3MjAwMDAwLCJpIjoiY2QtMDMifSwiY3ljbGVzIjp7InUiOjE3OTIwNTU0MDAwMDAsImkiOiJjeS0wMyJ9fX0",
"hasMore": true,
"cursorExpired": false,
"changes": {
"measurements": {
"upserts": [
{
"id": "m-04",
"externalId": "stats:HKQuantityTypeIdentifierHeartRate:2026-06-17T09",
"type": "PULSE",
"value": 64,
"unit": "bpm",
"measuredAt": "2026-06-17T09:30:00.000Z",
"source": "APPLE_HEALTH",
"notes": null,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
}
],
"tombstones": [
{
"id": "m-03",
"externalId": "w-weight-2",
"syncVersion": "<syncVersion>",
"deletedAt": "2026-10-15T11:30:00.000Z",
"updatedAt": "<updatedAt>"
}
]
},
"mood": {
"upserts": [],
"tombstones": []
},
"intakes": {
"upserts": [],
"tombstones": [
{
"id": "in-03",
"syncVersion": "<syncVersion>",
"deletedAt": "2026-10-15T11:30:00.000Z",
"updatedAt": "<updatedAt>"
}
]
},
"cycleDays": {
"upserts": [],
"tombstones": [
{
"id": "cd-03",
"externalId": "hk-cycle-0",
"syncVersion": "<syncVersion>",
"deletedAt": "2026-10-15T11:30:00.000Z",
"updatedAt": "<updatedAt>"
}
]
},
"cycles": {
"upserts": [],
"tombstones": [
{
"id": "cy-03",
"syncVersion": "<syncVersion>",
"deletedAt": "2026-10-15T11:30:00.000Z",
"updatedAt": "<updatedAt>"
}
]
}
}
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "sync.changes.pull"
},
"meta": {
"measurement_upserts": 1,
"measurement_tombstones": 1,
"mood_upserts": 0,
"mood_tombstones": 0,
"intake_upserts": 0,
"intake_tombstones": 1,
"cycle_day_upserts": 0,
"cycle_day_tombstones": 1,
"cycle_upserts": 0,
"cycle_tombstones": 1,
"cycle_module_enabled": true,
"has_more": true,
"cursor_present": true
},
"warnings": null,
"error": null
}
]
}
@@ -0,0 +1,72 @@
{
"status": 200,
"body": {
"data": {
"serverNow": "<now>",
"cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDUwNjAwMDAwLCJpIjoibS0wNiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzYwMDAwMCwiaSI6ImluLTAzIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU3MjAwMDAwLCJpIjoiY2QtMDMifSwiY3ljbGVzIjp7InUiOjE3OTIwNTU0MDAwMDAsImkiOiJjeS0wMyJ9fX0",
"hasMore": false,
"cursorExpired": false,
"changes": {
"measurements": {
"upserts": [
{
"id": "m-06",
"externalId": null,
"type": "BLOOD_PRESSURE_SYS",
"value": 128,
"unit": "mmHg",
"measuredAt": "2026-10-14T08:00:00.000Z",
"source": "MANUAL",
"notes": null,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
}
],
"tombstones": []
},
"mood": {
"upserts": [],
"tombstones": []
},
"intakes": {
"upserts": [],
"tombstones": []
},
"cycleDays": {
"upserts": [],
"tombstones": []
},
"cycles": {
"upserts": [],
"tombstones": []
}
}
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "sync.changes.pull"
},
"meta": {
"measurement_upserts": 1,
"measurement_tombstones": 0,
"mood_upserts": 0,
"mood_tombstones": 0,
"intake_upserts": 0,
"intake_tombstones": 0,
"cycle_day_upserts": 0,
"cycle_day_tombstones": 0,
"cycle_upserts": 0,
"cycle_tombstones": 0,
"cycle_module_enabled": true,
"has_more": false,
"cursor_present": true
},
"warnings": null,
"error": null
}
]
}
@@ -0,0 +1,59 @@
{
"status": 200,
"body": {
"data": {
"serverNow": "<now>",
"cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDUwNjAwMDAwLCJpIjoibS0wNiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzYwMDAwMCwiaSI6ImluLTAzIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU3MjAwMDAwLCJpIjoiY2QtMDMifSwiY3ljbGVzIjp7InUiOjE3OTIwNTU0MDAwMDAsImkiOiJjeS0wMyJ9fX0",
"hasMore": false,
"cursorExpired": false,
"changes": {
"measurements": {
"upserts": [],
"tombstones": []
},
"mood": {
"upserts": [],
"tombstones": []
},
"intakes": {
"upserts": [],
"tombstones": []
},
"cycleDays": {
"upserts": [],
"tombstones": []
},
"cycles": {
"upserts": [],
"tombstones": []
}
}
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "sync.changes.pull"
},
"meta": {
"measurement_upserts": 0,
"measurement_tombstones": 0,
"mood_upserts": 0,
"mood_tombstones": 0,
"intake_upserts": 0,
"intake_tombstones": 0,
"cycle_day_upserts": 0,
"cycle_day_tombstones": 0,
"cycle_upserts": 0,
"cycle_tombstones": 0,
"cycle_module_enabled": true,
"has_more": false,
"cursor_present": true
},
"warnings": null,
"error": null
}
]
}
@@ -0,0 +1,49 @@
{
"status": 200,
"body": {
"data": {
"serverNow": "<now>",
"cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzc0Nzg1NjAwMDAwLCJpIjoibS0wMSJ9fX0",
"hasMore": false,
"cursorExpired": true,
"changes": {
"measurements": {
"upserts": [],
"tombstones": []
},
"mood": {
"upserts": [],
"tombstones": []
},
"intakes": {
"upserts": [],
"tombstones": []
},
"cycleDays": {
"upserts": [],
"tombstones": []
},
"cycles": {
"upserts": [],
"tombstones": []
}
}
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "sync.changes.pull"
},
"meta": {
"cursor_expired": true,
"cursor_expired_reason": "retention",
"returned": 0
},
"warnings": null,
"error": null
}
]
}
@@ -0,0 +1,49 @@
{
"status": 200,
"body": {
"data": {
"serverNow": "<now>",
"cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDQ4MjAwMDAwLCJpIjoibS0wMiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzAwMDAwMCwiaSI6ImluLTAyIn0sImN5Y2xlRGF5cyI6eyJ1IjoxNzkyMDU2NjAwMDAwLCJpIjoiY2QtMDIifSwiY3ljbGVzIjp7InUiOjE3OTIwNTQ4MDAwMDAsImkiOiJjeS0wMiJ9fX0",
"hasMore": false,
"cursorExpired": true,
"changes": {
"measurements": {
"upserts": [],
"tombstones": []
},
"mood": {
"upserts": [],
"tombstones": []
},
"intakes": {
"upserts": [],
"tombstones": []
},
"cycleDays": {
"upserts": [],
"tombstones": []
},
"cycles": {
"upserts": [],
"tombstones": []
}
}
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "sync.changes.pull"
},
"meta": {
"cursor_expired": true,
"cursor_expired_reason": "sync_reset",
"returned": 0
},
"warnings": null,
"error": null
}
]
}
@@ -0,0 +1,191 @@
{
"status": 200,
"body": {
"data": {
"serverNow": "<now>",
"cursor": "eyJ2IjoyLCJ0IjoxNzkyMDY1NjAwMDAwLCJkIjp7Im1lYXN1cmVtZW50cyI6eyJ1IjoxNzkyMDUwNjAwMDAwLCJpIjoibS0wNiJ9LCJtb29kIjp7InUiOjE3OTIwNTE4MDAwMDAsImkiOiJtb29kLTAyIn0sImludGFrZXMiOnsidSI6MTc5MjA1MzYwMDAwMCwiaSI6ImluLTAzIn19fQ",
"hasMore": false,
"cursorExpired": false,
"changes": {
"measurements": {
"upserts": [
{
"id": "m-01",
"externalId": null,
"type": "WEIGHT",
"value": 81.2,
"unit": "kg",
"measuredAt": "2026-10-14T06:30:00.000Z",
"source": "MANUAL",
"notes": "after the run",
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
},
{
"id": "m-02",
"externalId": "w-pulse-1",
"type": "PULSE",
"value": 61,
"unit": "bpm",
"measuredAt": "2026-10-14T07:00:00.000Z",
"source": "WITHINGS",
"notes": null,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
},
{
"id": "m-04",
"externalId": "stats:HKQuantityTypeIdentifierHeartRate:2026-06-17T09",
"type": "PULSE",
"value": 64,
"unit": "bpm",
"measuredAt": "2026-06-17T09:30:00.000Z",
"source": "APPLE_HEALTH",
"notes": null,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
},
{
"id": "m-06",
"externalId": null,
"type": "BLOOD_PRESSURE_SYS",
"value": 128,
"unit": "mmHg",
"measuredAt": "2026-10-14T08:00:00.000Z",
"source": "MANUAL",
"notes": null,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
}
],
"tombstones": [
{
"id": "m-03",
"externalId": "w-weight-2",
"syncVersion": "<syncVersion>",
"deletedAt": "2026-10-15T11:30:00.000Z",
"updatedAt": "<updatedAt>"
}
]
},
"mood": {
"upserts": [
{
"id": "mood-01",
"date": "2026-10-14",
"mood": "GUT",
"score": 4,
"a1": 7,
"a2": 3,
"a3": null,
"a4": null,
"a5": null,
"context": {
"workStatus": "regular",
"workMinutes": 480,
"overtimeMinutes": null,
"workLoad": null,
"workSatisfaction": null,
"contactCircles": [],
"contactForm": null,
"contactExtent": null,
"contactQuality": null,
"contactSupport": null,
"leisureCategories": [],
"leisureMinutes": null,
"leisureJoy": null,
"leisureRecovery": null,
"eventType": null,
"eventValence": null,
"eventAt": null,
"note": null
},
"tags": "[\"slept well\"]",
"note": "a calm morning",
"moodLoggedAt": "2026-10-14T07:00:00.000Z",
"source": "MANUAL",
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
}
],
"tombstones": [
{
"id": "mood-02",
"syncVersion": "<syncVersion>",
"deletedAt": "2026-10-15T11:30:00.000Z",
"updatedAt": "<updatedAt>"
}
]
},
"intakes": {
"upserts": [
{
"id": "in-01",
"medicationId": "<id:1>",
"scheduledFor": "2026-10-14T06:00:00.000Z",
"takenAt": "2026-10-14T06:10:00.000Z",
"skipped": false,
"source": "WEB",
"autoRecorded": false,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
},
{
"id": "in-02",
"medicationId": "<id:1>",
"scheduledFor": "2026-10-13T06:00:00.000Z",
"takenAt": "2026-10-13T06:00:00.000Z",
"skipped": false,
"source": "WEB",
"autoRecorded": true,
"syncVersion": "<syncVersion>",
"updatedAt": "<updatedAt>"
}
],
"tombstones": [
{
"id": "in-03",
"syncVersion": "<syncVersion>",
"deletedAt": "2026-10-15T11:30:00.000Z",
"updatedAt": "<updatedAt>"
}
]
},
"cycleDays": {
"upserts": [],
"tombstones": []
},
"cycles": {
"upserts": [],
"tombstones": []
}
}
},
"error": null
},
"events": [
{
"status": 200,
"action": {
"name": "sync.changes.pull"
},
"meta": {
"measurement_upserts": 4,
"measurement_tombstones": 1,
"mood_upserts": 1,
"mood_tombstones": 1,
"intake_upserts": 2,
"intake_tombstones": 1,
"cycle_day_upserts": 0,
"cycle_day_tombstones": 0,
"cycle_upserts": 0,
"cycle_tombstones": 0,
"cycle_module_enabled": false,
"has_more": false,
"cursor_present": false
},
"warnings": null,
"error": null
}
]
}
@@ -0,0 +1,29 @@
{
"status": 422,
"body": {
"data": null,
"error": "Invalid sync query",
"details": {
"issues": [
{
"path": "limit",
"code": "too_small",
"message": "Too small: expected number to be >=1"
}
]
},
"meta": {
"errorCode": "http.invalid",
"requestId": "<requestId>"
}
},
"events": [
{
"status": 422,
"action": null,
"meta": null,
"warnings": null,
"error": null
}
]
}
@@ -0,0 +1,355 @@
/**
* Parity for `GET /api/sync/changes` while its domain pages move into
* `src/lib/sync/`.
*
* One account holds live and deleted rows in all five domains (a sealed note
* on a measurement and a mood entry, a mood context, a recorded and an
* auto-recorded intake, cycle days and cycles), plus a raw sample that a live
* hourly row folded away. The walk drains the feed in small pages to the
* end, pulls once caught up, then asks with a cursor past retention, a
* cursor from before a reset, the cycle module switched off, and a refused
* query. Every row carries a fixed id and `updatedAt`, so the keyset order
* and the opaque cursor are the same on every run.
*/
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { GET } from "@/app/api/sync/changes/route";
import { encryptNote } from "@/lib/crypto/note-cipher";
import { encodeCursor } from "@/lib/sync/cursor";
import { callRoute, type RouteAuth } from "../../../kit/call-route";
import { makeMedication, makeSession, makeUser } from "../../../kit/factories";
import {
eventFacts,
expectParity,
stable,
wideEventRecorder,
} from "../../../kit/parity";
import { getPrismaClient, truncateAllTables } from "../../setup";
vi.mock("@/lib/logging/transports", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@/lib/logging/transports")>();
const { wideEventRecorder: recorder } = await import("../../../kit/parity");
return {
...actual,
emitIfSampled: (event: (typeof recorder.events)[number]) => {
recorder.events.push(event);
},
};
});
const prisma = getPrismaClient();
const NOW = new Date("2026-10-15T12:00:00.000Z");
const MIN = 60_000;
const DAY = 86_400_000;
let userId: string;
let owner: RouteAuth;
/** An `updatedAt` some minutes before the clock, one per row. */
function touched(minutesAgo: number): Date {
return new Date(NOW.getTime() - minutesAgo * MIN);
}
async function seed(): Promise<void> {
const deletedAt = touched(30);
await prisma.measurement.createMany({
data: [
{
id: "m-01",
userId,
type: "WEIGHT",
value: 81.2,
unit: "kg",
measuredAt: new Date("2026-10-14T06:30:00.000Z"),
source: "MANUAL",
notesEncrypted: encryptNote("after the run"),
updatedAt: touched(300),
},
{
id: "m-02",
userId,
type: "PULSE",
value: 61,
unit: "bpm",
measuredAt: new Date("2026-10-14T07:00:00.000Z"),
source: "WITHINGS",
externalId: "w-pulse-1",
updatedAt: touched(290),
},
{
id: "m-03",
userId,
type: "WEIGHT",
value: 81.6,
unit: "kg",
measuredAt: new Date("2026-10-13T06:30:00.000Z"),
source: "WITHINGS",
externalId: "w-weight-2",
deletedAt,
updatedAt: touched(280),
},
// The hourly row and a raw sample it folded away, four months back:
// the sample's tombstone is the fold's, not a deletion anyone made.
{
id: "m-04",
userId,
type: "PULSE",
value: 64,
unit: "bpm",
measuredAt: new Date("2026-06-17T09:30:00.000Z"),
source: "APPLE_HEALTH",
externalId: "stats:HKQuantityTypeIdentifierHeartRate:2026-06-17T09",
updatedAt: touched(270),
},
{
id: "m-05",
userId,
type: "PULSE",
value: 66,
unit: "bpm",
measuredAt: new Date("2026-06-17T09:12:00.000Z"),
source: "APPLE_HEALTH",
externalId: "uuid-raw-pulse-1",
deletedAt: touched(260),
updatedAt: touched(260),
},
{
id: "m-06",
userId,
type: "BLOOD_PRESSURE_SYS",
value: 128,
unit: "mmHg",
measuredAt: new Date("2026-10-14T08:00:00.000Z"),
source: "MANUAL",
updatedAt: touched(250),
},
],
});
await prisma.moodEntry.createMany({
data: [
{
id: "mood-01",
userId,
date: "2026-10-14",
tz: "Europe/Berlin",
mood: "GUT",
score: 4,
moodA1: 7,
stressA2: 3,
tags: JSON.stringify(["slept well"]),
noteEncrypted: encryptNote("a calm morning"),
moodLoggedAt: new Date("2026-10-14T07:00:00.000Z"),
source: "MANUAL",
updatedAt: touched(240),
},
{
id: "mood-02",
userId,
date: "2026-10-13",
tz: "Europe/Berlin",
mood: "SCHLECHT",
score: 2,
moodA1: 3,
moodLoggedAt: new Date("2026-10-13T20:00:00.000Z"),
source: "MANUAL",
deletedAt,
updatedAt: touched(230),
},
],
});
await prisma.moodContext.create({
data: {
moodEntryId: "mood-01",
userId,
workStatus: "regular",
workMinutes: 480,
},
});
const medication = await makeMedication({ id: userId });
await prisma.medicationIntakeEvent.createMany({
data: [
{
id: "in-01",
userId,
medicationId: medication.id,
scheduledFor: new Date("2026-10-14T06:00:00.000Z"),
takenAt: new Date("2026-10-14T06:10:00.000Z"),
source: "WEB",
updatedAt: touched(220),
},
{
id: "in-02",
userId,
medicationId: medication.id,
scheduledFor: new Date("2026-10-13T06:00:00.000Z"),
takenAt: new Date("2026-10-13T06:00:00.000Z"),
autoRecorded: true,
source: "WEB",
updatedAt: touched(210),
},
{
id: "in-03",
userId,
medicationId: medication.id,
scheduledFor: new Date("2026-10-12T06:00:00.000Z"),
skipped: true,
source: "WEB",
deletedAt,
updatedAt: touched(200),
},
],
});
await prisma.menstrualCycle.createMany({
data: [
{
id: "cy-01",
userId,
startDate: "2026-09-12",
endDate: "2026-10-09",
lengthDays: 28,
periodEndDate: "2026-09-16",
updatedAt: touched(190),
},
{
id: "cy-02",
userId,
startDate: "2026-10-10",
updatedAt: touched(180),
},
{
id: "cy-03",
userId,
startDate: "2026-08-01",
deletedAt,
updatedAt: touched(170),
},
],
});
await prisma.cycleDayLog.createMany({
data: [
{
id: "cd-01",
userId,
date: "2026-10-10",
cycleId: "cy-02",
flow: "MEDIUM",
updatedAt: touched(160),
},
{
id: "cd-02",
userId,
date: "2026-10-11",
cycleId: "cy-02",
flow: "LIGHT",
basalBodyTempC: 36.4,
externalId: "hk-cycle-1",
updatedAt: touched(150),
},
{
id: "cd-03",
userId,
date: "2026-10-09",
externalId: "hk-cycle-0",
deletedAt,
updatedAt: touched(140),
},
],
});
}
type Feed = { cursor: string | null; hasMore: boolean };
async function pull(
name: string,
query: Record<string, string | number>,
): Promise<Feed> {
wideEventRecorder.take();
const res = await callRoute<Feed>(GET, {
method: "GET",
route: "/api/sync/changes",
query,
auth: owner,
});
const events = wideEventRecorder.take();
expect(events.length).toBeGreaterThan(0);
await expectParity(
`sync-changes.${name}`,
stable(
{ status: res.status, body: res.body, events: events.map(eventFacts) },
{ since: NOW },
),
);
return res.data;
}
beforeAll(async () => {
vi.useFakeTimers({ toFake: ["Date"], now: NOW });
await truncateAllTables(prisma);
const user = await makeUser({ gender: "FEMALE", timezone: "UTC" });
userId = user.id;
owner = await makeSession(user);
await prisma.cycleProfile.create({ data: { userId } });
await seed();
});
afterAll(() => {
vi.useRealTimers();
});
describe("GET /api/sync/changes parity", () => {
let firstCursor: string;
it("drains the feed in small pages to the end", async () => {
const first = await pull("01-first-page", { limit: 2 });
expect(first.hasMore).toBe(true);
firstCursor = first.cursor!;
const second = await pull("02-second-page", {
limit: 2,
cursor: first.cursor!,
});
const rest = await pull("03-the-rest", {
limit: 500,
cursor: second.cursor!,
});
expect(rest.hasMore).toBe(false);
await pull("04-caught-up", { cursor: rest.cursor! });
});
it("a cursor past retention, and one from before a reset", async () => {
await pull("05-past-retention", {
cursor: encodeCursor(
{
measurements: { updatedAtMs: NOW.getTime() - 200 * DAY, id: "m-01" },
},
NOW.getTime(),
),
});
await prisma.user.update({
where: { id: userId },
data: { syncResetAt: new Date(NOW.getTime() + MIN) },
});
await pull("06-before-reset", { cursor: firstCursor });
await prisma.user.update({
where: { id: userId },
data: { syncResetAt: null },
});
});
it("the cycle module switched off", async () => {
await prisma.cycleProfile.update({
where: { userId },
data: { cycleTrackingEnabled: false },
});
await pull("07-cycle-off", { limit: 500 });
await prisma.cycleProfile.update({
where: { userId },
data: { cycleTrackingEnabled: null },
});
});
it("a refused query", async () => {
await pull("08-invalid", { limit: 0 });
});
});