import { describe, expect, it, vi } from "vitest" import { positiveMinor, type RecordTerms, recordTerms, revision } from "../contract/index.js" import { recordBalanced, transition, validateBalanced } from "../core/index.js" import type { LedgerService } from "../core/ports.js" import { retryIdentity } from "../ui/retry.js" export const A = "acc_00000000-0000-4000-8000-000000000001" export const B = "acc_00000000-0000-4000-8000-000000000002" export const T = "txn_00000000-0000-4000-8000-000000000001" export const terms: RecordTerms = { effectiveAt: "2026-11-03T00:00:00Z", description: "Synthetic transfer", status: "pending", entries: [ { accountId: A, direction: "debit", amountMinor: "9007199254740993" }, { accountId: B, direction: "credit", amountMinor: "9007199254740993" }, ], } export const service = () => ({ verify: vi.fn(async () => ({ checkedAccounts: 2, drift: [] as string[] })), account: vi.fn(async (id: string) => ({ accountId: id, currency: "EUR", status: "active" })), record: vi.fn(async () => ({ transactionId: T })), transition: vi.fn(async () => ({ transactionId: T })), }) as unknown as LedgerService describe("exact ledger core and retry policy (port tier)", () => { it("accepts i64 maximum and refuses overflow, zero, floats and unsafe numeric inputs", () => { for (const schema of [positiveMinor, revision]) { expect(schema.parse("9223372036854775807")).toBe("9223372036854775807") for (const invalid of ["9223372036854775808", "0", "01", "1.5", 9007199254740992]) expect(schema.safeParse(invalid).success).toBe(false) } expect(recordTerms.parse(terms).entries[0]?.amountMinor).toBe("9007199254740993") }) it("balances exact BigInt totals by currency and detects one-unit and cross-currency negatives", async () => { const s = service() await validateBalanced(s, terms) const [debit, credit] = terms.entries if (!debit || !credit) throw new Error("missing synthetic entries") await expect( validateBalanced(s, { ...terms, entries: [debit, { ...credit, amountMinor: "9007199254740992" }], }), ).rejects.toThrow() vi.mocked(s.account).mockImplementation( async (id) => ({ accountId: id, currency: id === A ? "EUR" : "USD", status: "active" }) as never, ) await expect(validateBalanced(s, terms)).rejects.toThrow() }) it("does not pre-read mutable account or transaction state before service replay", async () => { const s = service() vi.mocked(s.account).mockRejectedValue(new Error("archived after first commit")) await recordBalanced(s, terms, "retry") await recordBalanced(s, terms, "retry") expect(s.account).not.toHaveBeenCalled() expect(vi.mocked(s.record).mock.calls.map((call) => call[1])).toEqual([ "app-ledger:record:retry", "app-ledger:record:retry", ]) await transition(s, "post", T, "9007199254740993", "retry") expect(s.transition).toHaveBeenCalledWith("post", T, "9007199254740993", "app-ledger:post:retry") }) it("fences new holds, posts and reversals on drift, fails closed on verifier outage, permits void recovery", async () => { const s = service() vi.mocked(s.verify).mockResolvedValue({ checkedAccounts: 2, drift: [A] }) await expect(recordBalanced(s, terms, "r")).rejects.toThrow() for (const action of ["post", "reverse"] as const) await expect(transition(s, action, T, "1", "r")).rejects.toThrow() expect(s.record).not.toHaveBeenCalled() expect(s.transition).not.toHaveBeenCalled() await transition(s, "void", T, "1", "r") vi.mocked(s.verify).mockRejectedValue(new Error("offline")) await expect(recordBalanced(s, terms, "r")).rejects.toThrow("offline") }) it("preserves service state/funds/overflow refusals without replacing them with local storage", async () => { const s = service() for (const reason of ["transaction-state", "insufficient-funds", "overflow", "revision-conflict"]) { const error = new Error(reason) vi.mocked(s.transition).mockRejectedValueOnce(error) await expect(transition(s, "post", T, "1", "r")).rejects.toBe(error) } }) it("reuses identical payload keys, rotates for changed input and for returning to an earlier payload", () => { const key = retryIdentity() const first = key(terms) expect(key(structuredClone(terms))).toBe(first) expect(key({ ...terms, description: "Changed operational label" })).not.toBe(first) expect(key(terms)).not.toBe(first) }) })