mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
test(workouts): pin the insight card's text-children rendering
The paragraph is model output. Rendering it as markup would be an XSS surface, and this project ships no markdown library so the question stays closed — the test is what keeps that from being treated as a formatting decision.
This commit is contained in:
1 parent
ac9170945d
commit
69b61770c4
2 files changed
+56
-11
No files matched your search
@@ -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("<WorkoutDetailDayLinks>", () => {
|
||||
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("<WorkoutInsightCard>", () => {
|
||||
it("renders the paragraph under the card's title", () => {
|
||||
const html = render(
|
||||
<WorkoutInsightCard
|
||||
insight={{
|
||||
paragraph: "A steady, aerobic-leaning ride.",
|
||||
generatedAt: "2026-05-15T07:35:00Z",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<WorkoutInsightCard
|
||||
insight={{
|
||||
paragraph: '<img src=x onerror="alert(1)"> **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("<img");
|
||||
expect(html).toContain("<img");
|
||||
expect(html).toContain("**bold**");
|
||||
});
|
||||
});
|
||||
@@ -7,18 +7,23 @@ import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* Reserved Activity-Insight seam (strategic-concept Wave C, bet 5).
|
||||
* The per-workout Activity Insight card.
|
||||
*
|
||||
* NOTHING renders here today: the workout payload's `aiInsight` field is
|
||||
* always `null`, so the page composes `{aiInsight ? <WorkoutInsightCard
|
||||
* insight={aiInsight}/> : 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 ? <WorkoutInsightCard insight={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. */
|
||||
|
||||
Reference in new issue
Block a user