From d343ab5dc248de7f8ee2cd1c5296fa6695afb971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 10 May 2026 01:36:22 +0200 Subject: [PATCH] test(insights): coverage for confidence computation + meter rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end integration test that pins the wrapper-override contract across the full stack: 1. MockAIProvider returns a payload where the model claims confidence=99. 2. generateInsight() parses + overwrites with the deterministic value (n=9, recencyDays=0, ratio=null → 65). 3. InsightAdvisorCard renders the parsed payload through RecommendationCard → ConfidenceMeter. 4. The rendered card shows the SERVER-computed band ("medium" / yellow), NOT the model-claimed 99 ("high" / green). The aria-label announces "Confidence: 65 of 100". A second case pins the small-data shield: with n=1 the wrapper hard-caps at <=15, the meter renders the draft pill (red), and the "Draft" tag is visible in the markup — Apple's quiet-when-unsure affordance flows through end-to-end. Coverage now includes: - Unit: computeConfidence() — 12+3 fixtures (input matrix + monotonicity + integer invariants) (commit 1). - Wrapper: 6 cases pinning the override contract (commit 2). - Component: 17 cases on ConfidenceMeter band rendering, both variants, EN/DE aria-label, clamp behaviour (commit 3). - Card wiring: 8 cases on slot fill, low-confidence caption, feedback-slot left untouched (commit 4). - Integration: 2 cases on full-flow override + draft band (this commit). Co-Authored-By: Claude Opus 4.7 (1M context) --- ...ation-card-confidence-integration.test.tsx | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/components/insights/__tests__/recommendation-card-confidence-integration.test.tsx diff --git a/src/components/insights/__tests__/recommendation-card-confidence-integration.test.tsx b/src/components/insights/__tests__/recommendation-card-confidence-integration.test.tsx new file mode 100644 index 000000000..1c1da0e76 --- /dev/null +++ b/src/components/insights/__tests__/recommendation-card-confidence-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: () =>
, +})); + +vi.mock("@/components/charts/mood-chart", () => ({ + MoodChart: () =>
, +})); + +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"; + +/** + * v1.4.16 phase B5d — full-flow integration. + * + * MockAIProvider returns a payload where the model claims a high + * `confidence: 99`. The wrapper MUST overwrite that with the + * deterministic `computeConfidence()` value, which (with the default + * resolver: n=9, recencyDays=0, ratio=null) yields: + * + * nScore = 10 + 10*log10(9) ≈ 19.54 + * recency = 30 (recencyDays=0) + * signal = 15 (null) + * total ≈ 64.54 → 65 + * + * After generation, we mount InsightAdvisorCard with the parsed payload + * and assert the rendered ConfidenceMeter band corresponds to the + * server-computed value (medium/yellow), NOT the model's claimed 99 + * (which would have rendered the green high band). + */ + +const strictPayloadWithModelConfidence = { + summary: "BP runs slightly above baseline.", + recommendations: [ + { + id: "rec-1", + text: "Discuss home BP log with your physician.", + severity: "important", + // Model claims very high confidence — wrapper overrides. + confidence: 99, + 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", + }, + }, + ], + citations: [ + { + type: "bloodPressure", + timeRange: "last7days", + summary: "avg 138/86 across 9 readings", + }, + ], + warnings: [], +}; + +describe("end-to-end — generateInsight() confidence override flows into the card", () => { + it("renders the deterministic confidence (NOT the model-supplied 99) on the card", async () => { + const provider = new MockAIProvider({ + responses: JSON.stringify(strictPayloadWithModelConfidence), + }); + + const outcome = await generateInsight(provider, { + systemPrompt: "system", + userPrompt: "user", + }); + + const rec = outcome.parsed.recommendations[0]; + // Server-computed value at n=9, recencyDays=0, ratio=null = 65 + expect(rec.confidence).toBe(65); + expect(rec.confidence).not.toBe(99); + + 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, + confidence: r.confidence, + })), + dataQuality: { coverage: "good", gaps: [], confidence: "hoch" }, + disclaimer: "Not a substitute for medical advice.", + }; + + const html = renderToStaticMarkup( + + + , + ); + + // Score 65 → medium / yellow band, NOT high / green. + expect(html).toContain('data-confidence-band="medium"'); + expect(html).not.toContain('data-confidence-band="high"'); + // aria-label must announce the SERVER-computed score, not 99. + expect(html).toContain('aria-label="Confidence: 65 of 100"'); + expect(html).not.toContain("99 of 100"); + }); + + it("falls into the draft band when sample count is too thin (n<3)", async () => { + const tinyN = { + ...strictPayloadWithModelConfidence, + recommendations: [ + { + ...strictPayloadWithModelConfidence.recommendations[0], + // Model still claims 99 — wrapper still discards. + confidence: 99, + metricSource: { + ...strictPayloadWithModelConfidence.recommendations[0] + .metricSource, + n: 1, + }, + }, + ], + }; + const provider = new MockAIProvider({ + responses: JSON.stringify(tinyN), + }); + const outcome = await generateInsight(provider, { + systemPrompt: "s", + userPrompt: "u", + }); + + expect(outcome.parsed.recommendations[0].confidence).toBeLessThanOrEqual(15); + + const insight: InsightResult = { + insightType: "general", + summary: "x", + classification: "gut", + classificationLabel: "Good", + findings: [], + correlations: [], + recommendations: outcome.parsed.recommendations.map((r) => ({ + text: r.text, + rationale: r.rationale, + metricSource: r.metricSource, + severity: r.severity, + id: r.id, + confidence: r.confidence, + })), + dataQuality: { coverage: "good", gaps: [], confidence: "gering" }, + disclaimer: "x", + }; + + const html = renderToStaticMarkup( + + + , + ); + + expect(html).toContain('data-confidence-band="draft"'); + expect(html).toMatch(/Draft/); + }); +});