test(insights): coverage for confidence computation + meter rendering

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) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 01:40:29 +02:00
1 parent 63cfd8e2f7
commit d343ab5dc2
1 file changed
+178
@@ -0,0 +1,178 @@
import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
vi.mock("@/components/charts/health-chart", () => ({
HealthChart: () => <div data-testid="health-chart-mock" />,
}));
vi.mock("@/components/charts/mood-chart", () => ({
MoodChart: () => <div data-testid="mood-chart-mock" />,
}));
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(
<I18nProvider initialLocale="en">
<InsightAdvisorCard title="Test" insight={insight} />
</I18nProvider>,
);
// 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(
<I18nProvider initialLocale="en">
<InsightAdvisorCard title="Test" insight={insight} />
</I18nProvider>,
);
expect(html).toContain('data-confidence-band="draft"');
expect(html).toMatch(/Draft/);
});
});