From 8feeaed473cdebb5dfd63669a8ce8a372d9b738a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 23 Oct 2026 14:05:55 +0200 Subject: [PATCH] test(measurements): pin the measurement series before moving its readers A parity walk over GET /api/measurements/series through every reader it chooses between: the plain and ladder-picked rows, pulse with its spread, the paired blood pressure (two sources and a corrupt systolic), the sleep nights, glucose raw and day-bucketed, a dense stream bucketed per hour, a refused query and a delegate in another language. The readers cut their windows against the database clock, so the record is laid out from the start of the current UTC day and the snapshot holds instants and day keys relative to it. --- .../measurement-series.01-weight.json.snap | 154 +++ .../measurement-series.02-steps.json.snap | 118 ++ ...asurement-series.03-pulse-spread.json.snap | 190 +++ ...urement-series.04-blood-pressure.json.snap | 187 +++ .../measurement-series.05-sleep.json.snap | 190 +++ ...easurement-series.06-glucose-raw.json.snap | 171 +++ ...asurement-series.07-glucose-long.json.snap | 237 ++++ ...ement-series.08-hrv-hour-buckets.json.snap | 1066 +++++++++++++++++ .../measurement-series.09-invalid.json.snap | 56 + .../measurement-series.10-delegated.json.snap | 115 ++ .../parity/measurement-series.parity.test.ts | 308 +++++ 11 files changed, 2792 insertions(+) create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.01-weight.json.snap create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.02-steps.json.snap create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.03-pulse-spread.json.snap create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.04-blood-pressure.json.snap create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.05-sleep.json.snap create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.06-glucose-raw.json.snap create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.07-glucose-long.json.snap create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.08-hrv-hour-buckets.json.snap create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.09-invalid.json.snap create mode 100644 tests/integration/routes/parity/__snapshots__/measurement-series.10-delegated.json.snap create mode 100644 tests/integration/routes/parity/measurement-series.parity.test.ts diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.01-weight.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.01-weight.json.snap new file mode 100644 index 000000000..13e3bb115 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.01-weight.json.snap @@ -0,0 +1,154 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "weight", + "unit": "kg", + "points": [ + { + "id": "", + "at": "anchor-20d+06:30:00", + "value": 78, + "secondary": null + }, + { + "id": "", + "at": "anchor-19d+06:30:00", + "value": 78.1, + "secondary": null + }, + { + "id": "", + "at": "anchor-18d+06:30:00", + "value": 78.2, + "secondary": null + }, + { + "id": "", + "at": "anchor-17d+06:30:00", + "value": 78.3, + "secondary": null + }, + { + "id": "", + "at": "anchor-16d+06:30:00", + "value": 78.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-15d+06:30:00", + "value": 78.5, + "secondary": null + }, + { + "id": "", + "at": "anchor-13d+06:30:00", + "value": 78.7, + "secondary": null + }, + { + "id": "", + "at": "anchor-12d+06:30:00", + "value": 78.8, + "secondary": null + }, + { + "id": "", + "at": "anchor-11d+06:30:00", + "value": 78.9, + "secondary": null + }, + { + "id": "", + "at": "anchor-10d+06:30:00", + "value": 79, + "secondary": null + }, + { + "id": "", + "at": "anchor-9d+06:30:00", + "value": 79.1, + "secondary": null + }, + { + "id": "", + "at": "anchor-8d+06:30:00", + "value": 79.2, + "secondary": null + }, + { + "id": "", + "at": "anchor-6d+06:30:00", + "value": 79.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-5d+06:30:00", + "value": 79.5, + "secondary": null + }, + { + "id": "", + "at": "anchor-4d+06:30:00", + "value": 79.6, + "secondary": null + }, + { + "id": "", + "at": "anchor-3d+06:30:00", + "value": 79.7, + "secondary": null + }, + { + "id": "", + "at": "anchor-2d+06:30:00", + "value": 79.8, + "secondary": null + }, + { + "id": "", + "at": "anchor-1d+06:30:00", + "value": 79.9, + "secondary": null + } + ], + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 78, + "to": 79.9, + "delta": 1.9, + "sentiment": "negative" + }, + "referenceLines": [], + "stats": { + "mean": 78.95, + "min": 78, + "max": 79.9, + "stdDev": 0.6, + "count": 18 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "weight", + "days": 30, + "count": 18 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.02-steps.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.02-steps.json.snap new file mode 100644 index 000000000..745cab7fa --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.02-steps.json.snap @@ -0,0 +1,118 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "steps", + "unit": "steps", + "points": [ + { + "id": "", + "at": "anchor-13d+12:00:00", + "value": 7300, + "secondary": null + }, + { + "id": "", + "at": "anchor-12d+12:00:00", + "value": 7200, + "secondary": null + }, + { + "id": "", + "at": "anchor-11d+12:00:00", + "value": 7100, + "secondary": null + }, + { + "id": "", + "at": "anchor-10d+12:00:00", + "value": 7000, + "secondary": null + }, + { + "id": "", + "at": "anchor-9d+12:00:00", + "value": 6900, + "secondary": null + }, + { + "id": "", + "at": "anchor-8d+12:00:00", + "value": 6800, + "secondary": null + }, + { + "id": "", + "at": "anchor-6d+12:00:00", + "value": 6600, + "secondary": null + }, + { + "id": "", + "at": "anchor-5d+12:00:00", + "value": 6500, + "secondary": null + }, + { + "id": "", + "at": "anchor-4d+12:00:00", + "value": 6400, + "secondary": null + }, + { + "id": "", + "at": "anchor-3d+12:00:00", + "value": 6300, + "secondary": null + }, + { + "id": "", + "at": "anchor-2d+12:00:00", + "value": 6200, + "secondary": null + }, + { + "id": "", + "at": "anchor-1d+12:00:00", + "value": 6100, + "secondary": null + } + ], + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 7300, + "to": 6100, + "delta": -1200, + "sentiment": "negative" + }, + "referenceLines": [], + "stats": { + "mean": 6700, + "min": 6100, + "max": 7300, + "stdDev": 389.44, + "count": 12 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "steps", + "days": 14, + "count": 12 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.03-pulse-spread.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.03-pulse-spread.json.snap new file mode 100644 index 000000000..86cf96473 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.03-pulse-spread.json.snap @@ -0,0 +1,190 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "pulse", + "unit": "bpm", + "points": [ + { + "id": "", + "at": "anchor-6d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-6d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-6d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-5d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-5d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-5d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-4d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-4d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-4d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-3d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-3d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-3d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-2d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-2d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-2d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + }, + { + "id": "", + "at": "anchor-1d+09:00:00", + "value": 69, + "secondary": null, + "valueMin": 64, + "valueMax": 79 + }, + { + "id": "", + "at": "anchor-1d+14:00:00", + "value": 74, + "secondary": null, + "valueMin": 69, + "valueMax": 84 + }, + { + "id": "", + "at": "anchor-1d+20:00:00", + "value": 80, + "secondary": null, + "valueMin": 75, + "valueMax": 90 + } + ], + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "change": { + "from": 74.33, + "to": 74.33, + "delta": 0, + "sentiment": "positive" + }, + "referenceLines": [], + "stats": { + "mean": 74.33, + "min": 69, + "max": 80, + "stdDev": 4.5, + "count": 18 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "pulse", + "days": 7, + "count": 18 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.04-blood-pressure.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.04-blood-pressure.json.snap new file mode 100644 index 000000000..f1e9eb005 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.04-blood-pressure.json.snap @@ -0,0 +1,187 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "bloodPressure", + "unit": "mmHg", + "points": [ + { + "id": "", + "at": "anchor-20d+07:01:00", + "value": 126, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-19d+07:01:00", + "value": 130, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-18d+07:01:00", + "value": 129, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-17d+07:01:00", + "value": 128, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-16d+07:01:00", + "value": 127, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-15d+07:01:00", + "value": 126, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-13d+07:01:00", + "value": 129, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-12d+07:01:00", + "value": 128, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-11d+07:01:00", + "value": 127, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-10d+07:01:00", + "value": 126, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-9d+07:01:00", + "value": 130, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-8d+07:01:00", + "value": 129, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-6d+07:01:00", + "value": 127, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-5d+07:01:00", + "value": 126, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-4d+07:01:00", + "value": 130, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-3d+07:01:00", + "value": 129, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-2d+07:01:00", + "value": 128, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-1d+07:01:00", + "value": 127, + "secondary": 82 + } + ], + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "change": { + "from": 126, + "to": 127, + "delta": 1, + "sentiment": "neutral" + }, + "referenceLines": [ + { + "value": 120, + "kind": "lower", + "source": "personal", + "appliesTo": "value", + "labelKey": "series.reference.targetLower", + "label": "Your target, lower limit" + }, + { + "value": 129, + "kind": "upper", + "source": "personal", + "appliesTo": "value", + "labelKey": "series.reference.targetUpper", + "label": "Your target, upper limit" + }, + { + "value": 70, + "kind": "lower", + "source": "personal", + "appliesTo": "secondary", + "labelKey": "series.reference.targetLower", + "label": "Your target, lower limit" + }, + { + "value": 79, + "kind": "upper", + "source": "personal", + "appliesTo": "secondary", + "labelKey": "series.reference.targetUpper", + "label": "Your target, upper limit" + } + ], + "stats": { + "mean": 127.89, + "min": 126, + "max": 130, + "stdDev": 1.41, + "count": 18 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "bloodPressure", + "days": 30, + "count": 18 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.05-sleep.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.05-sleep.json.snap new file mode 100644 index 000000000..e9adee053 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.05-sleep.json.snap @@ -0,0 +1,190 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "sleep", + "unit": "h", + "points": [ + { + "id": "sleep:day-13", + "at": "anchor-13d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-12", + "at": "anchor-12d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-11", + "at": "anchor-11d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-10", + "at": "anchor-10d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-9", + "at": "anchor-9d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-8", + "at": "anchor-8d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-6", + "at": "anchor-6d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-5", + "at": "anchor-5d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-4", + "at": "anchor-4d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-3", + "at": "anchor-3d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-2", + "at": "anchor-2d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + }, + { + "id": "sleep:day-1", + "at": "anchor-1d+04:30:00", + "value": 5.5, + "secondary": null, + "sleepStages": { + "CORE": 3, + "DEEP": 1, + "REM": 1.5, + "AWAKE": 0.17 + } + } + ], + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 5.5, + "to": 5.5, + "delta": 0, + "sentiment": "neutral" + }, + "referenceLines": [], + "stats": { + "mean": 5.5, + "min": 5.5, + "max": 5.5, + "stdDev": 0, + "count": 12 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "sleep", + "days": 14, + "count": 12 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.06-glucose-raw.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.06-glucose-raw.json.snap new file mode 100644 index 000000000..f0879d8ac --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.06-glucose-raw.json.snap @@ -0,0 +1,171 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "glucose", + "unit": "mmol/L", + "points": [ + { + "id": "", + "at": "anchor-20d+08:00:00", + "value": 6.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-19d+08:00:00", + "value": 6.3, + "secondary": null + }, + { + "id": "", + "at": "anchor-18d+08:00:00", + "value": 6.3, + "secondary": null + }, + { + "id": "", + "at": "anchor-17d+08:00:00", + "value": 6.2, + "secondary": null + }, + { + "id": "", + "at": "anchor-16d+08:00:00", + "value": 6.2, + "secondary": null + }, + { + "id": "", + "at": "anchor-15d+08:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "", + "at": "anchor-13d+08:00:00", + "value": 6, + "secondary": null + }, + { + "id": "", + "at": "anchor-12d+08:00:00", + "value": 5.9, + "secondary": null + }, + { + "id": "", + "at": "anchor-11d+08:00:00", + "value": 5.9, + "secondary": null + }, + { + "id": "", + "at": "anchor-10d+08:00:00", + "value": 5.8, + "secondary": null + }, + { + "id": "", + "at": "anchor-9d+08:00:00", + "value": 5.8, + "secondary": null + }, + { + "id": "", + "at": "anchor-8d+08:00:00", + "value": 5.7, + "secondary": null + }, + { + "id": "", + "at": "anchor-6d+08:00:00", + "value": 5.6, + "secondary": null + }, + { + "id": "", + "at": "anchor-5d+08:00:00", + "value": 5.5, + "secondary": null + }, + { + "id": "", + "at": "anchor-4d+08:00:00", + "value": 5.5, + "secondary": null + }, + { + "id": "", + "at": "anchor-3d+08:00:00", + "value": 5.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-2d+08:00:00", + "value": 5.4, + "secondary": null + }, + { + "id": "", + "at": "anchor-1d+08:00:00", + "value": 5.3, + "secondary": null + } + ], + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 6.4, + "to": 5.3, + "delta": -1.1, + "sentiment": "positive" + }, + "referenceLines": [ + { + "value": 3.9, + "kind": "lower", + "source": "clinical", + "appliesTo": "value", + "labelKey": "series.reference.clinicalLower", + "label": "Clinical lower limit" + }, + { + "value": 10, + "kind": "upper", + "source": "clinical", + "appliesTo": "value", + "labelKey": "series.reference.clinicalUpper", + "label": "Clinical upper limit" + } + ], + "stats": { + "mean": 5.9, + "min": 5.3, + "max": 6.4, + "stdDev": 0.3, + "count": 18 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "glucose", + "days": 30, + "count": 18 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.07-glucose-long.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.07-glucose-long.json.snap new file mode 100644 index 000000000..e33221200 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.07-glucose-long.json.snap @@ -0,0 +1,237 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "glucose", + "unit": "mmol/L", + "points": [ + { + "id": "day:day-70", + "at": "anchor-70d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-69", + "at": "anchor-69d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-68", + "at": "anchor-68d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-67", + "at": "anchor-67d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-66", + "at": "anchor-66d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-65", + "at": "anchor-65d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-64", + "at": "anchor-64d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-63", + "at": "anchor-63d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-62", + "at": "anchor-62d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-61", + "at": "anchor-61d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-60", + "at": "anchor-60d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-20", + "at": "anchor-20d+00:00:00", + "value": 6.4, + "secondary": null + }, + { + "id": "day:day-19", + "at": "anchor-19d+00:00:00", + "value": 6.3, + "secondary": null + }, + { + "id": "day:day-18", + "at": "anchor-18d+00:00:00", + "value": 6.3, + "secondary": null + }, + { + "id": "day:day-17", + "at": "anchor-17d+00:00:00", + "value": 6.2, + "secondary": null + }, + { + "id": "day:day-16", + "at": "anchor-16d+00:00:00", + "value": 6.2, + "secondary": null + }, + { + "id": "day:day-15", + "at": "anchor-15d+00:00:00", + "value": 6.1, + "secondary": null + }, + { + "id": "day:day-13", + "at": "anchor-13d+00:00:00", + "value": 6, + "secondary": null + }, + { + "id": "day:day-12", + "at": "anchor-12d+00:00:00", + "value": 5.9, + "secondary": null + }, + { + "id": "day:day-11", + "at": "anchor-11d+00:00:00", + "value": 5.9, + "secondary": null + }, + { + "id": "day:day-10", + "at": "anchor-10d+00:00:00", + "value": 5.8, + "secondary": null + }, + { + "id": "day:day-9", + "at": "anchor-9d+00:00:00", + "value": 5.8, + "secondary": null + }, + { + "id": "day:day-8", + "at": "anchor-8d+00:00:00", + "value": 5.7, + "secondary": null + }, + { + "id": "day:day-6", + "at": "anchor-6d+00:00:00", + "value": 5.6, + "secondary": null + }, + { + "id": "day:day-5", + "at": "anchor-5d+00:00:00", + "value": 5.5, + "secondary": null + }, + { + "id": "day:day-4", + "at": "anchor-4d+00:00:00", + "value": 5.5, + "secondary": null + }, + { + "id": "day:day-3", + "at": "anchor-3d+00:00:00", + "value": 5.4, + "secondary": null + }, + { + "id": "day:day-2", + "at": "anchor-2d+00:00:00", + "value": 5.4, + "secondary": null + }, + { + "id": "day:day-1", + "at": "anchor-1d+00:00:00", + "value": 5.3, + "secondary": null + } + ], + "direction": { + "direction": "up-bad", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 6.1, + "to": 5.3, + "delta": -0.8, + "sentiment": "positive" + }, + "referenceLines": [ + { + "value": 3.9, + "kind": "lower", + "source": "clinical", + "appliesTo": "value", + "labelKey": "series.reference.clinicalLower", + "label": "Clinical lower limit" + }, + { + "value": 10, + "kind": "upper", + "source": "clinical", + "appliesTo": "value", + "labelKey": "series.reference.clinicalUpper", + "label": "Clinical upper limit" + } + ], + "stats": { + "mean": 6, + "min": 5.3, + "max": 6.4, + "stdDev": 0.3, + "count": 29 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "glucose", + "days": 365, + "count": 29 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.08-hrv-hour-buckets.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.08-hrv-hour-buckets.json.snap new file mode 100644 index 000000000..a3d19aec3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.08-hrv-hour-buckets.json.snap @@ -0,0 +1,1066 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "heartRateVariability", + "unit": "ms", + "points": [ + { + "id": "hour:anchor-8d+22:00:00", + "at": "anchor-8d+22:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-8d+23:00:00", + "at": "anchor-8d+23:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-7d+00:00:00", + "at": "anchor-7d+00:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-7d+01:00:00", + "at": "anchor-7d+01:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-7d+02:00:00", + "at": "anchor-7d+02:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-7d+03:00:00", + "at": "anchor-7d+03:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-7d+04:00:00", + "at": "anchor-7d+04:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-7d+05:00:00", + "at": "anchor-7d+05:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-7d+06:00:00", + "at": "anchor-7d+06:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-7d+07:00:00", + "at": "anchor-7d+07:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-7d+08:00:00", + "at": "anchor-7d+08:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-7d+09:00:00", + "at": "anchor-7d+09:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-7d+10:00:00", + "at": "anchor-7d+10:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-7d+11:00:00", + "at": "anchor-7d+11:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-7d+12:00:00", + "at": "anchor-7d+12:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-7d+13:00:00", + "at": "anchor-7d+13:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-7d+14:00:00", + "at": "anchor-7d+14:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-7d+15:00:00", + "at": "anchor-7d+15:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-7d+16:00:00", + "at": "anchor-7d+16:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-7d+17:00:00", + "at": "anchor-7d+17:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-7d+18:00:00", + "at": "anchor-7d+18:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-7d+19:00:00", + "at": "anchor-7d+19:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-7d+20:00:00", + "at": "anchor-7d+20:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-7d+21:00:00", + "at": "anchor-7d+21:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-7d+22:00:00", + "at": "anchor-7d+22:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-7d+23:00:00", + "at": "anchor-7d+23:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-6d+00:00:00", + "at": "anchor-6d+00:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-6d+01:00:00", + "at": "anchor-6d+01:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-6d+02:00:00", + "at": "anchor-6d+02:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-6d+03:00:00", + "at": "anchor-6d+03:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-6d+04:00:00", + "at": "anchor-6d+04:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-6d+05:00:00", + "at": "anchor-6d+05:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-6d+06:00:00", + "at": "anchor-6d+06:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-6d+07:00:00", + "at": "anchor-6d+07:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-6d+08:00:00", + "at": "anchor-6d+08:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-6d+09:00:00", + "at": "anchor-6d+09:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-6d+10:00:00", + "at": "anchor-6d+10:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-6d+11:00:00", + "at": "anchor-6d+11:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-6d+12:00:00", + "at": "anchor-6d+12:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-6d+13:00:00", + "at": "anchor-6d+13:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-6d+14:00:00", + "at": "anchor-6d+14:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-6d+15:00:00", + "at": "anchor-6d+15:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-6d+16:00:00", + "at": "anchor-6d+16:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-6d+17:00:00", + "at": "anchor-6d+17:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-6d+18:00:00", + "at": "anchor-6d+18:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-6d+19:00:00", + "at": "anchor-6d+19:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-6d+20:00:00", + "at": "anchor-6d+20:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-6d+21:00:00", + "at": "anchor-6d+21:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-6d+22:00:00", + "at": "anchor-6d+22:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-6d+23:00:00", + "at": "anchor-6d+23:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-5d+00:00:00", + "at": "anchor-5d+00:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-5d+01:00:00", + "at": "anchor-5d+01:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-5d+02:00:00", + "at": "anchor-5d+02:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-5d+03:00:00", + "at": "anchor-5d+03:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-5d+04:00:00", + "at": "anchor-5d+04:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-5d+05:00:00", + "at": "anchor-5d+05:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-5d+06:00:00", + "at": "anchor-5d+06:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-5d+07:00:00", + "at": "anchor-5d+07:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-5d+08:00:00", + "at": "anchor-5d+08:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-5d+09:00:00", + "at": "anchor-5d+09:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-5d+10:00:00", + "at": "anchor-5d+10:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-5d+11:00:00", + "at": "anchor-5d+11:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-5d+12:00:00", + "at": "anchor-5d+12:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-5d+13:00:00", + "at": "anchor-5d+13:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-5d+14:00:00", + "at": "anchor-5d+14:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-5d+15:00:00", + "at": "anchor-5d+15:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-5d+16:00:00", + "at": "anchor-5d+16:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-5d+17:00:00", + "at": "anchor-5d+17:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-5d+18:00:00", + "at": "anchor-5d+18:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-5d+19:00:00", + "at": "anchor-5d+19:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-5d+20:00:00", + "at": "anchor-5d+20:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-5d+21:00:00", + "at": "anchor-5d+21:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-5d+22:00:00", + "at": "anchor-5d+22:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-5d+23:00:00", + "at": "anchor-5d+23:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-4d+00:00:00", + "at": "anchor-4d+00:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-4d+01:00:00", + "at": "anchor-4d+01:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-4d+02:00:00", + "at": "anchor-4d+02:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-4d+03:00:00", + "at": "anchor-4d+03:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-4d+04:00:00", + "at": "anchor-4d+04:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-4d+05:00:00", + "at": "anchor-4d+05:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-4d+06:00:00", + "at": "anchor-4d+06:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-4d+07:00:00", + "at": "anchor-4d+07:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-4d+08:00:00", + "at": "anchor-4d+08:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-4d+09:00:00", + "at": "anchor-4d+09:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-4d+10:00:00", + "at": "anchor-4d+10:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-4d+11:00:00", + "at": "anchor-4d+11:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-4d+12:00:00", + "at": "anchor-4d+12:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-4d+13:00:00", + "at": "anchor-4d+13:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-4d+14:00:00", + "at": "anchor-4d+14:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-4d+15:00:00", + "at": "anchor-4d+15:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-4d+16:00:00", + "at": "anchor-4d+16:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-4d+17:00:00", + "at": "anchor-4d+17:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-4d+18:00:00", + "at": "anchor-4d+18:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-4d+19:00:00", + "at": "anchor-4d+19:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-4d+20:00:00", + "at": "anchor-4d+20:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-4d+21:00:00", + "at": "anchor-4d+21:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-4d+22:00:00", + "at": "anchor-4d+22:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-4d+23:00:00", + "at": "anchor-4d+23:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-3d+00:00:00", + "at": "anchor-3d+00:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-3d+01:00:00", + "at": "anchor-3d+01:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-3d+02:00:00", + "at": "anchor-3d+02:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-3d+03:00:00", + "at": "anchor-3d+03:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-3d+04:00:00", + "at": "anchor-3d+04:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-3d+05:00:00", + "at": "anchor-3d+05:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-3d+06:00:00", + "at": "anchor-3d+06:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-3d+07:00:00", + "at": "anchor-3d+07:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-3d+08:00:00", + "at": "anchor-3d+08:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-3d+09:00:00", + "at": "anchor-3d+09:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-3d+10:00:00", + "at": "anchor-3d+10:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-3d+11:00:00", + "at": "anchor-3d+11:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-3d+12:00:00", + "at": "anchor-3d+12:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-3d+13:00:00", + "at": "anchor-3d+13:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-3d+14:00:00", + "at": "anchor-3d+14:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-3d+15:00:00", + "at": "anchor-3d+15:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-3d+16:00:00", + "at": "anchor-3d+16:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-3d+17:00:00", + "at": "anchor-3d+17:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-3d+18:00:00", + "at": "anchor-3d+18:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-3d+19:00:00", + "at": "anchor-3d+19:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-3d+20:00:00", + "at": "anchor-3d+20:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-3d+21:00:00", + "at": "anchor-3d+21:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-3d+22:00:00", + "at": "anchor-3d+22:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-3d+23:00:00", + "at": "anchor-3d+23:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-2d+00:00:00", + "at": "anchor-2d+00:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-2d+01:00:00", + "at": "anchor-2d+01:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-2d+02:00:00", + "at": "anchor-2d+02:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-2d+03:00:00", + "at": "anchor-2d+03:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-2d+04:00:00", + "at": "anchor-2d+04:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-2d+05:00:00", + "at": "anchor-2d+05:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-2d+06:00:00", + "at": "anchor-2d+06:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-2d+07:00:00", + "at": "anchor-2d+07:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-2d+08:00:00", + "at": "anchor-2d+08:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-2d+09:00:00", + "at": "anchor-2d+09:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-2d+10:00:00", + "at": "anchor-2d+10:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-2d+11:00:00", + "at": "anchor-2d+11:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-2d+12:00:00", + "at": "anchor-2d+12:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-2d+13:00:00", + "at": "anchor-2d+13:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-2d+14:00:00", + "at": "anchor-2d+14:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-2d+15:00:00", + "at": "anchor-2d+15:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-2d+16:00:00", + "at": "anchor-2d+16:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-2d+17:00:00", + "at": "anchor-2d+17:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-2d+18:00:00", + "at": "anchor-2d+18:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-2d+19:00:00", + "at": "anchor-2d+19:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-2d+20:00:00", + "at": "anchor-2d+20:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-2d+21:00:00", + "at": "anchor-2d+21:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-2d+22:00:00", + "at": "anchor-2d+22:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-2d+23:00:00", + "at": "anchor-2d+23:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-1d+00:00:00", + "at": "anchor-1d+00:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-1d+01:00:00", + "at": "anchor-1d+01:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-1d+02:00:00", + "at": "anchor-1d+02:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-1d+03:00:00", + "at": "anchor-1d+03:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-1d+04:00:00", + "at": "anchor-1d+04:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-1d+05:00:00", + "at": "anchor-1d+05:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-1d+06:00:00", + "at": "anchor-1d+06:00:00", + "value": 47.4, + "secondary": null + }, + { + "id": "hour:anchor-1d+07:00:00", + "at": "anchor-1d+07:00:00", + "value": 48.6, + "secondary": null + }, + { + "id": "hour:anchor-1d+08:00:00", + "at": "anchor-1d+08:00:00", + "value": 47.53, + "secondary": null + }, + { + "id": "hour:anchor-1d+09:00:00", + "at": "anchor-1d+09:00:00", + "value": 48.45, + "secondary": null + }, + { + "id": "hour:anchor-1d+10:00:00", + "at": "anchor-1d+10:00:00", + "value": 47.67, + "secondary": null + }, + { + "id": "hour:anchor-1d+11:00:00", + "at": "anchor-1d+11:00:00", + "value": 48.3, + "secondary": null + }, + { + "id": "hour:anchor-1d+12:00:00", + "at": "anchor-1d+12:00:00", + "value": 47.8, + "secondary": null + }, + { + "id": "hour:anchor-1d+13:00:00", + "at": "anchor-1d+13:00:00", + "value": 48.15, + "secondary": null + }, + { + "id": "hour:anchor-1d+14:00:00", + "at": "anchor-1d+14:00:00", + "value": 47.93, + "secondary": null + }, + { + "id": "hour:anchor-1d+15:00:00", + "at": "anchor-1d+15:00:00", + "value": 48, + "secondary": null + }, + { + "id": "hour:anchor-1d+16:00:00", + "at": "anchor-1d+16:00:00", + "value": 48.07, + "secondary": null + }, + { + "id": "hour:anchor-1d+17:00:00", + "at": "anchor-1d+17:00:00", + "value": 47.85, + "secondary": null + }, + { + "id": "hour:anchor-1d+18:00:00", + "at": "anchor-1d+18:00:00", + "value": 48.2, + "secondary": null + }, + { + "id": "hour:anchor-1d+19:00:00", + "at": "anchor-1d+19:00:00", + "value": 47.7, + "secondary": null + }, + { + "id": "hour:anchor-1d+20:00:00", + "at": "anchor-1d+20:00:00", + "value": 48.33, + "secondary": null + }, + { + "id": "hour:anchor-1d+21:00:00", + "at": "anchor-1d+21:00:00", + "value": 47.55, + "secondary": null + }, + { + "id": "hour:anchor-1d+22:00:00", + "at": "anchor-1d+22:00:00", + "value": 48.47, + "secondary": null + }, + { + "id": "hour:anchor-1d+23:00:00", + "at": "anchor-1d+23:00:00", + "value": 47.4, + "secondary": null + } + ], + "direction": { + "direction": "up-good", + "targetPosition": null, + "basis": "metric_default" + }, + "change": { + "from": 48.07, + "to": 47.98, + "delta": -0.09, + "sentiment": "negative" + }, + "referenceLines": [], + "stats": { + "mean": 48, + "min": 40, + "max": 56, + "stdDev": 4.9, + "count": 10200 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "heartRateVariability", + "days": 30, + "count": 170 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.09-invalid.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.09-invalid.json.snap new file mode 100644 index 000000000..4287a32ba --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.09-invalid.json.snap @@ -0,0 +1,56 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Validation failed", + "details": { + "issues": [ + { + "path": "kind", + "code": "invalid_value", + "message": "Invalid option: expected one of \"weight\"|\"bloodPressure\"|\"pulse\"|\"bodyFat\"|\"glucose\"|\"sleep\"|\"steps\"|\"totalBodyWater\"|\"boneMass\"|\"oxygenSaturation\"|\"restingHeartRate\"|\"heartRateVariability\"|\"vo2Max\"" + }, + { + "path": "days", + "code": "too_small", + "message": "Too small: expected number to be >=1" + } + ] + }, + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "measurements.series.validation-failed" + }, + "meta": { + "issue_count": 2, + "received_keys": [ + "kind", + "days" + ], + "received_shape_excerpt": "{\"kind\":\"temperature\",\"days\":\"0\"}", + "zod_issues": [ + { + "path": "kind", + "code": "invalid_value", + "message": "Invalid option: expected one of \"weight\"|\"bloodPressure\"|\"pulse\"|\"bodyFat\"|\"glucose\"|\"sleep\"|\"steps\"|\"totalBodyWater\"|\"boneMass\"|\"oxygenSaturation\"|\"restingHeartRate\"|\"heartRateVariability\"|\"vo2Max\"" + }, + { + "path": "days", + "code": "too_small", + "message": "Too small: expected number to be >=1" + } + ], + "rejected_attempt": "measurements.series.validation-failed" + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/__snapshots__/measurement-series.10-delegated.json.snap b/tests/integration/routes/parity/__snapshots__/measurement-series.10-delegated.json.snap new file mode 100644 index 000000000..200db4ea9 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/measurement-series.10-delegated.json.snap @@ -0,0 +1,115 @@ +{ + "status": 200, + "body": { + "data": { + "kind": "bloodPressure", + "unit": "mmHg", + "points": [ + { + "id": "", + "at": "anchor-6d+07:01:00", + "value": 127, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-5d+07:01:00", + "value": 126, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-4d+07:01:00", + "value": 130, + "secondary": 82 + }, + { + "id": "", + "at": "anchor-3d+07:01:00", + "value": 129, + "secondary": 81 + }, + { + "id": "", + "at": "anchor-2d+07:01:00", + "value": 128, + "secondary": 83 + }, + { + "id": "", + "at": "anchor-1d+07:01:00", + "value": 127, + "secondary": 82 + } + ], + "direction": { + "direction": "hold", + "targetPosition": "inside", + "basis": "profile_default" + }, + "change": { + "from": 127, + "to": 127, + "delta": 0, + "sentiment": "positive" + }, + "referenceLines": [ + { + "value": 120, + "kind": "lower", + "source": "personal", + "appliesTo": "value", + "labelKey": "series.reference.targetLower", + "label": "Dein Ziel, Untergrenze" + }, + { + "value": 129, + "kind": "upper", + "source": "personal", + "appliesTo": "value", + "labelKey": "series.reference.targetUpper", + "label": "Dein Ziel, Obergrenze" + }, + { + "value": 70, + "kind": "lower", + "source": "personal", + "appliesTo": "secondary", + "labelKey": "series.reference.targetLower", + "label": "Dein Ziel, Untergrenze" + }, + { + "value": 79, + "kind": "upper", + "source": "personal", + "appliesTo": "secondary", + "labelKey": "series.reference.targetUpper", + "label": "Dein Ziel, Obergrenze" + } + ], + "stats": { + "mean": 127.83, + "min": 126, + "max": 130, + "stdDev": 1.34, + "count": 6 + } + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "measurements.series" + }, + "meta": { + "kind": "bloodPressure", + "days": 7, + "count": 6 + }, + "warnings": null, + "error": null + } + ] +} diff --git a/tests/integration/routes/parity/measurement-series.parity.test.ts b/tests/integration/routes/parity/measurement-series.parity.test.ts new file mode 100644 index 000000000..bf2619e96 --- /dev/null +++ b/tests/integration/routes/parity/measurement-series.parity.test.ts @@ -0,0 +1,308 @@ +/** + * Parity for `GET /api/measurements/series` while its readers move into + * `src/lib/measurements/series/`. + * + * One account holds weight, blood pressure from two sources (with a corrupt + * systolic row), pulse with hourly spreads, a stream of per-minute pulse + * dense enough to be bucketed per hour, glucose in mmol/L preference, sleep + * stages and steps. The walk asks for each kind it reads differently: the + * plain readers, the paired blood pressure, the sleep nights, a dense kind + * inside the raw window, the hour buckets, the day buckets of a long window, + * a refused query, and a delegate reading the record in another language. + * + * The clock is NOT pinned here: the series readers cut their windows in SQL + * against the database's own `now()`, which a pinned `Date` would disagree + * with. The record is laid out relative to the start of the current UTC day + * instead, no reading sits on a window's edge day, and every instant and day + * key in the snapshot is written relative to that start, so the snapshot is + * the same on every day it runs. + */ +import { beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET } from "@/app/api/measurements/series/route"; +import type { Prisma } from "@/generated/prisma/client"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeGrant, makeSession, makeUser } from "../../../kit/factories"; +import { + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { + getPrismaClient, + switchSessionTo, + truncateAllTables, +} from "../../setup"; + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +const prisma = getPrismaClient(); +const DAY = 86_400_000; +/** The start of the current UTC day: the anchor the record is laid out from. */ +const ANCHOR = new Date(Math.floor(Date.now() / DAY) * DAY); +/** + * Days back the daily readings sit. The windows asked for are 7, 14, 30 and + * 365 days; a reading on exactly that many days back would fall in or out of + * the window with the time of day the test runs, so those days are skipped. + */ +const READING_DAYS = Array.from({ length: 20 }, (_, i) => i + 1).filter( + (d) => d !== 7 && d !== 14, +); + +let userId: string; +let owner: RouteAuth; +let delegate: { auth: RouteAuth & { sessionId: string } }; + +function at(daysAgo: number, hour: number, minute = 0): Date { + return new Date( + ANCHOR.getTime() - daysAgo * DAY + (hour * 60 + minute) * 60_000, + ); +} + +const ISO_INSTANT = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g; +const DAY_KEY = /\b\d{4}-\d{2}-\d{2}\b/g; + +/** An instant as days and time from the anchor: `anchor-13d+06:30`. */ +function relativeInstant(iso: string): string { + const ms = Date.parse(iso) - ANCHOR.getTime(); + const days = Math.floor(ms / DAY); + const rest = new Date(ms - days * DAY).toISOString().slice(11, 19); + return `anchor${days >= 0 ? "+" : ""}${days}d+${rest}`; +} + +/** A day key as days from the anchor's day: `day-13`. */ +function relativeDay(key: string): string { + const days = Math.round( + (Date.parse(`${key}T00:00:00Z`) - ANCHOR.getTime()) / DAY, + ); + return `day${days >= 0 ? "+" : ""}${days}`; +} + +function relativeDates(value: unknown): unknown { + if (typeof value === "string") { + return value + .replace(ISO_INSTANT, relativeInstant) + .replace(DAY_KEY, relativeDay); + } + if (Array.isArray(value)) return value.map(relativeDates); + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, relativeDates(v)]), + ); + } + return value; +} + +function seedRows(): Prisma.MeasurementCreateManyInput[] { + const rows: Prisma.MeasurementCreateManyInput[] = []; + const add = (row: Omit) => + rows.push({ userId, ...row }); + for (const d of READING_DAYS) { + add({ + type: "WEIGHT", + value: 80 - d * 0.1, + unit: "kg", + measuredAt: at(d, 6, 30), + source: d % 2 ? "MANUAL" : "WITHINGS", + externalId: d % 2 ? null : `w-${d}`, + }); + for (const [source, offset] of [ + ["MANUAL", 0], + ["WITHINGS", 1], + ] as const) { + add({ + type: "BLOOD_PRESSURE_SYS", + value: 125 + (d % 5) + offset, + unit: "mmHg", + measuredAt: at(d, 7, offset), + source, + externalId: source === "WITHINGS" ? `bp-s-${d}` : null, + }); + add({ + type: "BLOOD_PRESSURE_DIA", + value: 80 + (d % 3) + offset, + unit: "mmHg", + measuredAt: at(d, 7, offset + 2), + source, + externalId: source === "WITHINGS" ? `bp-d-${d}` : null, + }); + } + add({ + type: "BLOOD_GLUCOSE", + value: 95 + d, + unit: "mg/dL", + measuredAt: at(d, 8), + source: "MANUAL", + }); + add({ + type: "ACTIVITY_STEPS", + value: 6_000 + d * 100, + unit: "steps", + measuredAt: at(d, 12), + source: "APPLE_HEALTH", + externalId: `stats:steps:${d}`, + aggregationProvenance: "HEALTHKIT_STATISTICS", + }); + for (const [stage, start, minutes] of [ + ["CORE", 23, 180], + ["DEEP", 2, 60], + ["REM", 3, 90], + ["AWAKE", 4, 10], + ] as const) { + const begin = at(d, start); + if (start === 23) begin.setTime(begin.getTime() - DAY); + add({ + type: "SLEEP_DURATION", + value: minutes, + unit: "min", + measuredAt: new Date(begin.getTime() + minutes * 60_000), + source: "APPLE_HEALTH", + sleepStage: stage, + externalId: `sleep-${d}-${stage}`, + }); + } + for (const hour of [9, 14, 20]) { + add({ + type: "PULSE", + value: 60 + hour, + valueMin: 55 + hour, + valueMax: 70 + hour, + unit: "bpm", + measuredAt: at(d, hour), + source: "APPLE_HEALTH", + externalId: `stats:hr:${d}:${hour}`, + aggregationProvenance: "HEALTHKIT_STATISTICS", + }); + } + } + // A seed-era corrupt systolic the series must never surface. + add({ + type: "BLOOD_PRESSURE_SYS", + value: 0, + unit: "mmHg", + measuredAt: at(2, 9), + source: "MANUAL", + }); + // Two months back, so a long window has more than the raw window holds. + for (let d = 60; d <= 70; d++) { + add({ + type: "BLOOD_GLUCOSE", + value: 110, + unit: "mg/dL", + measuredAt: at(d, 8), + source: "MANUAL", + }); + } + return rows; +} + +/** A per-minute heart-rate stream dense enough to be bucketed per hour. */ +function denseHeartRate(): Prisma.MeasurementCreateManyInput[] { + return Array.from({ length: 10_200 }, (_, i) => ({ + userId, + type: "HEART_RATE_VARIABILITY", + value: 40 + (i % 17), + unit: "ms", + measuredAt: new Date(ANCHOR.getTime() - (i + 1) * 60_000), + source: "APPLE_HEALTH", + externalId: `hrv-${i}`, + })); +} + +async function step( + name: string, + query: Record, + auth: RouteAuth = owner, +): Promise { + wideEventRecorder.take(); + const res = await callRoute(GET, { + method: "GET", + route: "/api/measurements/series", + query, + auth, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `measurement-series.${name}`, + stable( + relativeDates({ + status: res.status, + body: res.body, + events: events.map(eventFacts), + }), + { since: new Date() }, + ), + ); +} + +beforeAll(async () => { + await truncateAllTables(prisma); + const user = await makeUser({ + // UTC, so a daylight-saving change between two runs cannot move a local + // day boundary relative to the anchor. + timezone: "UTC", + glucoseUnit: "mmol/L", + dateOfBirth: new Date("1970-01-01T00:00:00.000Z"), + locale: "en", + }); + userId = user.id; + owner = await makeSession(user); + await prisma.measurement.createMany({ data: seedRows() }); + await prisma.measurement.createMany({ data: denseHeartRate() }); + const reader = await makeUser({ timezone: "UTC", locale: "de" }); + await makeGrant(user, reader); + const session = await makeSession(reader); + delegate = { auth: session }; +}); + +describe("GET /api/measurements/series parity", () => { + it("the plain readers", async () => { + await step("01-weight", { kind: "weight" }); + await step("02-steps", { kind: "steps", days: 14 }); + await step("03-pulse-spread", { kind: "pulse", days: 7 }); + }); + + it("the paired blood pressure and the sleep nights", async () => { + await step("04-blood-pressure", { kind: "bloodPressure" }); + await step("05-sleep", { kind: "sleep", days: 14 }); + }); + + it("glucose raw, and day-bucketed past the raw window", async () => { + await step("06-glucose-raw", { kind: "glucose" }); + await step("07-glucose-long", { kind: "glucose", days: 365 }); + }); + + it("a dense stream bucketed per hour", async () => { + await step("08-hrv-hour-buckets", { + kind: "heartRateVariability", + days: 30, + }); + }); + + it("a refused query", async () => { + await step("09-invalid", { kind: "temperature", days: 0 }); + }); + + it("a delegate reading the record in another language", async () => { + await switchSessionTo(delegate.auth.sessionId, userId); + await step( + "10-delegated", + { kind: "bloodPressure", days: 7 }, + delegate.auth, + ); + await switchSessionTo(delegate.auth.sessionId, null); + }); +});