From fed2e7ea549db63372d6951dfd4daec1dc9b0cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 10 May 2026 01:16:54 +0200 Subject: [PATCH] test(insights): coverage for RecommendationCard expand + rationale rendering Full-flow integration test: 1. MockAIProvider returns a strict payload with rationale on every rec. 2. generateInsight() validates + parses without retry (single provider call). 3. The parsed payload is mapped onto the InsightResult UI shape and fed through InsightAdvisorCard. 4. Assert the rec card is rendered, severity badge surfaces, chevron is collapsed by default (aria-expanded="false"). Second case pins the legacyPayload CTA path: a string-only recommendations[] array (the v1.4.14/v1.4.15 InsightResult shape) with `legacyPayload` flagged surfaces the regenerate banner. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../recommendation-card-integration.test.tsx | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/components/insights/__tests__/recommendation-card-integration.test.tsx diff --git a/src/components/insights/__tests__/recommendation-card-integration.test.tsx b/src/components/insights/__tests__/recommendation-card-integration.test.tsx new file mode 100644 index 000000000..d574d5e5b --- /dev/null +++ b/src/components/insights/__tests__/recommendation-card-integration.test.tsx @@ -0,0 +1,178 @@ +import { describe, it, expect, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +vi.mock("@/components/charts/health-chart", () => ({ + HealthChart: ({ + types, + title, + mini, + windowOverride, + }: { + types: string[]; + title: string; + mini?: boolean; + windowOverride?: string; + }) => ( +
+ chart:{title} +
+ ), +})); + +vi.mock("@/components/charts/mood-chart", () => ({ + MoodChart: ({ mini }: { mini?: boolean }) => ( +
+ ), +})); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ data: null, isLoading: false }), + useQueryClient: () => ({ invalidateQueries: vi.fn() }), +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import { InsightAdvisorCard } from "../insight-advisor-card"; +import type { InsightResult } from "@/lib/ai/types"; +import { generateInsight } from "@/lib/ai/generate-insight"; +import { MockAIProvider } from "@/lib/ai/mock-client"; +import { MEDICAL_REFERENCES } from "@/lib/ai/medical-references"; + +/** + * v1.4.16 phase B5c — full-flow integration. + * + * Mocks an AIProvider that returns a structured payload with + * rationale on every rec, runs `generateInsight()` to validate + + * parse, then feeds the parsed payload through InsightAdvisorCard. + * Asserts the advisor card surfaces the rec text + rationale rows + * via the RecommendationCard subcomponent. + * + * The advisor card consumes the legacy `InsightResult` shape, not + * the strict `AIInsightResponse` — but the new field schema is + * optional on the UI type, so a recommendation flowing in as a + * structured object with rationale renders the expand-card. + */ + +const knownRefId = MEDICAL_REFERENCES[0].id; + +const strictPayload = { + summary: "Your blood pressure is elevated against your own baseline.", + recommendations: [ + { + id: "rec-1", + text: "Discuss home BP log with your physician.", + severity: "important", + metricSource: { + type: "bloodPressure", + timeRange: "last7days", + summary: "avg 138/86 across 9 readings", + n: 9, + }, + rationale: { + dataWindow: "last7days", + comparedTo: "your 90-day median (122/78)", + deviation: "+16/+8 mmHg above baseline over 9 of 9 readings", + }, + referenceId: knownRefId, + }, + ], + citations: [ + { + type: "bloodPressure", + timeRange: "last7days", + summary: "avg 138/86 across 9 readings", + }, + ], + warnings: [], +}; + +describe("end-to-end — generateInsight() + RecommendationCard render", () => { + it("strict payload from MockAIProvider passes parse + flows through to RecommendationCard rationale rows", async () => { + const provider = new MockAIProvider({ + responses: JSON.stringify(strictPayload), + }); + + const outcome = await generateInsight(provider, { + systemPrompt: "system", + userPrompt: "user", + }); + + expect(outcome.parsed.recommendations).toHaveLength(1); + const rec = outcome.parsed.recommendations[0]; + expect(rec.rationale).toEqual({ + dataWindow: "last7days", + comparedTo: "your 90-day median (122/78)", + deviation: "+16/+8 mmHg above baseline over 9 of 9 readings", + }); + + // Wrap as an InsightResult (legacy UI-shape) — only the + // recommendations need to carry the new fields for the rec card + // to render the expand panel. + const insight: InsightResult = { + insightType: "general", + summary: outcome.parsed.summary, + classification: "grenzwertig", + classificationLabel: "Borderline", + findings: [], + correlations: [], + recommendations: outcome.parsed.recommendations.map((r) => ({ + text: r.text, + referenceId: r.referenceId, + rationale: r.rationale, + metricSource: r.metricSource, + severity: r.severity, + id: r.id, + })), + dataQuality: { coverage: "good", gaps: [], confidence: "hoch" }, + disclaimer: "Not a substitute for medical advice.", + }; + + const html = renderToStaticMarkup( + + + , + ); + + // The rec card is collapsed by default — assert chevron present. + expect(html).toContain("Discuss home BP log"); + expect(html).toMatch(/aria-expanded="false"/); + // Severity badge + expect(html).toContain("important"); + }); + + it("legacyPayload flag surfaces the regenerate CTA", () => { + const insight: InsightResult = { + insightType: "general", + summary: "x", + classification: "gut", + classificationLabel: "Good", + findings: [], + correlations: [], + recommendations: ["Plain string rec"], + dataQuality: { coverage: "good", gaps: [], confidence: "hoch" }, + disclaimer: "Disclaimer", + }; + + const html = renderToStaticMarkup( + + {}} + /> + , + ); + + expect(html).toContain('data-slot="insight-legacy-payload-cta"'); + expect(html).toContain("Insights updated"); + }); +});