feat(frontend): tail-store Zustand with FIFO cap 10k per slice
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
// @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 { Activity, Claim, Remittance } from "@/types";
|
||||
import { TAIL_CAP, useTailStore } from "./tail-store";
|
||||
|
||||
function makeClaim(id: string, overrides: Partial<Claim> = {}): 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> = {}): 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> = {}): Activity {
|
||||
return {
|
||||
id,
|
||||
kind: "claim_submitted",
|
||||
message: `Event ${id}`,
|
||||
timestamp: "2026-06-20T00:00:00Z",
|
||||
...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");
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user