import { describe, expect, it } from "vitest";
import { CASES } from "../server/cases";
import { assess, extractLocal } from "../server/context";
import {
checkBlocker,
findingEvidence,
formatDate,
initials,
medicinePlan,
patientProfile,
plural,
recordsFromText,
resultSummary,
} from "../src/flow";
import type { Assessment, Review } from "../src/types";
const includeAll = (draft: ReturnType<typeof extractLocal>): Review =>
Object.fromEntries(
draft.facts.filter((f) => !f.problem).map((f) => [f.id, "include" as const])
);
const run = (sources = CASES[0].sources) => {
const draft = extractLocal(sources);
const review = includeAll(draft);
return {
draft,
review,
assessment: assess(sources, review, draft.fingerprint, true) as Assessment,
};
};
describe("display helpers", () => {
it("formats ISO dates and leaves other text alone", () => {
expect(formatDate("2026-10-12")).toBe("12 Oct 2026");
expect(formatDate("2026-13-01")).toBe("2026-13-01");
expect(formatDate("Yesterday")).toBe("Yesterday");
});
it("pluralises counts", () => {
expect(plural(1, "finding")).toBe("1 finding");
expect(plural(0, "finding")).toBe("0 findings");
});
it("builds initials from the first two names", () => {
expect(initials(" Mariam Adeyemi Ola ")).toBe("MA");
expect(initials("")).toBe("?");
});
});
describe("checkBlocker", () => {
const draft = extractLocal(CASES[0].sources);
it("explains each gate in order", () => {
expect(
checkBlocker({ draft: null, review: {}, acknowledged: false, editing: false })
).toMatch(/Read the records/);
expect(
checkBlocker({ draft, review: {}, acknowledged: false, editing: true })
).toMatch(/record edit/);
expect(
checkBlocker({ draft, review: {}, acknowledged: true, editing: false })
).toBe(`Include or exclude ${draft.facts.length} more facts.`);
});
it("requires a medication and acknowledgement of unmapped notes", () => {
const noMedicine = Object.fromEntries(
draft.facts.map((f) => [f.id, f.kind === "medicine" ? "exclude" : "include"])
) as Review;
expect(
checkBlocker({ draft, review: noMedicine, acknowledged: true, editing: false })
).toMatch(/at least one medication/);
const withNotes = { ...draft, sourceReviewRequired: true };
expect(
checkBlocker({
draft: withNotes,
review: includeAll(draft),
acknowledged: false,
editing: false,
})
).toMatch(/original notes/);
expect(
checkBlocker({
draft: withNotes,
review: includeAll(draft),
acknowledged: true,
editing: false,
})
).toBeNull();
});
it("names conflicting records and clears once one entry is excluded", () => {
const conflicted = extractLocal(CASES[2].sources);
const review = includeAll(conflicted);
expect(
checkBlocker({ draft: conflicted, review, acknowledged: true, editing: false })
).toMatch(/Records disagree about Metformin/);
const older = conflicted.facts.find(
(f) => f.kind === "medicine" && f.detail.startsWith("500")
)!;
expect(
checkBlocker({
draft: conflicted,
review: { ...review, [older.id]: "exclude" },
acknowledged: true,
editing: false,
})
).toBeNull();
});
it("blocks an included fact that still needs clarification", () => {
const fact = { ...draft.facts[0], problem: "Ambiguous strength" };
expect(
checkBlocker({
draft: { ...draft, facts: [fact, ...draft.facts.slice(1)] },
review: Object.fromEntries(draft.facts.map((f) => [f.id, "include"])),
acknowledged: true,
editing: false,
})
).toMatch(/needs clarification/);
});
});
describe("result presentation", () => {
it("leads with the findings context added and links their evidence", () => {
const { assessment } = run();
const summary = resultSummary(assessment);
expect(summary).toMatchObject({ before: 0, after: 1, added: 1 });
expect(summary.headline).toBe(
"Personalized to Ada: 1 finding the medication list alone did not show."
);
const finding = assessment.contextual.findings[0];
expect(findingEvidence(assessment, finding).map((f) => f.label)).toEqual([
"Metformin",
"eGFR 38",
]);
});
it("never presents zero findings as safe", () => {
const { assessment } = run();
const empty = {
...assessment,
addedRuleIds: [],
baseline: { ...assessment.baseline, findings: [] },
contextual: { ...assessment.contextual, findings: [] },
};
const summary = resultSummary(empty);
expect(summary.headline).toBe("No findings from the supported rules.");
expect(summary.subline).toMatch(/does not mean the prescription is safe/);
});
});
describe("recordsFromText", () => {
let n = 0;
const id = () => `id-${++n}`;
it("keeps each upload as a separate titled record and skips empty text", () => {
const { records, error } = recordsFromText(
[
{ name: "01-mariam_clinic-note.txt", text: " Patient: Mariam \n" },
{ name: "Pasted note", text: " " },
],
0,
id
);
expect(error).toBeNull();
expect(records).toHaveLength(1);
expect(records[0]).toMatchObject({
title: "Mariam clinic note",
text: "Patient: Mariam",
kind: "Synthetic note",
});
expect(records[0].id).toMatch(/^source-[a-z0-9-]+$/);
});
it("rejects empty, oversized, binary and over-limit input", () => {
expect(recordsFromText([], 0, id).error).toMatch(/at least one record/);
expect(
recordsFromText([{ name: "big.txt", text: "x".repeat(12001) }], 0, id).error
).toMatch(/longer than 12,000/);
expect(
recordsFromText([{ name: "bin.txt", text: "a\u0000b" }], 0, id).error
).toMatch(/not plain text/);
expect(
recordsFromText([{ name: "a.txt", text: "a" }, { name: "b.txt", text: "b" }], 7, id)
.error
).toMatch(/up to 8 records. Remove 1/);
});
});
describe("personalized check", () => {
const grace = CASES.find((c) => c.id === "personal-factors")!;
const graceRun = () => run(grace.sources);
it("finds what only her age, weight and potassium reveal", () => {
const { assessment } = graceRun();
expect(assessment.baseline.findings).toEqual([]);
expect(assessment.contextual.findings.map((f) => f.ruleId).sort()).toEqual([
"lab-potassium-raised",
"older-long-acting-sulfonylurea",
"paracetamol-lower-maximum",
]);
expect(resultSummary(assessment).headline).toBe(
"Personalized to Grace: 3 findings the medication list alone did not show."
);
});
it("lists used, recorded and missing personal factors honestly", () => {
const profile = patientProfile(graceRun().assessment);
const byId = Object.fromEntries(profile.map((p) => [p.id, p]));
expect(byId.age).toMatchObject({ value: "78 years", status: "used" });
expect(byId.weight).toMatchObject({ value: "46 kg", status: "used" });
expect(byId.sex).toMatchObject({ value: "female", status: "not_assessed" });
expect(byId.potassium).toMatchObject({
value: "5.8 mmol/L",
note: "20 Oct 2026 · Potassium rules",
status: "used",
});
expect(byId.egfr.value).toBe("52 mL/min");
expect(byId.conditions.status).toBe("not_assessed");
const ada = Object.fromEntries(
patientProfile(run().assessment).map((p) => [p.id, p])
);
expect(ada.weight).toMatchObject({ value: null, status: "missing" });
expect(ada.allergies).toMatchObject({ value: "Unknown", status: "missing" });
});
it("gives each medicine its own dose status and findings", () => {
const plan = medicinePlan(graceRun().assessment);
expect(plan.map((m) => m.fact.label)).toEqual([
"Lisinopril",
"Glibenclamide",
"Paracetamol",
]);
expect(plan[0].status).toBe("check_with_pharmacist");
expect(plan[0].findings.map((f) => f.ruleId)).toEqual([
"lab-potassium-raised",
]);
expect(plan[2].findings.map((f) => f.ruleId)).toEqual([
"paracetamol-lower-maximum",
]);
});
});