// The adapters, on JSON shaped like the backend's serializers (src/clinician/** in marmar-cds:
// profile.ts, workspace/patients.ts, sharing/shared-data.ts, workspace/review.ts, check-ins/*).
import { afterEach, describe, expect, it, vi } from "vitest";
import * as A from "../src/api/adapt";
import { checkInFromWire, eventFromWire, planFromWire } from "../src/api/adapt-checkins";
import { clinicianApi } from "../src/api/client";
import type * as W from "../src/api/wire";
import { applyEvent } from "../src/logic/checkin-events";
const clinician: W.Clinician = {
id: "c1", email: "emeka.okafor@example.com", fullName: "Dr Emeka Okafor", title: "Dr", displayName: "Doctor Okafor", pronoun: "he",
profession: "doctor", licenceBody: "MDCN", licenceNumber: "MDCN/R/104233", countryCode: "NG", mobilePhone: null, status: "verified",
verifiedAt: "2026-10-30T08:00:00.000Z", verificationExpiresAt: "2027-10-30T08:00:00.000Z", idleLockMinutes: 30, termsVersion: null,
termsAcceptedAt: null, dpaVersion: null, dpaAcceptedAt: null,
};
const me = (session: Partial<W.Me["session"]>, totp = true): W.Me => ({
clinician: { ...clinician, lapsed: false },
session: { stepUpDone: false, locked: false, idleLockMinutes: 30, startedAt: "2026-10-30T08:00:00.000Z", endsAt: "2026-10-30T20:00:00.000Z", ...session },
secondFactor: { totp },
features: { realPatients: true, invites: true, checkIns: true },
});
describe("session and profile (GET /me)", () => {
it("reads the step-up as enrol, required or done, and the terms", () => {
expect(A.sessionFromMe(me({}, false))).toMatchObject({ stepUp: "enrol", methods: [], verification: "verified", termsAccepted: false, idleLockMinutes: 30, expiresAt: "2026-10-30T20:00:00.000Z" });
expect(A.sessionFromMe(me({ stepUpDone: true })).stepUp).toBe("done");
expect(A.sessionFromMe(me({ stepUpDone: true, locked: true })).stepUp).toBe("required");
expect(A.sessionFromMe({ ...me({}), clinician: { ...clinician, lapsed: true } }).verification).toBe("lapsed");
expect(A.sessionFromMe({ ...me({}), clinician: { ...clinician, status: "suspended", lapsed: false } }).verification).toBe("suspended");
});
it("splits the register name and keeps the spoken name and a pronoun not yet chosen", () => {
const p = A.profileFromWire({ ...clinician, pronoun: null }, { browser: true });
expect(p).toMatchObject({ title: "Dr", firstName: "Emeka", lastName: "Okafor", fullName: "Dr Emeka Okafor", spokenName: "Doctor Okafor", pronoun: null, licensingBody: "MDCN", notifications: { browser: true, email: true } });
expect(A.splitName("Pharm Tunde Bakare", "Pharm")).toEqual({ firstName: "Tunde", lastName: "Bakare" });
});
it("sends only what PATCH /me takes; notifications stay in the browser", () => {
expect(A.meChange({ spokenName: " Doctor Okafor ", pronoun: "he", idleLockMinutes: 15, notifications: { browser: true, email: true } })).toEqual({ displayName: "Doctor Okafor", pronoun: "he", idleLockMinutes: 15 });
expect(A.meChange({ notifications: { browser: false, email: true } })).toBeNull();
});
});
describe("the clinic (GET and PATCH /clinic)", () => {
const clinic: W.Clinic = {
id: "k1", name: "Surulere Family Clinic", addressLine: "12 Example Close", city: "Lagos", region: null, countryCode: "NG", timezone: "Africa/Lagos",
phone: "+234 1 555 0142", openingHours: [{ day: 1, open: "08:00", close: "18:00" }, { day: 7, open: "10:00", close: "12:00" }], alertWatchHours: null,
backupAlertName: null, backupAlertPhone: null, backupAlertEmail: null, version: 3, updatedAt: "2026-10-30T08:00:00.000Z", role: "owner",
emergencyNumbers: [{ number: "112", label: "Nigeria", source: "registry", testDialledAt: null, registryRevision: 4 }],
};
it("turns days 1 to 7 (Monday first) into 0 to 6 (Sunday first), and back", () => {
const view = A.clinicFromWire(clinic);
expect(view.openingHours).toEqual([{ day: 1, open: "08:00", close: "18:00" }, { day: 0, open: "10:00", close: "12:00" }]);
expect(view).toMatchObject({ address: "12 Example Close", alertWatchHours: [], backupContact: null, role: "owner", version: 3 });
expect(A.wireHours(view.openingHours)).toEqual(clinic.openingHours);
});
it("never sends the registry's numbers back as the clinic's own, and sends the version read", () => {
const view = A.clinicFromWire(clinic);
const change = A.clinicChange({ name: view.name, phone: view.phone, emergencyNumbers: view.emergencyNumbers, backupContact: { name: "Bisi", phone: "+234 803 555 0199", email: "" } }, view);
expect(change).toEqual({ expectedVersion: 3, name: "Surulere Family Clinic", phone: "+234 1 555 0142", backupAlertName: "Bisi", backupAlertPhone: "+234 803 555 0199", backupAlertEmail: null });
});
it("carries the backup contact's email both ways, and the time zone and hours only when given", () => {
const view = A.clinicFromWire({ ...clinic, backupAlertEmail: "bisi@clinic.example.test" });
expect(view.backupContact).toEqual({ name: "", phone: "", email: "bisi@clinic.example.test" });
expect(A.clinicChange({ backupContact: { name: "Bisi", phone: "", email: " Bisi@Clinic.Example.Test " } }, view))
.toEqual({ expectedVersion: 3, backupAlertName: "Bisi", backupAlertPhone: null, backupAlertEmail: "bisi@clinic.example.test" });
expect(A.clinicChange({ backupContact: null }, view)).toEqual({ expectedVersion: 3, backupAlertName: null, backupAlertPhone: null, backupAlertEmail: null });
expect(A.clinicChange({ timeZone: "Africa/Kigali", alertWatchHours: [{ day: 0, open: "09:00", close: "13:00" }] }, view))
.toEqual({ expectedVersion: 3, timezone: "Africa/Kigali", alertWatchHours: [{ day: 7, open: "09:00", close: "13:00" }] });
});
});
describe("access requests and invitations", () => {
const input = {
fullName: "Dr Ada Obi", profession: "doctor" as const, licensingBody: "MDCN" as const, licenceNumber: "MDCN/R/123456", country: "NG", clinicName: "Yaba Clinic",
clinicPhone: "+234 1 555 0100", workEmail: "ada@example.com", mobile: "+234 803 555 0100", heardFrom: "patient_invite" as const, heardFromOther: null,
acceptedTermsVersion: "clinician-terms-2026-10", inviteToken: "c_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG",
};
it("names the fields as POST /access-requests does (strict)", () => {
expect(A.accessBody(input)).toEqual({
fullName: "Dr Ada Obi", profession: "doctor", licenceBody: "MDCN", licenceNumber: "MDCN/R/123456", countryCode: "NG", facilityName: "Yaba Clinic",
facilityPhone: "+234 1 555 0100", workEmail: "ada@example.com", mobilePhone: "+234 803 555 0100", heardFrom: "patient_invite", acceptTerms: true,
termsVersion: "clinician-terms-2026-10", inviteCode: input.inviteToken,
});
expect(A.accessBody({ ...input, country: "OTHER" }).countryCode).toBe("ZZ");
expect(A.receiptFromWire({ status: "received", message: "Thank you." }, input)).toEqual({ status: "received", message: "Thank you.", priority: true, countryOpen: true });
});
it("shows they/them for an invitation, which carries no pronoun", () => {
const w: W.Invitation = { id: "i1", patientName: "Mariam Adeyemi", patientAge: 67, withPerson: { name: "Funke", relation: "daughter" }, scopes: ["medicines"],
allowsCheckInCalls: true, withheldSome: false, sentAt: "2026-10-30T08:00:00.000Z", expiresAt: "2026-11-13T08:00:00.000Z", possibleMatch: null };
expect(A.invitationFromWire(w).patientPronoun).toBeNull();
});
});
describe("patients (GET /patients, GET /patients/:id and the shared tile)", () => {
const item: W.PatientItem = {
id: "p1", kind: "connected", clinicId: "k1", displayName: "Mariam Adeyemi", dateOfBirth: "1959-03-04T00:00:00.000Z", birthYear: 1959, ageYears: null,
ageRecordedOn: null, sex: "female", shareId: "s1", lastActivityAt: "2026-10-30T08:00:00.000Z", createdAt: "2026-10-30T08:00:00.000Z", name: "Mariam Adeyemi",
age: 67, attention: { urgent: false, unreadNotes: 0, newSinceLastCheck: true }, lastCheckId: null, lastCheckAt: null,
newInformation: { since: "2026-10-30T08:00:00.000Z", items: ["3 records not checked yet"] },
sharing: { status: "connected", scopes: ["medicines", "doses", "check_ins", "records"], allowsCheckInCalls: true, changedAt: "2026-10-30T08:00:00.000Z" },
medicines: null, doses: null, lastCheckIn: null,
};
const detail: W.PatientDetail = { ...item, firstName: "Mariam", dateOfBirthIso: "1959-03-04", identityOnFile: "date_of_birth", timeZone: "Africa/Lagos",
hasOwnAccount: false, phone: null, pronoun: null, preferredTitle: null, activeCheckInId: null };
const tile: W.SharedSummary = { asOf: "2026-10-30T09:00:00.000Z", sharing: { status: "connected", sharedBy: ["Mariam", "Funke"], scopes: ["medicines", "doses"], allowsCheckInCalls: true,
withheldSome: true, changedAt: "2026-10-30T08:00:00.000Z" }, medicines: { count: 3 }, doses: { taken: 12, expected: 14, onTime: 11 }, lastCheckIn: null, phone: "+234 803 555 0147" };
const plan = {
eligibility: { ok: true, code: null, message: null }, limits: { usedToday: 0, usedThisWeek: 1, maxPerDay: 1, maxPerWeek: 3, nextPossibleAt: null },
window: { patientTimeZone: "Africa/Lagos", clinicTimeZone: "Africa/Lagos", quietHours: null, blockedTimes: [{ day: 7, start: "10:00", end: "12:00" }], defaultExpiryHours: 24, maxExpiryHours: 72 },
} as unknown as W.PlanTemplate;
it("reads the list's sharing without names, and the item's counts as not shared on the list", () => {
const s = A.summaryFromWire(item);
expect(s).toMatchObject({ name: "Mariam Adeyemi", age: 67, pronoun: null, medicines: null, sharing: { sharedBy: [], withheldSome: false, allowsCheckInCalls: true } });
});
it("fills a connected patient's page from the tile, the plan and this clinician's check-ins", () => {
const d = A.detailFromWire(detail, { tile, plan, checkIns: { checkIns: [{ id: "ci1", status: "later", statusAt: "", requestedAt: "", urgent: false, noteCount: 0, unreadNotes: 0, summaryReady: false }] } });
expect(d).toMatchObject({
dateOfBirth: "1959-03-04", sex: "female", medicines: { count: 3 }, doses: { taken: 12, expected: 14, onTime: 11 }, phone: "+234 803 555 0147",
hasOwnAccount: true, quietHours: { start: "00:00", end: "00:00" }, blockedTimes: [{ day: 0, start: "10:00", end: "12:00", label: "" }],
checkInLimits: { usedThisWeek: 1 }, activeCheckInId: "ci1", sharing: { sharedBy: ["Mariam", "Funke"], withheldSome: true },
});
expect(A.detailFromWire(detail, { tile, plan: { ...plan, eligibility: { ok: false, code: "no_own_account", message: "x" } } }).hasOwnAccount).toBe(false);
expect(A.detailFromWire({ ...detail, kind: "clinic_added", sharing: null }).hasOwnAccount).toBe(false);
});
it("adds a clinic patient with displayName and the PAT-4 attestation, a date of birth or an age", () => {
expect(A.newPatientBody({ name: " Tunde Bakare ", dateOfBirth: null, age: 54, sex: null, attestation: { careRelationship: true, patientAgreed: true } })).toEqual({
displayName: "Tunde Bakare", ageYears: 54, attestation: { careRelationship: true, patientAgreement: true, version: "2026-10-30" },
});
});
});
describe("reading and review", () => {
const wire = {
id: "e1", patientId: "p1", revision: 2, createdAt: "2026-10-30T08:00:00.000Z", facts: [], unparsed: [], fingerprint: "f".repeat(64), mode: "local", model: null, durationMs: 1,
records: [{ recordId: "r1", status: "held_back", error: null, inReading: true }], decisions: { a: "include" }, coverageReview: { m1: { fingerprint: "f".repeat(64), choice: "not_current", reason: "", confirmedBy: "Dr Emeka Okafor" } },
acknowledgedOriginals: true, confirmedHardToRead: ["a"], stale: false, recordsNotReady: [{ recordId: "r1", title: "Ikeja discharge", status: "held_back" }],
} as unknown as W.Extraction;
it("titles each record from the records list, the profile included", () => {
const ex = A.extractionFromWire(wire, A.titlesOf("p1", [{ id: "r1", title: "Ikeja discharge" }]));
expect(ex.records).toEqual([{ recordId: "r1", title: "Ikeja discharge", status: "held_back", error: null }]);
expect(ex.titles["profile-p1"]).toBe("Patient profile");
expect(ex.coverageReview.m1.choice).toBe("not_current");
expect(ex.recordsNotReady).toHaveLength(1);
});
it("sends the choice, drops empty reasons and confirms only included Hard to read facts", () => {
const body = A.decisionsBody({
revision: 2, decisions: { a: "include", b: "exclude" }, acknowledgedOriginals: true, confirmedHardToRead: ["a", "b"],
coverageReview: { m1: { fingerprint: "f", choice: "not_current", reason: " ", confirmedBy: "Dr X" }, m2: { fingerprint: "f", reason: "Old prescription, stopped in 2024", confirmedBy: "Dr X" } },
});
expect(body).toEqual({ revision: 2, decisions: { a: "include", b: "exclude" }, acknowledgedOriginals: true, confirmedHardToRead: ["a"],
coverageReview: { m1: { choice: "not_current", fingerprint: "f" }, m2: { reason: "Old prescription, stopped in 2024", fingerprint: "f" } } });
});
});
describe("check-ins", () => {
it("reads the plan's window in Context's days and fills a clinic-added patient's", () => {
const wire = { eligibility: { ok: false, code: "clinic_added", message: "Invite Tunde to MARMAR Health to use check-ins." }, limits: null,
window: { patientTimeZone: null, clinicTimeZone: "Africa/Lagos", quietHours: { start: "22:00", end: "07:00" }, defaultExpiryHours: 24, maxExpiryHours: 72 } } as unknown as W.PlanTemplate;
const plan = planFromWire(wire);
expect(plan.window).toMatchObject({ patientTimeZone: "Africa/Lagos", clinicHours: [], known: false, dayStart: "08:00", dayEnd: "20:00" });
expect(plan.limits.maxPerWeek).toBe(3);
});
it("reads a check-in without linked tiers or the script, and folds the alert event", () => {
const wire: W.CheckIn = {
id: "ci1", patientId: "p1", patientName: "Mariam Adeyemi", requestedBy: "Dr Emeka Okafor", requestedAt: "2026-10-30T10:00:00.000Z",
plan: { questions: [], closingIds: ["sent"], window: { kind: "now" }, expiresAt: "2026-10-31T10:00:00.000Z", linkedFindingIds: [], readToday: false },
script: { opening: "Good afternoon.", identity: [], consent: "", feeling: "", alwaysLast: "", closing: [], protocolVersion: "yci-protocol-2026-10-30-draft" },
status: "sent", statusAt: "2026-10-30T10:00:00.000Z", history: [], window: { start: "2026-10-30T10:00:00.000Z", end: "2026-10-31T10:00:00.000Z" }, later: null,
urgent: false, urgentAcknowledgedAt: null, summary: null, transcript: [], lastEventId: 4,
notes: [{ id: "n1", kind: "symptom", text: "Tired", quote: "I'm tired", questionId: null, transcriptLineIds: [], at: "2026-10-30T10:02:00.000Z", urgent: false,
acknowledgedAt: null, acknowledgedBy: null, disputed: false, addedToCheckAt: null, validation: "pending", endedEarly: false }],
};
const view = checkInFromWire(wire);
expect(view.plan.linkedTiers).toEqual([]);
expect(view).not.toHaveProperty("script");
expect(view.notes[0].validation).toBe("pending");
const next = applyEvent(view, eventFromWire({ id: 5, type: "alert", at: "2026-10-30T10:20:00.000Z", kind: "escalated", message: "Not acknowledged for 15 minutes" }));
expect(next.alerts).toEqual([{ id: 5, at: "2026-10-30T10:20:00.000Z", kind: "escalated", message: "Not acknowledged for 15 minutes" }]);
expect(applyEvent(next, { id: 5, type: "alert", at: "", kind: null, message: "again" }).alerts).toHaveLength(1);
});
});
describe("the client calls the backend's routes", () => {
afterEach(() => vi.unstubAllGlobals());
const reply = (status: number, body: unknown) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
it("adds a patient with POST /patients, then reads its detail; searches by POST; locks and unlocks", async () => {
const calls: string[] = [];
vi.stubGlobal("fetch", async (url: string, init: RequestInit) => {
calls.push(`${init.method} ${url}${init.body ? ` ${init.body}` : ""}`);
if (url.endsWith("/patients") && init.method === "POST") return reply(201, { patient: { id: "p9" } });
if (url.endsWith("/patients/p9")) return reply(200, { patient: { id: "p9", kind: "clinic_added", name: "Tunde Bakare", sharing: null, attention: { urgent: false, unreadNotes: 0, newSinceLastCheck: false } } });
if (url.endsWith("/patients/search")) return reply(200, { items: [], asOf: "2026-10-30T10:00:00.000Z" });
if (url.endsWith("/me")) return reply(200, me({ stepUpDone: true }));
return reply(200, { locked: true, stepUpDone: true, method: "totp" });
});
await clinicianApi.createPatient({ name: "Tunde Bakare", dateOfBirth: "1972-01-02", age: null, sex: null, attestation: { careRelationship: true, patientAgreed: true } });
await clinicianApi.searchPatients("Tunde");
expect((await clinicianApi.lock()).stepUp).toBe("done");
await clinicianApi.verifyTotp("123456");
expect(calls).toEqual([
'POST /api/clinician/patients {"displayName":"Tunde Bakare","dateOfBirth":"1972-01-02","attestation":{"careRelationship":true,"patientAgreement":true,"version":"2026-10-30"}}',
"GET /api/clinician/patients/p9",
'POST /api/clinician/patients/search {"query":"Tunde"}',
"POST /api/clinician/session/lock {}",
"GET /api/clinician/me",
'POST /api/clinician/step-up/verify {"code":"123456"}',
"GET /api/clinician/me",
]);
});
it("reads a switched-off area as empty and carries the error's extra fields", async () => {
vi.stubGlobal("fetch", async (url: string) =>
url.endsWith("/invitations")
? reply(404, { code: "CLINICIAN_INVITES_NOT_ENABLED", message: "Inviting a clinician is not available yet" })
: reply(409, { code: "URGENT_CALL_FIRST", message: "Call Mariam first.", phone: "+234 803 555 0147" }),
);
expect(await clinicianApi.invitations()).toEqual({ invitations: [] });
await expect(clinicianApi.createCheckIn("p1", {} as never)).rejects.toMatchObject({ status: 409, code: "URGENT_CALL_FIRST", details: { phone: "+234 803 555 0147" } });
});
});