diff --git a/src/components/insights/__tests__/workout-detail.test.tsx b/src/components/insights/__tests__/workout-detail.test.tsx index 96e0020de..86bc9508e 100644 --- a/src/components/insights/__tests__/workout-detail.test.tsx +++ b/src/components/insights/__tests__/workout-detail.test.tsx @@ -11,6 +11,7 @@ import { WorkoutDetailSplits, WorkoutDetailDayLinks, } from "../workout-detail"; +import { WorkoutInsightCard } from "../workout-detail/insight-slot"; import type { WorkoutDetailPayload } from "@/hooks/use-workouts"; import type { RouteCoordinate } from "@/lib/workouts/route-svg"; @@ -262,3 +263,42 @@ describe("", () => { expect(html).toContain('href="/insights/mood"'); }); }); + +/** + * The Activity Insight card. + * + * Two contracts worth pinning at the render layer: the paragraph is shown + * verbatim, and it is shown as TEXT. Model output rendered as markup is an XSS + * surface, and this project ships no markdown library precisely so that the + * question never arises — a test is what keeps someone from "improving" that. + */ +describe("", () => { + it("renders the paragraph under the card's title", () => { + const html = render( + , + ); + expect(html).toContain("A steady, aerobic-leaning ride."); + expect(html).toContain('data-slot="workout-detail-insight"'); + }); + + it("escapes markup rather than rendering it", () => { + const html = render( + **bold**', + generatedAt: "2026-05-15T07:35:00Z", + }} + />, + ); + // The tag is escaped, not emitted, and the asterisks stay literal — there + // is no markdown renderer in the path and there must not be one. + expect(html).not.toContain(" : null}` and this component never mounts. The - * shape below is what the Phase-2 pg-boss job will write onto the - * workout row — when it lands, the server maps the row to `aiInsight`, - * the union in `use-workouts.ts` widens from `null` to - * `WorkoutActivityInsightData | null`, and the card mounts right under - * the hero header with zero layout rework. Non-diagnostic by - * construction: a descriptive paragraph, device-attributed, never a - * verdict. + * Mounts only when the workout payload carries a paragraph — the page composes + * `{aiInsight ? : null}`, and `null` + * is the common case rather than an error state: a paragraph exists only for a + * session that LANDED while the feature was live, ran over ten minutes, fell + * under the day's cap, and found a provider. Every workout that predates the + * feature, every re-synced one, and every one on a provider-less install + * renders nothing here, permanently. Opening this page never generates one. + * + * Non-diagnostic by construction: a descriptive paragraph, device-attributed, + * compared only to the user's own history, never a verdict and never a training + * prescription (the contract lives in `@/lib/ai/prompts/workout-insight`). + * + * The paragraph renders as React TEXT CHILDREN, deliberately. There is no + * markdown library in this project and adding one is a security decision, not a + * formatting one — model output rendered as markup is an XSS surface. */ export interface WorkoutActivityInsightData { /** The cached, plain-text insight paragraph. Rendered as text children. */