// @vitest-environment node // `useTailStore` is a pure zustand store with no DOM / React dependencies, // so a node environment is fine. We import `getState()` and `setState` // from the store directly (no React render) and assert on the resulting // state shape. import { beforeEach, describe, expect, it } from "vitest"; import type { Ack, Activity, Claim, ClaimAck, Remittance, Ta1Ack, } from "@/types"; import { TAIL_CAP, useTailStore } from "./tail-store"; function makeClaim(id: string, overrides: Partial = {}): Claim { return { id, patientName: `Patient ${id}`, providerNpi: "1234567890", payerName: "Medicaid", cptCode: "99213", billedAmount: 100, receivedAmount: 0, status: "submitted", submissionDate: "2026-06-20T00:00:00Z", ...overrides, }; } function makeRemittance(id: string, overrides: Partial = {}): Remittance { return { id, claimId: `CLM-${id}`, payerName: "Medicaid", paidAmount: 100, adjustmentAmount: 0, receivedDate: "2026-06-20", checkNumber: id, status: "received", ...overrides, }; } function makeActivity(id: string, overrides: Partial = {}): Activity { return { id, kind: "claim_submitted", message: `Event ${id}`, timestamp: "2026-06-20T00:00:00Z", ...overrides, }; } function makeAck(id: number, overrides: Partial = {}): Ack { return { id, sourceBatchId: `BATCH-${id}`, acceptedCount: 1, rejectedCount: 0, receivedCount: 1, ackCode: "A", parsedAt: "2026-07-02T12:00:00Z", patientControlNumber: `PCN-${id}`, ...overrides, }; } function makeTa1Ack(id: number, overrides: Partial = {}): Ta1Ack { return { id, controlNumber: `0000000${id}`, ackCode: "A", noteCode: "000", interchangeDate: "2026-07-02", interchangeTime: "1200", senderId: "SENDER", receiverId: "RECEIVER", sourceBatchId: `TA1-BATCH-${id}`, parsedAt: "2026-07-02T12:00:00Z", ...overrides, }; } function makeClaimAck(id: number, overrides: Partial = {}): ClaimAck { return { id, claimId: "CLM-1", batchId: null, ackId: 99, ackKind: "999", ak2Index: 0, setControlNumber: "991102989", setAcceptRejectCode: "A", linkedAt: "2026-07-02T12:00:00Z", linkedBy: "auto", claimState: "received", ...overrides, }; } describe("useTailStore", () => { // Each test starts from a known-empty state. The store is module-level // (singleton), so we use `reset()` to clear between cases — that's also // what production code calls on stream open. beforeEach(() => { useTailStore.getState().reset("claims"); useTailStore.getState().reset("remittances"); useTailStore.getState().reset("activity"); useTailStore.getState().reset("acks"); useTailStore.getState().reset("ta1_acks"); useTailStore.getState().reset("claim-acks"); }); it("test_add_claim_adds_new_claim_keyed_by_id", () => { const { addClaim } = useTailStore.getState(); addClaim(makeClaim("CLM-1")); addClaim(makeClaim("CLM-2")); const { claims } = useTailStore.getState(); expect(Object.keys(claims)).toHaveLength(2); expect(claims["CLM-1"]?.id).toBe("CLM-1"); expect(claims["CLM-2"]?.id).toBe("CLM-2"); }); it("test_add_claim_with_duplicate_id_is_noop", () => { const { addClaim } = useTailStore.getState(); addClaim(makeClaim("CLM-1", { patientName: "Original" })); addClaim(makeClaim("CLM-1", { patientName: "Updated" })); const { claims } = useTailStore.getState(); expect(Object.keys(claims)).toHaveLength(1); // First write wins; duplicates are silently dropped so the snapshot // replay on reconnect doesn't trample the canonical row. expect(claims["CLM-1"]?.patientName).toBe("Original"); }); it("test_reset_claims_clears_only_claims_slice", () => { const { addClaim, addRemittance, addActivity, reset } = useTailStore.getState(); addClaim(makeClaim("CLM-1")); addRemittance(makeRemittance("RMT-1")); addActivity(makeActivity("ACT-1")); reset("claims"); const s = useTailStore.getState(); expect(Object.keys(s.claims)).toHaveLength(0); expect(Object.keys(s.remittances)).toHaveLength(1); expect(s.activity).toHaveLength(1); }); it("test_add_activity_appends", () => { const { addActivity } = useTailStore.getState(); addActivity(makeActivity("ACT-1", { message: "first" })); addActivity(makeActivity("ACT-2", { message: "second" })); addActivity(makeActivity("ACT-3", { message: "third" })); const { activity } = useTailStore.getState(); expect(activity).toHaveLength(3); // Activity has no stable id; it lives in an array. Insertion order is // the only ordering signal. expect(activity.map((a) => a.message)).toEqual(["first", "second", "third"]); }); it("test_fifo_cap_evicts_oldest_when_over_10000", () => { // Insert 5 over the cap so the eviction policy must actually run. // The plan calls for evicting the oldest 100 (or enough to be back at // the cap) — we just assert: size == TAIL_CAP and the very-first // items are gone. // // 10 005 individual `addClaim` calls is intentionally a stress test // of the eviction path: each call rebuilds the dict + order array // so the wall-clock cost is O(N^2) in the number of items ever // inserted. Bump the per-test timeout from the default 5s to 60s // so this case can complete in CI on slow runners. const N = TAIL_CAP + 5; const { addClaim } = useTailStore.getState(); for (let i = 0; i < N; i++) { addClaim(makeClaim(`CLM-${i.toString().padStart(6, "0")}`)); } const { claims } = useTailStore.getState(); expect(Object.keys(claims)).toHaveLength(TAIL_CAP); // The five oldest (CLM-000000 .. CLM-000004) must be gone. expect(claims["CLM-000000"]).toBeUndefined(); expect(claims["CLM-000004"]).toBeUndefined(); // The most recent (CLM-0010004) must be present. const lastKey = `CLM-${(N - 1).toString().padStart(6, "0")}`; expect(claims[lastKey]).toBeDefined(); }, 60_000); // ------------------------------------------------------------------------- // SP25: acks + ta1_acks slices mirror the claims/remittances key-by-id // pattern (both have a stable `id` from the database row). // ------------------------------------------------------------------------- it("test_add_ack_adds_new_ack_keyed_by_id", () => { const { addAck } = useTailStore.getState(); addAck(makeAck(1)); addAck(makeAck(2)); const { acks } = useTailStore.getState(); expect(Object.keys(acks)).toHaveLength(2); expect(acks["1"]?.sourceBatchId).toBe("BATCH-1"); expect(acks["2"]?.sourceBatchId).toBe("BATCH-2"); }); it("test_add_ack_with_duplicate_id_is_noop", () => { const { addAck } = useTailStore.getState(); addAck(makeAck(1, { sourceBatchId: "BATCH-original" })); addAck(makeAck(1, { sourceBatchId: "BATCH-replay" })); const { acks } = useTailStore.getState(); expect(Object.keys(acks)).toHaveLength(1); // First write wins; snapshot replay must not trample the canonical row. expect(acks["1"]?.sourceBatchId).toBe("BATCH-original"); }); it("test_reset_acks_clears_only_acks_slice", () => { const { addAck, addClaim, reset } = useTailStore.getState(); addAck(makeAck(1)); addClaim(makeClaim("CLM-1")); reset("acks"); const s = useTailStore.getState(); expect(Object.keys(s.acks)).toHaveLength(0); // The other slices are untouched. expect(Object.keys(s.claims)).toHaveLength(1); }); it("test_add_ta1_ack_adds_new_ta1_ack_keyed_by_id", () => { const { addTa1Ack } = useTailStore.getState(); addTa1Ack(makeTa1Ack(1)); addTa1Ack(makeTa1Ack(2)); const { ta1Acks } = useTailStore.getState(); expect(Object.keys(ta1Acks)).toHaveLength(2); expect(ta1Acks["1"]?.controlNumber).toBe("00000001"); expect(ta1Acks["2"]?.controlNumber).toBe("00000002"); }); it("test_add_ta1_ack_with_duplicate_id_is_noop", () => { const { addTa1Ack } = useTailStore.getState(); addTa1Ack(makeTa1Ack(7, { senderId: "ORIGINAL" })); addTa1Ack(makeTa1Ack(7, { senderId: "REPLAY" })); const { ta1Acks } = useTailStore.getState(); expect(Object.keys(ta1Acks)).toHaveLength(1); expect(ta1Acks["7"]?.senderId).toBe("ORIGINAL"); }); it("test_reset_ta1_acks_clears_only_ta1_acks_slice", () => { const { addTa1Ack, addAck, reset } = useTailStore.getState(); addTa1Ack(makeTa1Ack(1)); addAck(makeAck(1)); reset("ta1_acks"); const s = useTailStore.getState(); expect(Object.keys(s.ta1Acks)).toHaveLength(0); expect(Object.keys(s.acks)).toHaveLength(1); }); // ------------------------------------------------------------------------- // SP28: the claim↔ack link slice mirrors the SP25 acks/ta1_acks slice // (numeric id from the `claim_acks` table, first write wins, FIFO-capped // at TAIL_CAP). The slice drives both the ClaimDrawer's // panel and the AckDrawer's panel // via `useTailStream("claim-acks")` + `useMergedTail("claim-acks", …)`. // ------------------------------------------------------------------------- it("test_add_claim_ack_adds_new_claim_ack_keyed_by_id", () => { const { addClaimAck } = useTailStore.getState(); addClaimAck(makeClaimAck(1)); addClaimAck(makeClaimAck(2)); const { claimAcks } = useTailStore.getState(); expect(Object.keys(claimAcks)).toHaveLength(2); expect(claimAcks["1"]?.claimId).toBe("CLM-1"); expect(claimAcks["2"]?.claimId).toBe("CLM-1"); }); it("test_add_claim_ack_with_duplicate_id_is_noop", () => { const { addClaimAck } = useTailStore.getState(); addClaimAck(makeClaimAck(1, { setAcceptRejectCode: "A" })); addClaimAck(makeClaimAck(1, { setAcceptRejectCode: "R" })); const { claimAcks } = useTailStore.getState(); expect(Object.keys(claimAcks)).toHaveLength(1); // First write wins; snapshot replay must not trample the canonical row. expect(claimAcks["1"]?.setAcceptRejectCode).toBe("A"); }); it("test_reset_claim_acks_clears_only_claim_acks_slice", () => { const { addClaimAck, addAck, reset } = useTailStore.getState(); addClaimAck(makeClaimAck(1)); addAck(makeAck(1)); reset("claim-acks"); const s = useTailStore.getState(); expect(Object.keys(s.claimAcks)).toHaveLength(0); // The other slices are untouched. expect(Object.keys(s.acks)).toHaveLength(1); }); it("test_claim_acks_slice_evicts_oldest_when_over_10000", () => { // Cap-eviction smoke test — same shape as the claims FIFO test // above. 5 over the cap to exercise the eviction path without // pinning down the wall-clock cost of a full 10 005-insert run. const N = TAIL_CAP + 5; const { addClaimAck } = useTailStore.getState(); for (let i = 0; i < N; i++) { addClaimAck(makeClaimAck(i)); } const { claimAcks } = useTailStore.getState(); expect(Object.keys(claimAcks)).toHaveLength(TAIL_CAP); // The five oldest ids must be gone. expect(claimAcks["0"]).toBeUndefined(); expect(claimAcks["4"]).toBeUndefined(); // The most recent id must be present. expect(claimAcks[String(N - 1)]).toBeDefined(); }, 60_000); });