Files
HealthLog/src/lib/insights/__tests__/status-shared.test.ts
T
Marc-André Bombeck 93c0d616bb fix(insights): withhold malformed status output instead of rendering raw JSON
A truncated or malformed { "summary": "..." } envelope fell through the
status-card parser and rendered raw — braces, the summary property name,
quotes, and literal \n — in the Assessment card (resting pulse and the
other per-metric cards). parseSummaryFromContent returned the raw content
on any JSON parse failure, and a truncated envelope with no closing brace
also fell into that path, so partial structured output reached the reader.

Split the read into extractAssessmentSummary, which classifies a completion
as a parsed summary, genuine prose, or an unparseable structured envelope.
A payload that opened like a JSON object but failed to parse (or parsed
without a usable summary) resolves to the controlled fallback rather than
raw text; plain prose that merely contains a brace is still preserved.
finalizeStatusSummary withholds an unparseable envelope so every status
card serves its deterministic stub, and the two direct callers
(medication compliance, derived-score assessment) get the same guard.

Refs #608
2026-07-24 07:45:02 +02:00

139 lines
4.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
extractAssessmentSummary,
parseSummaryFromContent,
stripJsonFences,
} from "@/lib/insights/status-shared";
/**
* The exact malformed shape reported against the resting-pulse Assessment
* (HealthLog#608): a truncated `{ "summary": "…` — an OPEN brace, the property
* name, quotes and an unclosed string, with no closing `}`. Before the fix this
* fell through the parser and rendered raw (braces, `"summary"`, quotes, literal
* `\n`) in the Assessment card. It must resolve to the controlled fallback.
*/
const TRUNCATED_PULSE_ENVELOPE =
'{ "summary": "Your resting pulse has been running lower than the last check';
/**
* The Anthropic + local providers have no native JSON mode, so a compliant
* model still routinely wraps its `{ "summary": … }` reply in a ```json
* fence or prefixes it with a sentence. These guards pin the fence-strip
* fallback so such replies parse instead of surfacing the raw fenced string
* as the user-facing assessment.
*/
describe("stripJsonFences", () => {
it("strips a ```json fence", () => {
const out = stripJsonFences('```json\n{"summary":"ok"}\n```');
expect(out).toBe('{"summary":"ok"}');
});
it("strips a bare ``` fence", () => {
const out = stripJsonFences('```\n{"summary":"ok"}\n```');
expect(out).toBe('{"summary":"ok"}');
});
it("narrows to the first { … last } when prose surrounds the object", () => {
const out = stripJsonFences('Here is the result: {"summary":"ok"} done.');
expect(out).toBe('{"summary":"ok"}');
});
it("is a no-op on already-clean JSON", () => {
expect(stripJsonFences('{"summary":"ok"}')).toBe('{"summary":"ok"}');
});
it("returns the trimmed input when there is no object body", () => {
expect(stripJsonFences(" plain prose ")).toBe("plain prose");
});
});
describe("parseSummaryFromContent", () => {
it("parses a clean JSON envelope", () => {
expect(parseSummaryFromContent('{"summary":"hello"}')).toBe("hello");
});
it("parses a ```json-fenced envelope (no native JSON mode)", () => {
expect(parseSummaryFromContent('```json\n{"summary":"hello"}\n```')).toBe(
"hello",
);
});
it("parses a sentence-prefixed envelope", () => {
expect(parseSummaryFromContent('Sure:\n{"summary":"hello"} — done')).toBe(
"hello",
);
});
it("falls back to the raw content for bare prose", () => {
expect(parseSummaryFromContent("just prose, no json")).toBe(
"just prose, no json",
);
});
it("returns empty string for a truncated envelope (never the raw JSON)", () => {
const out = parseSummaryFromContent(TRUNCATED_PULSE_ENVELOPE);
expect(out).toBe("");
expect(out).not.toContain('"summary"');
expect(out).not.toContain("{");
});
});
describe("extractAssessmentSummary", () => {
it("classifies a structured object with a valid summary", () => {
const out = extractAssessmentSummary('{"summary":"hello"}');
expect(out).toEqual({ kind: "summary", text: "hello" });
});
it("parses a JSON STRING that carries a top-level summary", () => {
// A provider handed back the envelope as a serialized string. It parses to
// the same summary as an inline object — not treated as prose.
const jsonString = JSON.stringify({
summary: "resting pulse steady at 58",
});
const out = extractAssessmentSummary(jsonString);
expect(out).toEqual({
kind: "summary",
text: "resting pulse steady at 58",
});
});
it("classifies a fenced envelope as summary (no native JSON mode)", () => {
const out = extractAssessmentSummary('```json\n{"summary":"hello"}\n```');
expect(out).toEqual({ kind: "summary", text: "hello" });
});
it("preserves a plain-text reply as prose, verbatim", () => {
const prose =
"Your resting pulse is averaging 58 bpm, in line with baseline.";
expect(extractAssessmentSummary(prose)).toEqual({
kind: "prose",
text: prose,
});
});
it("does not treat a plain sentence containing a brace as JSON", () => {
const prose = "Your resting pulse sits around 58 bpm {your usual range}.";
expect(extractAssessmentSummary(prose)).toEqual({
kind: "prose",
text: prose,
});
});
it("resolves a truncated envelope to unparseable (regression: #608)", () => {
expect(extractAssessmentSummary(TRUNCATED_PULSE_ENVELOPE)).toEqual({
kind: "unparseable",
});
});
it("resolves a summary-less object to unparseable", () => {
expect(extractAssessmentSummary("{}")).toEqual({ kind: "unparseable" });
});
it("resolves an empty-summary object to unparseable", () => {
expect(extractAssessmentSummary('{"summary":""}')).toEqual({
kind: "unparseable",
});
});
});