marmar-context / web / context / test / engine-extras.test.ts
engine-extras.test.ts
Raw
// The engine's optional clinician fields (feat/engine-clinician-context) and how Context shows them.
import { describe, expect, it } from "vitest";
import type { CheckRun, EngineCalculatedEgfr, EngineKidneyLimit } from "../src/api/types";
import {
  allergyReactionLine,
  calculatedEgfrLine,
  findingWording,
  kidneyLimitLine,
  notCheckedFor,
  ruleReviewLabel,
  testMedicines,
} from "../src/logic/engine-extras";
import { draftWordingPill } from "../src/logic/pills";
import { SPECIFIC_REFERENCES } from "./fixture/engine-options";

type Finding = CheckRun["assessment"]["contextual"]["findings"][number];
const finding = (extra: Record<string, unknown> = {}): Finding =>
  ({
    id: "f1",
    kind: "allergy",
    tier: "today",
    ruleId: "allergy-penicillin",
    medicines: [{ id: "m1", name: "Amoxicillin" }],
    title: "Call today: Amoxicillin and their Penicillin allergy",
    why: "",
    what_to_do: [],
    watch_for: [],
    numbers: null,
    evidence_label: "",
    source_ids: [],
    questions_for_doctor: [],
    allow_stop: false,
    clinical: "Amoxicillin with documented Penicillin allergy (same class).",
    teach_back: null,
    ...extra,
  }) as Finding;

describe("clinician wording (CHK-5)", () => {
  it("prefers the engine's clinician wording and marks a draft", () => {
    const f = finding({ clinician_title: "Amoxicillin and a recorded Penicillin allergy", clinician_why: "Same class.", clinician_wording_review: "draft" });
    expect(findingWording(f, undefined)).toEqual({ title: "Amoxicillin and a recorded Penicillin allergy", why: "Same class.", draft: true });
  });
  it("falls back to the API's wording, then the engine's clinical line", () => {
    const detail = { clinicianTitle: "From the API", clinicianExplanation: "API why" } as CheckRun["details"][string];
    expect(findingWording(finding(), detail)).toMatchObject({ title: "From the API", why: "API why", draft: false });
    expect(findingWording(finding(), undefined).why).toBe("Amoxicillin with documented Penicillin allergy (same class).");
  });
  it("marks wording nobody signed off: the rule's own note (derived) or the engine's", () => {
    for (const wording of ["derived", "engine"] as const) {
      const detail = { wording, clinicianTitle: "From the API", clinicianExplanation: "The rule's note" } as CheckRun["details"][string];
      expect(findingWording(finding(), detail)).toMatchObject({ why: "The rule's note", draft: true });
    }
    expect(draftWordingPill).toMatchObject({ label: "Not yet signed off for clinicians", tone: "grey" });
  });
});

describe("rule review and the reaction (CHK-6a, CHK-7)", () => {
  it("labels unsigned rules and says nothing for a signed-off one", () => {
    expect(ruleReviewLabel(finding({ rule_review: "not_in_accepted_sign_off" }), true)).toBe("Rule not yet signed off for real patients");
    expect(ruleReviewLabel(finding({ rule_review: "changed_since_sign_off" }), true)).toBe("Rule changed since sign-off");
    expect(ruleReviewLabel(finding({ rule_review: "signed_off" }), false)).toBeNull();
    expect(ruleReviewLabel(finding(), false)).toBe("Rule not yet reviewed");
  });
  it("says how the reaction was read and what the tier was without it", () => {
    const urgent = finding({
      tier: "get_help_now",
      tier_without_reaction: "today",
      allergy_reaction: { allergy: "Penicillin", reaction: null, reaction_kind: null, onset: null, severity: null, year: null, reaction_class: "unknown" },
    });
    expect(allergyReactionLine(urgent)).toBe("Recorded reaction to Penicillin: not recorded, not known, so treated as severe. Without the reaction: Review today.");
    const mild = finding({ tier_without_reaction: "today", allergy_reaction: { allergy: "Penicillin", reaction: "Hives", reaction_kind: null, onset: "Within a day", severity: null, year: 2019, reaction_class: "not_severe" } });
    expect(allergyReactionLine(mild)).toBe("Recorded reaction to Penicillin: hives (Within a day, in 2019), read as not severe.");
    expect(allergyReactionLine(finding())).toBeNull();
  });
  it("carries the specific references the engine branch records (FND-2)", () => {
    expect(SPECIFIC_REFERENCES["lab-metformin-egfr-45"][0]).toMatchObject({ sourceId: "smpc-metformin", section: expect.stringContaining("4.2") });
    expect(SPECIFIC_REFERENCES["allergy-penicillin"].map((r) => r.sourceId)).toEqual(["smpc-amoxicillin", "nice-cg183"]);
  });
});

