diff --git a/src/app/api/workouts/[id]/route.ts b/src/app/api/workouts/[id]/route.ts index 362f6bef4..f7a7350b9 100644 --- a/src/app/api/workouts/[id]/route.ts +++ b/src/app/api/workouts/[id]/route.ts @@ -259,17 +259,22 @@ export const GET = apiHandler( null, )?.id ?? null; - const route = row.route - ? { - geometry: routeGeometry, - // `compact=1` drops the (up to 20k-entry) timestamp array; the - // SVG needs geometry, not the per-sample timestamps, and the - // splits above are already derived from them server-side. - sampleTimestamps: compact - ? null - : (row.route.sampleTimestamps ?? null), - } - : null; + // A route whose track cannot be read (a sealed value that no longer + // opens, or an old row stored as a JSON null) is reported as no route at + // all: clients decode `route.geometry` as required, so a route object + // with a null geometry would fail the whole workout detail for them. + const route = + row.route && routeGeometry + ? { + geometry: routeGeometry, + // `compact=1` drops the (up to 20k-entry) timestamp array; the + // SVG needs geometry, not the per-sample timestamps, and the + // splits above are already derived from them server-side. + sampleTimestamps: compact + ? null + : (row.route.sampleTimestamps ?? null), + } + : null; // v1.10.0 — route-independent per-workout HR series. Present for // both indoor (no route) and outdoor workouts that shipped a diff --git a/tests/integration/workout-detail-hr-series.test.ts b/tests/integration/workout-detail-hr-series.test.ts index e578ba906..7f3d904be 100644 --- a/tests/integration/workout-detail-hr-series.test.ts +++ b/tests/integration/workout-detail-hr-series.test.ts @@ -23,6 +23,7 @@ * rather than only in a count. */ import { NextRequest } from "next/server"; +import { Prisma } from "@/generated/prisma/client"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { cookieJar, headerJar } from "./mock-next-headers"; @@ -277,3 +278,18 @@ describe("GET /api/workouts/{id} — reconstructed heart-rate curve", () => { expect(response.status).toBe(404); }); }); + +describe("GET /api/workouts/{id} — a route whose track cannot be read", () => { + it("reports no route at all instead of a route with a null geometry", async () => { + const id = await createWorkout(); + // Neither the sealed column nor the legacy readable one holds a track: + // the shape an unreadable or JSON-null row resolves to. + await getPrismaClient().workoutRoute.create({ + data: { workoutId: id, geometry: Prisma.JsonNull }, + }); + const body = (await fetchDetail(id)) as unknown as { + data: { route: unknown }; + }; + expect(body.data.route).toBeNull(); + }); +});