mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
test(insights): cover the adherence-card screen and the hero-line envelope guard
Compliance card: a dose-change imperative and a fabricated risk figure are withheld to the deterministic fallback (no model text persisted, updatedAt stays null), while a clean grounded summary still renders and persists. Hero lines: a well-formed, truncated, or fenced JSON envelope resolves to the deterministic lead/template for both the arrival reaction and the coach nudge; a plain-text line still ships verbatim.
This commit is contained in:
1 parent
0baffe04cd
commit
f251ae13f8
2 files changed
+162
No files matched your search
@@ -26,6 +26,7 @@ vi.mock("@/lib/medication-category", () => ({
|
||||
import { prisma } from "@/lib/db";
|
||||
import { runStatusCompletion } from "@/lib/insights/status-provider";
|
||||
import { getMedicationCategories } from "@/lib/medication-category";
|
||||
import { getNoKeyMedicationComplianceStatusText } from "@/lib/insights/no-key-fallbacks";
|
||||
import { generateMedicationComplianceStatusForUser } from "../medication-compliance-status";
|
||||
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
@@ -218,6 +219,100 @@ describe("generateMedicationComplianceStatusForUser — cache-read skips a stub"
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateMedicationComplianceStatusForUser — outbound screen (v1.32.20)", () => {
|
||||
/**
|
||||
* The medication-adherence card is the highest-leverage surface for the
|
||||
* outbound screen — a dose-change imperative or a fabricated figure lands
|
||||
* exactly here. v1.32.20 routes it through `finalizeStatusSummary`, so it now
|
||||
* WITHHOLDS the same dose / risk / causal violations its seven sibling status
|
||||
* cards do (previously it only withheld unparseable envelopes). WITHHOLD =
|
||||
* serve the deterministic no-key line, persist no model text, `updatedAt`
|
||||
* stays null; the per-medication placeholder rows still render.
|
||||
*/
|
||||
function setupCard(now: Date) {
|
||||
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(prisma.medication.findMany).mockResolvedValue([
|
||||
medFixture(now),
|
||||
] as never);
|
||||
vi.mocked(prisma.medicationIntakeEvent.findMany).mockResolvedValue(
|
||||
[] as never,
|
||||
);
|
||||
vi.mocked(prisma.auditLog.create).mockResolvedValue({
|
||||
createdAt: now,
|
||||
} as never);
|
||||
}
|
||||
|
||||
it("withholds a dose-change imperative instead of rendering the raw prose", async () => {
|
||||
const now = new Date();
|
||||
setupCard(now);
|
||||
stubCompletion('{"summary":"Increase your dose to 10 mg next week."}');
|
||||
|
||||
const result = await generateMedicationComplianceStatusForUser("user-1", {
|
||||
locale: "en",
|
||||
});
|
||||
|
||||
expect(result.summary).toBe(getNoKeyMedicationComplianceStatusText("en"));
|
||||
expect(result.summary).not.toContain("10 mg");
|
||||
expect(result.summary).not.toContain("Increase");
|
||||
expect(result.updatedAt).toBeNull();
|
||||
// The per-medication placeholder rows still surface for the active med.
|
||||
expect(result.medications).toHaveLength(1);
|
||||
// No cache row ever persisted the screened model text.
|
||||
for (const call of vi.mocked(prisma.auditLog.create).mock.calls) {
|
||||
const details = JSON.parse(
|
||||
(call[0] as { data: { details: string } }).data.details,
|
||||
) as { summary?: string };
|
||||
expect(details.summary ?? "").not.toContain("10 mg");
|
||||
}
|
||||
});
|
||||
|
||||
it("withholds a fabricated risk figure instead of rendering the raw prose", async () => {
|
||||
const now = new Date();
|
||||
setupCard(now);
|
||||
stubCompletion(
|
||||
'{"summary":"Your 10-year cardiovascular risk is about 14%."}',
|
||||
);
|
||||
|
||||
const result = await generateMedicationComplianceStatusForUser("user-1", {
|
||||
locale: "en",
|
||||
});
|
||||
|
||||
expect(result.summary).toBe(getNoKeyMedicationComplianceStatusText("en"));
|
||||
expect(result.summary).not.toContain("14%");
|
||||
expect(result.updatedAt).toBeNull();
|
||||
expect(result.medications).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("still renders a clean grounded compliance summary", async () => {
|
||||
const now = new Date();
|
||||
setupCard(now);
|
||||
stubCompletion(
|
||||
'{"summary":"Adherence held steady at 96% across the last 30 days."}',
|
||||
);
|
||||
|
||||
const result = await generateMedicationComplianceStatusForUser("user-1", {
|
||||
locale: "en",
|
||||
});
|
||||
|
||||
expect(result.summary).toBe(
|
||||
"Adherence held steady at 96% across the last 30 days.",
|
||||
);
|
||||
expect(result.updatedAt).not.toBeNull();
|
||||
expect(result.cached).toBe(false);
|
||||
// The clean assessment was persisted (not the negative stub).
|
||||
const persisted = vi
|
||||
.mocked(prisma.auditLog.create)
|
||||
.mock.calls.map(
|
||||
(call) =>
|
||||
JSON.parse(
|
||||
(call[0] as { data: { details: string } }).data.details,
|
||||
) as { summary?: string },
|
||||
)
|
||||
.find((details) => (details.summary ?? "").includes("96%"));
|
||||
expect(persisted).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateMedicationComplianceStatusForUser — token-leak hardening (v1.4.27 F16)", () => {
|
||||
it("strips metric: tokens out of the cached summary", async () => {
|
||||
const now = new Date();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// The sanitisers are pure, but their modules import the db client at load time.
|
||||
// Stub it so the unit under test never reaches for a connection.
|
||||
vi.mock("@/lib/db", () => ({ prisma: {} }));
|
||||
|
||||
import { sanitiseReactionLine } from "@/lib/jobs/reaction-line";
|
||||
import { sanitiseAiBody } from "@/lib/jobs/coach-nudge-ai";
|
||||
|
||||
/**
|
||||
* v1.32.20 — both background hero lines (the arrival reaction and the coach
|
||||
* nudge body) now run the status cards' structured-envelope detection
|
||||
* (`extractAssessmentSummary`). A hero line is one plain sentence; a brace-led
|
||||
* or fenced `{"line":"…"}` reply, or a truncated envelope under the char cap,
|
||||
* is a broken contract and must resolve to the deterministic lead/template
|
||||
* rather than render raw JSON. Genuine prose still ships.
|
||||
*/
|
||||
describe("sanitiseReactionLine — JSON-envelope detection", () => {
|
||||
it("rejects a well-formed JSON envelope so the deterministic lead stands", () => {
|
||||
expect(
|
||||
sanitiseReactionLine(
|
||||
'{"line":"Nice work today, your numbers are holding steady."}',
|
||||
"en",
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a truncated brace-led envelope", () => {
|
||||
expect(sanitiseReactionLine('{"line":"Nice work today', "en")).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a fenced JSON envelope", () => {
|
||||
expect(
|
||||
sanitiseReactionLine('```json\n{"line":"Steady week."}\n```', "en"),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("still renders a plain-text line verbatim", () => {
|
||||
expect(
|
||||
sanitiseReactionLine(
|
||||
"Nice work today, your numbers are holding steady.",
|
||||
"en",
|
||||
),
|
||||
).toBe("Nice work today, your numbers are holding steady.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitiseAiBody — JSON-envelope detection", () => {
|
||||
it("rejects a well-formed JSON envelope so the template stands", () => {
|
||||
expect(
|
||||
sanitiseAiBody(
|
||||
'{"body":"A gentle check-in on your routine this week."}',
|
||||
"en",
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects a truncated brace-led envelope", () => {
|
||||
expect(sanitiseAiBody('{"body":"A gentle check-in', "en")).toBeNull();
|
||||
});
|
||||
|
||||
it("still renders a plain-text body verbatim", () => {
|
||||
expect(
|
||||
sanitiseAiBody("A gentle check-in on your routine this week.", "en"),
|
||||
).toBe("A gentle check-in on your routine this week.");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user