describe("calculated eGFR, kidney limits and monitoring (CHK-10, CHK-11, FND-7)", () => {
  it("labels a calculated eGFR with its inputs, or says why it was not calculated", () => {
    const calculated: EngineCalculatedEgfr = {
      status: "calculated",
      value: 52,
      value_unrounded: 51.8,
      unit: "mL/min/1.73m2",
      label: "calculated",
      formula: "CKD-EPI 2021",
      observed_at: "2026-10-20",
      inputs: { creatinine: { value: 1.2, unit: "mg/dL", value_mg_dl: 1.2, observed_at: "2026-10-20" }, age: 60, sex: "female" },
    };
    expect(calculatedEgfrLine(calculated)).toBe("eGFR 52 mL/min/1.73m2, calculated from creatinine 1.2 mg/dL (20 Oct 2026), age 60, female, with CKD-EPI 2021.");
    expect(calculatedEgfrLine({ status: "not_calculated", reason: "sex_not_recorded", plain: "Sex: needed to calculate eGFR from creatinine (CKD-EPI 2021).", formula: "CKD-EPI 2021" })).toContain("Sex: needed");
    expect(calculatedEgfrLine(undefined)).toBeNull();
  });
  it("names the kidney band and the label's maximum", () => {
    const k: EngineKidneyLimit = { ingredient: "metformin", egfr: 38, egfr_comparator: null, egfr_observed_at: "2026-10-12", egfr_calculated: false, band: "30-44", max_daily_mg: 1000, daily_mg: 1000, source_id: "smpc-metformin", section: "4.2" };
    expect(kidneyLimitLine(k)).toBe("eGFR 38, 12 Oct 2026: band 30-44 mL/min, the label's maximum in this band is 1,000 mg a day; this list adds up to 1,000 mg a day. Section 4.2.");
    expect(kidneyLimitLine({ ...k, egfr: 25, band: "under 30", max_daily_mg: null, egfr_calculated: true })).toContain("(calculated)");
  });
  it("names the medicines a test is needed with", () => {
    const test = { test: "Kidney function (eGFR)", why: "", when: "", source_id: "label-metformin", status: "up_to_date", last_result_at: null, medicines: [{ id: "m1", name: "Metformin" }] } as unknown as CheckRun["assessment"]["contextual"]["tests_to_ask_about"][number];
    expect(testMedicines(test, [])).toEqual(["Metformin"]);
    expect(testMedicines({ ...test, medicines: undefined } as typeof test, ["Lisinopril"])).toEqual(["Lisinopril"]);
  });
});

describe("what was not checked (CHK-3)", () => {
  const check = (contextual: Record<string, unknown>): CheckRun =>
    ({
      assessment: { contextual: { findings: [], ...contextual } },
      notChecked: { productsWithoutRules: ["agbo"], unconfirmedFacts: 1, rulesAwaitingReview: 8, checkInAnswers: true, other: [] },
      versions: { engine: "2.2.2", rules: "2026-10-15", rulesReviewed: false },
    }) as unknown as CheckRun;
  it("uses the engine's coverage lines when it gives them", () => {
    const lines = notCheckedFor(check({ coverage: { lines: ["Agbo: Recorded, not checked: no rules for this product yet.", "Answers from check-ins are recorded as notes; no rule reads them."] } }));
    expect(lines).toEqual(["Agbo: Recorded, not checked: no rules for this product yet.", "Answers from check-ins are recorded as notes; no rule reads them."]);
  });
  it("falls back to the API's statement and lists rules skipped as unsigned", () => {
    const lines = notCheckedFor(check({ signed_off_only: { accepted_sign_offs: [], skipped: [{ rule_id: "lab-potassium-raised", kind: "table", review: "not_in_accepted_sign_off", medicines: [{ id: "m", name: "Lisinopril" }] }], plain: "" } }));
    expect(lines[0]).toBe("agbo: recorded, not checked. No rules for this product yet.");
    expect(lines).toContain("Rules waiting for review, not run: lab-potassium-raised (Lisinopril).");
    expect(lines.some((l) => /check-ins/.test(l))).toBe(true);
  });
});