diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.delete-01-synced.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-01-synced.json.snap new file mode 100644 index 000000000..0791cfb77 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-01-synced.json.snap @@ -0,0 +1,70 @@ +{ + "status": 409, + "body": { + "data": null, + "error": "Only a workout entered by hand can be deleted here", + "meta": { + "errorCode": "workout.delete.synced_source", + "requestId": "" + } + }, + "events": [ + { + "status": 409, + "action": { + "name": "workout.delete", + "entity_type": "workout", + "entity_id": "" + }, + "meta": { + "outcome": "synced_source", + "source": "APPLE_HEALTH" + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "workouts": [ + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-0", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:walk-1", + "sportType": "walking" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-ride-1", + "sportType": "cycling" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:run-twin", + "sportType": "running" + } + ], + "records": [ + { + "metricSlot": "longest_distance_run", + "value": 3000, + "externalId": "manual:walk-1", + "source": "MANUAL" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.delete-02-missing.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-02-missing.json.snap new file mode 100644 index 000000000..d8364db37 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-02-missing.json.snap @@ -0,0 +1,21 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Workout not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.delete-03-other-account.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-03-other-account.json.snap new file mode 100644 index 000000000..47e99ce41 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-03-other-account.json.snap @@ -0,0 +1,63 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Workout not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "workouts": [ + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-0", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:walk-1", + "sportType": "walking" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-ride-1", + "sportType": "cycling" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:run-twin", + "sportType": "running" + } + ], + "records": [ + { + "metricSlot": "longest_distance_run", + "value": 3000, + "externalId": "manual:walk-1", + "source": "MANUAL" + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.delete-04-manual.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-04-manual.json.snap new file mode 100644 index 000000000..1d47c0edf --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.delete-04-manual.json.snap @@ -0,0 +1,65 @@ +{ + "status": 200, + "body": { + "data": { + "deleted": true + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workout.delete", + "entity_type": "workout", + "entity_id": "" + }, + "meta": { + "outcome": "deleted", + "personalRecordsRemoved": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueuePrDetection", + "args": [ + "", + { + "silent": true + } + ] + } + ], + "stored": { + "workouts": [ + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-0", + "sportType": "running" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-ride-1", + "sportType": "cycling" + }, + { + "id": "", + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "sportType": "running" + }, + { + "id": "", + "source": "MANUAL", + "externalId": "manual:run-twin", + "sportType": "running" + } + ], + "records": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap new file mode 100644 index 000000000..b4b299c13 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-01-canonical.json.snap @@ -0,0 +1,182 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "sportType": "running", + "startedAt": "2026-10-14T06:30:00.000Z", + "endedAt": "2026-10-14T06:46:00.000Z", + "dayKey": "2026-10-14", + "durationSec": 960, + "distanceM": 3200, + "activeEnergyKcal": 260, + "avgHr": 146, + "maxHr": 158, + "minHr": null, + "storedAvgHr": 146, + "storedMaxHr": 158, + "stepCount": null, + "elevationM": 6, + "pauseDurationSec": null, + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "metadata": null, + "route": { + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 7.2162, + 51.4818, + 98 + ], + [ + 7.2262, + 51.4868, + 104 + ], + [ + 7.2383, + 51.4929, + 101 + ] + ] + }, + "sampleTimestamps": [ + { + "t": "2026-10-14T06:30:00.000Z", + "hr": 120 + }, + { + "t": "2026-10-14T06:38:00.000Z", + "hr": 141 + }, + { + "t": "2026-10-14T06:46:00.000Z", + "hr": 150 + } + ] + }, + "samples": { + "sampleCount": 4, + "samples": [ + { + "t": "2026-10-14T06:30:00.000Z", + "hr": 121 + }, + { + "t": "2026-10-14T06:35:00.000Z", + "hr": 142 + }, + { + "t": "2026-10-14T06:40:00.000Z", + "hr": 151 + }, + { + "t": "2026-10-14T06:45:00.000Z", + "hr": 147 + } + ] + }, + "hrSeries": { + "source": "workout_series", + "bucketSec": 5, + "points": [ + { + "tSec": 0, + "mean": 121, + "min": 121, + "max": 121 + }, + { + "tSec": 300, + "mean": 142, + "min": 142, + "max": 142 + }, + { + "tSec": 600, + "mean": 151, + "min": 151, + "max": 151 + }, + { + "tSec": 900, + "mean": 147, + "min": 147, + "max": 147 + } + ], + "envelope": false + }, + "zones": { + "model": "tanaka", + "hrMax": 176, + "zones": [ + { + "zone": 1, + "lowBpm": 88, + "highBpm": 106, + "seconds": 0 + }, + { + "zone": 2, + "lowBpm": 106, + "highBpm": 123, + "seconds": 5 + }, + { + "zone": 3, + "lowBpm": 123, + "highBpm": 141, + "seconds": 0 + }, + { + "zone": 4, + "lowBpm": 141, + "highBpm": 158, + "seconds": 15 + }, + { + "zone": 5, + "lowBpm": 158, + "highBpm": null, + "seconds": 0 + } + ] + }, + "splits": null, + "sportContext": { + "count": 1, + "avgDurationSec": 1800, + "avgDistanceM": 5000, + "avgAvgHr": 140 + }, + "aiInsight": null, + "ai": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "canonicalId": "", + "previousWorkoutId": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workouts.detail.hr_series" + }, + "meta": { + "workoutId": "", + "source": "workout_series", + "points": 4 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap new file mode 100644 index 000000000..d1510e4c2 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-02-compact.json.snap @@ -0,0 +1,152 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "sportType": "running", + "startedAt": "2026-10-14T06:30:00.000Z", + "endedAt": "2026-10-14T06:46:00.000Z", + "dayKey": "2026-10-14", + "durationSec": 960, + "distanceM": 3200, + "activeEnergyKcal": 260, + "avgHr": 146, + "maxHr": 158, + "minHr": null, + "storedAvgHr": 146, + "storedMaxHr": 158, + "stepCount": null, + "elevationM": 6, + "pauseDurationSec": null, + "source": "APPLE_HEALTH", + "externalId": "hk-run-1", + "metadata": null, + "route": { + "geometry": { + "type": "LineString", + "coordinates": [ + [ + 7.2162, + 51.4818, + 98 + ], + [ + 7.2262, + 51.4868, + 104 + ], + [ + 7.2383, + 51.4929, + 101 + ] + ] + }, + "sampleTimestamps": null + }, + "samples": { + "sampleCount": 4, + "samples": null + }, + "hrSeries": { + "source": "workout_series", + "bucketSec": 5, + "points": [ + { + "tSec": 0, + "mean": 121, + "min": 121, + "max": 121 + }, + { + "tSec": 300, + "mean": 142, + "min": 142, + "max": 142 + }, + { + "tSec": 600, + "mean": 151, + "min": 151, + "max": 151 + }, + { + "tSec": 900, + "mean": 147, + "min": 147, + "max": 147 + } + ], + "envelope": false + }, + "zones": { + "model": "tanaka", + "hrMax": 176, + "zones": [ + { + "zone": 1, + "lowBpm": 88, + "highBpm": 106, + "seconds": 0 + }, + { + "zone": 2, + "lowBpm": 106, + "highBpm": 123, + "seconds": 5 + }, + { + "zone": 3, + "lowBpm": 123, + "highBpm": 141, + "seconds": 0 + }, + { + "zone": 4, + "lowBpm": 141, + "highBpm": 158, + "seconds": 15 + }, + { + "zone": 5, + "lowBpm": 158, + "highBpm": null, + "seconds": 0 + } + ] + }, + "splits": null, + "sportContext": { + "count": 1, + "avgDurationSec": 1800, + "avgDistanceM": 5000, + "avgAvgHr": 140 + }, + "aiInsight": null, + "ai": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "canonicalId": "", + "previousWorkoutId": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workouts.detail.hr_series" + }, + "meta": { + "workoutId": "", + "source": "workout_series", + "points": 4 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-03-hidden-twin.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-03-hidden-twin.json.snap new file mode 100644 index 000000000..f7e3fe114 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-03-hidden-twin.json.snap @@ -0,0 +1,62 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "sportType": "running", + "startedAt": "2026-10-14T06:30:20.000Z", + "endedAt": "2026-10-14T06:46:20.000Z", + "dayKey": "2026-10-14", + "durationSec": 960, + "distanceM": 3150, + "activeEnergyKcal": null, + "avgHr": 146, + "maxHr": 158, + "minHr": null, + "storedAvgHr": null, + "storedMaxHr": null, + "stepCount": null, + "elevationM": null, + "pauseDurationSec": null, + "source": "MANUAL", + "externalId": "manual:run-twin", + "metadata": null, + "route": null, + "samples": null, + "hrSeries": null, + "zones": null, + "splits": null, + "sportContext": { + "count": 1, + "avgDurationSec": 1800, + "avgDistanceM": 5000, + "avgAvgHr": 140 + }, + "aiInsight": null, + "ai": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "canonicalId": "", + "previousWorkoutId": "" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workouts.detail.hr_series" + }, + "meta": { + "workoutId": "", + "source": "none", + "points": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-04-ride.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-04-ride.json.snap new file mode 100644 index 000000000..32fbc0f48 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-04-ride.json.snap @@ -0,0 +1,57 @@ +{ + "status": 200, + "body": { + "data": { + "id": "", + "sportType": "cycling", + "startedAt": "2026-10-12T16:00:00.000Z", + "endedAt": "2026-10-12T17:00:00.000Z", + "dayKey": "2026-10-12", + "durationSec": 3600, + "distanceM": 24000, + "activeEnergyKcal": null, + "avgHr": null, + "maxHr": null, + "minHr": null, + "storedAvgHr": null, + "storedMaxHr": null, + "stepCount": null, + "elevationM": null, + "pauseDurationSec": null, + "source": "APPLE_HEALTH", + "externalId": "hk-ride-1", + "metadata": null, + "route": null, + "samples": null, + "hrSeries": null, + "zones": null, + "splits": null, + "sportContext": null, + "aiInsight": null, + "ai": { + "available": false, + "reason": "no_provider", + "onDeviceAllowed": true + }, + "canonicalId": "", + "previousWorkoutId": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "workouts.detail.hr_series" + }, + "meta": { + "workoutId": "", + "source": "none", + "points": 0 + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-05-other-account.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-05-other-account.json.snap new file mode 100644 index 000000000..6ece14ae3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-05-other-account.json.snap @@ -0,0 +1,25 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Workout not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": { + "name": "workouts.detail" + }, + "meta": { + "workoutId": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-06-missing.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-06-missing.json.snap new file mode 100644 index 000000000..550f1d9d3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-06-missing.json.snap @@ -0,0 +1,25 @@ +{ + "status": 404, + "body": { + "data": null, + "error": "Workout not found", + "meta": { + "errorCode": "http.not_found", + "requestId": "" + } + }, + "events": [ + { + "status": 404, + "action": { + "name": "workouts.detail" + }, + "meta": { + "workoutId": "cmissingworkout000000000" + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/__snapshots__/workout-detail.get-07-module-off.json.snap b/tests/integration/routes/parity/__snapshots__/workout-detail.get-07-module-off.json.snap new file mode 100644 index 000000000..ad700627a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/workout-detail.get-07-module-off.json.snap @@ -0,0 +1,26 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "Module \"workouts\" is not enabled", + "meta": { + "errorCode": "module.disabled", + "module": "workouts", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": { + "name": "workouts.detail" + }, + "meta": { + "workoutId": "" + }, + "warnings": null, + "error": null + } + ], + "effects": [] +} diff --git a/tests/integration/routes/parity/workout-detail.parity.test.ts b/tests/integration/routes/parity/workout-detail.parity.test.ts new file mode 100644 index 000000000..3b5e8debf --- /dev/null +++ b/tests/integration/routes/parity/workout-detail.parity.test.ts @@ -0,0 +1,311 @@ +/** + * Parity for `GET` and `DELETE /api/workouts/{id}` while their logic moves + * into `src/lib/workouts/`. + * + * Workouts land through the real batch ingest (a HealthKit run with its + * heart-rate series and a kept track, the same run entered by hand, an + * earlier run, a ride), then the detail read walks the cases it tells apart: + * the canonical row, its hidden twin, the compact body, another account's + * row, and the module switched off. The delete walk covers a hand-entered + * workout with the record it set, a synced workout and a missing one. Each + * snapshot holds the response, the wide event, the recorded queue tail and, + * for the deletes, the workouts and records read back. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { DELETE, GET } from "@/app/api/workouts/[id]/route"; +import { POST as postBatch } from "@/app/api/workouts/batch/route"; + +import { callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + stable, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, 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); + }, + }; +}); +vi.mock("@/lib/arrivals/emit-shared", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + emitDataArrival: async (...args: unknown[]) => { + effectRecorder.record("emitDataArrival", args); + }, + }; +}); +vi.mock("@/lib/jobs/pr-detection", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueuePrDetection: async (...args: unknown[]) => { + effectRecorder.record("enqueuePrDetection", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const NOW = new Date("2026-10-15T12:00:00.000Z"); + +let userId: string; +let owner: RouteAuth; +let stranger: RouteAuth; +const ids: Record = {}; + +const SERIES = [ + { t: "2026-10-14T06:30:00.000Z", hr: 121 }, + { t: "2026-10-14T06:35:00.000Z", hr: 142 }, + { t: "2026-10-14T06:40:00.000Z", hr: 151 }, + { t: "2026-10-14T06:45:00.000Z", hr: 147 }, +]; + +const TRACK = { + geometry: { + type: "LineString", + coordinates: [ + [7.2162, 51.4818, 98], + [7.2262, 51.4868, 104], + [7.2383, 51.4929, 101], + ], + }, + sampleTimestamps: [ + { t: "2026-10-14T06:30:00.000Z", hr: 120 }, + { t: "2026-10-14T06:38:00.000Z", hr: 141 }, + { t: "2026-10-14T06:46:00.000Z", hr: 150 }, + ], +}; + +function workout( + externalId: string, + start: string, + minutes: number, + extra: Record = {}, +) { + const startedAt = new Date(start); + return { + sportType: "running", + startedAt: startedAt.toISOString(), + endedAt: new Date(startedAt.getTime() + minutes * 60_000).toISOString(), + source: "APPLE_HEALTH", + externalId, + ...extra, + }; +} + +async function idOf(externalId: string): Promise { + const row = await prisma.workout.findFirstOrThrow({ + where: { userId, externalId }, + select: { id: true }, + }); + return row.id; +} + +async function stored() { + const [workouts, records] = await Promise.all([ + prisma.workout.findMany({ + where: { userId }, + orderBy: [{ startedAt: "asc" }, { source: "asc" }], + select: { id: true, source: true, externalId: true, sportType: true }, + }), + prisma.personalRecord.findMany({ + where: { userId }, + orderBy: { metricSlot: "asc" }, + select: { metricSlot: true, value: true, externalId: true, source: true }, + }), + ]); + return { workouts, records }; +} + +async function step( + name: string, + call: { + method: "GET" | "DELETE"; + id: string; + query?: Record; + auth?: RouteAuth; + readBack?: boolean; + }, +): Promise { + wideEventRecorder.take(); + effectRecorder.take(); + const res = await callRoute(call.method === "GET" ? GET : DELETE, { + method: call.method, + route: "/api/workouts/{id}", + params: { id: call.id }, + query: call.query, + auth: call.auth ?? owner, + }); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + await expectParity( + `workout-detail.${name}`, + stable( + { + status: res.status, + body: res.body, + events: events.map(eventFacts), + effects: effectRecorder.take(), + ...(call.readBack ? { stored: await stored() } : {}), + }, + { since: NOW }, + ), + ); +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const user = await makeUser({ + timezone: "Europe/Berlin", + dateOfBirth: new Date("1980-05-01T00:00:00.000Z"), + storeWorkoutRoutes: true, + }); + userId = user.id; + owner = await makeSession(user); + stranger = await makeSession(await makeUser({ timezone: "UTC" })); + + const res = await callRoute(postBatch, { + method: "POST", + route: "/api/workouts/batch", + auth: owner, + body: { + workouts: [ + workout("hk-run-1", "2026-10-14T06:30:00.000Z", 16, { + totalDistanceM: 3_200, + totalEnergyKcal: 260, + avgHeartRate: 146, + maxHeartRate: 158, + samples: SERIES, + route: TRACK, + }), + workout("hk-run-0", "2026-10-07T06:30:00.000Z", 30, { + totalDistanceM: 5_000, + avgHeartRate: 140, + }), + workout("hk-ride-1", "2026-10-12T16:00:00.000Z", 60, { + sportType: "cycling", + totalDistanceM: 24_000, + }), + workout("manual:walk-1", "2026-10-10T18:00:00.000Z", 40, { + sportType: "walking", + source: "MANUAL", + totalDistanceM: 3_000, + }), + ], + }, + }); + expect(res.status).toBe(200); + // The same run typed in by hand: a twin the canonical picker hides. + const twin = await prisma.workout.create({ + data: { + userId, + sportType: "running", + startedAt: new Date("2026-10-14T06:30:20.000Z"), + endedAt: new Date("2026-10-14T06:46:20.000Z"), + durationSec: 960, + source: "MANUAL", + externalId: "manual:run-twin", + totalDistanceM: 3_150, + }, + }); + ids.twin = twin.id; + ids.run = await idOf("hk-run-1"); + ids.ride = await idOf("hk-ride-1"); + ids.walk = await idOf("manual:walk-1"); + await prisma.personalRecord.create({ + data: { + userId, + metricType: "WALKING_RUNNING_DISTANCE", + metricSlot: "longest_distance_run", + direction: "MAX", + value: 3_000, + unit: "m", + achievedAt: new Date("2026-10-10T18:00:00.000Z"), + source: "MANUAL", + externalId: "manual:walk-1", + }, + }); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/workouts/{id} parity", () => { + it("the detail reads", async () => { + await step("get-01-canonical", { method: "GET", id: ids.run }); + await step("get-02-compact", { + method: "GET", + id: ids.run, + query: { compact: "1" }, + }); + await step("get-03-hidden-twin", { method: "GET", id: ids.twin }); + await step("get-04-ride", { method: "GET", id: ids.ride }); + await step("get-05-other-account", { + method: "GET", + id: ids.run, + auth: stranger, + }); + await step("get-06-missing", { + method: "GET", + id: "cmissingworkout000000000", + }); + }); + + it("the module switched off", async () => { + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: { workouts: false } }, + }); + await step("get-07-module-off", { method: "GET", id: ids.run }); + await prisma.user.update({ + where: { id: userId }, + data: { modulePreferencesJson: {} }, + }); + }); +}); + +describe("DELETE /api/workouts/{id} parity", () => { + it("a synced workout, a missing one, another account's", async () => { + await step("delete-01-synced", { + method: "DELETE", + id: ids.ride, + readBack: true, + }); + await step("delete-02-missing", { + method: "DELETE", + id: "cmissingworkout000000000", + }); + await step("delete-03-other-account", { + method: "DELETE", + id: ids.walk, + auth: stranger, + readBack: true, + }); + }); + + it("a hand-entered workout and the record it set", async () => { + await step("delete-04-manual", { + method: "DELETE", + id: ids.walk, + readBack: true, + }); + }); +});