marmar-context / web / context / test / medication-omissions.test.ts
medication-omissions.test.ts
Raw
import { describe, expect, it } from "vitest";
import { parseCompactDraft } from "../server/compact";
import { assess } from "../server/context";
import type { Draft, Review, Source } from "../src/types";

// Synthetic, deterministic fault injection. These tests measure the review gate,
// not the rate at which any live model omits medication information.
const variants = [
  { name: "and-separated medicines", text: "She takes Warfarin 5 mg and Ibuprofen 200 mg.", ibuprofenRef: 2 },
  { name: "comma-separated medicines", text: "She takes Warfarin 5 mg, Ibuprofen 200 mg.", ibuprofenRef: 2 },
  { name: "semicolon-separated medicines", text: "She takes Warfarin 5 mg; she also takes Ibuprofen 200 mg.", ibuprofenRef: 2 },
  { name: "reversed medicine order", text: "She takes Ibuprofen 200 mg and Warfarin 5 mg.", ibuprofenRef: 2 },
  { name: "medicine on a separate line", text: "She takes Warfarin 5 mg.\nShe also takes Ibuprofen 200 mg.", ibuprofenRef: 3 },
];

function extraction(variant: (typeof variants)[number], complete: boolean) {
  const sources: Source[] = [{
    id: "synthetic-note",
    title: "Synthetic omission regression",
    kind: "Synthetic",
    date: "2026-10-29",
    text: "Patient: Amina Stone\n" + variant.text,
  }];
  const facts: [number, string][] = [[2, "Medication: Warfarin; strength: 5 mg"]];
  if (complete) facts.push([variant.ibuprofenRef, "Medication: Ibuprofen; strength: 200 mg"]);
  const draft = parseCompactDraft({ facts }, sources, "simulated-response", 0);
  const review: Review = Object.fromEntries(draft.facts.map((fact) => [fact.id, "include"]));
  return { sources, draft, review };
}

function check(input: { sources: Source[]; draft: Draft; review: Review }, acknowledged: boolean) {
  return assess(input.sources, input.review, input.draft.fingerprint, acknowledged, input.draft);
}

describe("medication omission safety contract", () => {
  it.each(variants)("complete extraction retains the interaction: $name", (variant) => {
    const input = extraction(variant, true);
    expect(input.draft.facts.filter((fact) => fact.kind === "medicine")).toHaveLength(2);
    expect(input.draft.warnings).toHaveLength(0);
    const finding = check(input, true).contextual.findings.find((item) => item.ruleId === "vka-nsaid");
    expect(finding).toBeDefined();
    expect(finding!.medicines.map((medicine) => medicine.id).sort()).toEqual(
      input.draft.facts.filter((fact) => fact.kind === "medicine").map((fact) => fact.id).sort(),
    );
  });

  it.each(variants)("existing acknowledgement gate remains enforced: $name", (variant) => {
    const input = extraction(variant, false);
    expect(input.draft.facts.filter((fact) => fact.kind === "medicine")).toHaveLength(1);
    expect(input.draft.sourceReviewRequired).toBe(true);
    expect(() => check(input, false)).toThrow("Review the original notes for omissions");
  });

  // Generic acknowledgement cannot resolve a detected unmatched medication.
  it.each(variants)("unresolved omission blocks assessment after acknowledgement: $name", (variant) => {
    const input = extraction(variant, false);
    expect(() => check(input, true)).toThrow(/omission|coverage|unresolved medication/i);
  });
});