marmar-context / web / context / test / flow.test.ts
flow.test.ts
Raw
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",
    ]);
  });
});