mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
test(sync): pin the delta feed before moving its domain pages
This commit is contained in:
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
|
||||
}
|
||||
]
|
||||
}
|
||||
+49
@@ -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 });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user