import { describe, it, expect } from "@jest/globals"; import { canonicalize, hashObligationTerms, validateObligationTerms, evaluateClauses, evaluateCommit, evaluateAbort, buildIssueInstrumentObligation, type ObligationTerms, } from "../../src/services/obligations"; import { evaluateCondition, resolvePath } from "../../src/services/obligations/evaluator"; describe("Obligation layer (gap-analysis v2 ยง4.1)", () => { const instrument = { applicant: "ACME Corp", issuingBankBIC: "CHASUS33", beneficiaryBankBIC: "EBILAEAD", beneficiaryName: "Acme Beneficiary Ltd", beneficiaryAccount: "AE070331234567890123456", amount: 1_000_000, currency: "USD", tenor: "1Y", expiryDate: "2026-12-31", placeOfPresentation: "Dubai", governingLaw: "URDG 758", templateRef: "emirates-islamic-sblc-v3", templateHash: "a".repeat(64), }; const authorizedParticipants = [ { role: "coordinator" as const, actorId: "actor-1" }, { role: "approver" as const, actorId: "actor-2" }, { role: "releaser" as const, actorId: "actor-3" }, { role: "validator" as const, actorId: "actor-4" }, { role: "exception_manager" as const, actorId: "actor-5" }, ]; describe("canonicalize()", () => { it("sorts object keys at every depth", () => { const a = canonicalize({ b: 1, a: { d: 2, c: 3 } }); const b = canonicalize({ a: { c: 3, d: 2 }, b: 1 }); expect(a).toBe(b); expect(a).toBe('{"a":{"c":3,"d":2},"b":1}'); }); it("preserves array order", () => { expect(canonicalize({ x: [3, 1, 2] })).toBe('{"x":[3,1,2]}'); }); it("handles null and nested arrays of objects", () => { expect( canonicalize({ a: null, b: [{ y: 2, x: 1 }, { z: 3 }] }), ).toBe('{"a":null,"b":[{"x":1,"y":2},{"z":3}]}'); }); }); describe("hashObligationTerms()", () => { const terms = buildIssueInstrumentObligation({ instrument, payor: "ACME Corp", payee: "Acme Beneficiary Ltd", authorizedParticipants, }); it("produces a 64-char hex hash", () => { expect(hashObligationTerms(terms)).toMatch(/^[0-9a-f]{64}$/); }); it("is insensitive to key ordering", () => { const shuffled: ObligationTerms = { ...terms, consideration: { payee: terms.consideration.payee, currency: terms.consideration.currency, amount: terms.consideration.amount, payor: terms.consideration.payor, }, }; expect(hashObligationTerms(shuffled)).toBe(hashObligationTerms(terms)); }); it("changes when any field mutates", () => { const mutated: ObligationTerms = { ...terms, consideration: { ...terms.consideration, amount: 999 }, }; expect(hashObligationTerms(mutated)).not.toBe(hashObligationTerms(terms)); }); }); describe("validateObligationTerms()", () => { const valid = buildIssueInstrumentObligation({ instrument, payor: "A", payee: "B", authorizedParticipants, }); it("accepts a well-formed obligation", () => { expect(validateObligationTerms(valid).ok).toBe(true); }); it("rejects non-object input", () => { expect(validateObligationTerms(null).ok).toBe(false); expect(validateObligationTerms("nope").ok).toBe(false); }); it("flags missing consideration fields", () => { const bad = { ...valid, consideration: { payor: "A", payee: "B", currency: "usd", amount: -5 }, }; const r = validateObligationTerms(bad); expect(r.ok).toBe(false); expect(r.errors).toEqual( expect.arrayContaining([ expect.stringContaining("ISO-4217"), expect.stringContaining("amount"), ]), ); }); it("flags bad template hash", () => { const bad = { ...valid, governingDocuments: [ { templateRef: "t", templateHash: "not-a-hash" }, ], }; const r = validateObligationTerms(bad); expect(r.ok).toBe(false); expect(r.errors.some((e) => e.includes("hex SHA-256"))).toBe(true); }); it("flags empty authorizedParticipants[].role", () => { const bad = { ...valid, authorizedParticipants: [{ actorId: "x" }], }; const r = validateObligationTerms(bad); expect(r.ok).toBe(false); }); }); describe("evaluator", () => { it("resolvePath handles dotted + indexed paths", () => { const ctx = { plan: { steps: [{ type: "pay" }, { type: "issueInstrument" }] } }; expect(resolvePath("plan.steps[1].type", ctx)).toBe("issueInstrument"); expect(resolvePath("plan.missing.x", ctx)).toBeUndefined(); }); it("evaluates all/any/not combinators", () => { const ctx = { a: 1, b: 2 }; expect( evaluateCondition( { all: [ { path: "a", op: "eq", value: 1 }, { path: "b", op: "gt", value: 1 }, ], }, ctx, ), ).toBe(true); expect( evaluateCondition( { any: [ { path: "a", op: "eq", value: 99 }, { path: "b", op: "gt", value: 1 }, ], }, ctx, ), ).toBe(true); expect( evaluateCondition({ not: { path: "a", op: "eq", value: 2 } }, ctx), ).toBe(true); }); it("matches regex operator safely (no eval)", () => { expect( evaluateCondition( { path: "h", op: "matches", value: "^0x[0-9a-f]{4}$" }, { h: "0xbeef" }, ), ).toBe(true); expect( evaluateCondition( { path: "h", op: "matches", value: "^0x[0-9a-f]{4}$" }, { h: "0xBEEFG" }, ), ).toBe(false); }); }); describe("evaluateClauses / evaluateCommit / evaluateAbort", () => { const terms = buildIssueInstrumentObligation({ instrument, payor: "ACME Corp", payee: "Acme Beneficiary Ltd", authorizedParticipants, }); const passingCtx = { state: "VALIDATING", dlt: { tx_hash: "0x" + "b".repeat(64) }, bank: { iso_message_id: "MSG-1" }, exceptions: { active: [] }, instrument: { template_hash: instrument.templateHash, dispatched: true }, payment: { amount: instrument.amount, currency: instrument.currency, failed: false, }, }; it("evaluateCommit returns ok=true when all commit clauses pass", () => { const r = evaluateCommit(terms, passingCtx); expect(r.ok).toBe(true); expect(r.results.every((x) => x.ok)).toBe(true); }); it("evaluateCommit returns ok=false with per-clause reasons on failure", () => { const badCtx = { ...passingCtx, dlt: { tx_hash: "not-hex" } }; const r = evaluateCommit(terms, badCtx); expect(r.ok).toBe(false); const failing = r.results.find((x) => !x.ok); expect(failing?.clauseId).toBe("commit.dlt_tx_hash"); expect(failing?.failureReason).toBeTruthy(); }); it("evaluateAbort fires when an active exception exists", () => { const ctx = { ...passingCtx, exceptions: { active: [{ kind: "timeout" }] }, }; const r = evaluateAbort(terms, ctx); expect(r.ok).toBe(true); expect(r.results.find((x) => x.clauseId === "abort.exception_raised")?.ok).toBe( true, ); }); it("evaluateClauses surfaces evaluator errors without throwing", () => { const bogus = [ { id: "bogus", description: "bad regex", binds: "both" as const, assert: { path: "h", op: "matches" as const, value: "[" }, // invalid regex }, ]; const r = evaluateClauses(bogus, { h: "x" }); expect(r.ok).toBe(false); expect(r.results[0].failureReason).toBeTruthy(); }); }); describe("buildIssueInstrumentObligation()", () => { it("binds the instrument template hash into governingDocuments", () => { const terms = buildIssueInstrumentObligation({ instrument, payor: "A", payee: "B", authorizedParticipants, }); expect(terms.governingDocuments[0].templateHash).toBe(instrument.templateHash); expect(terms.governingDocuments[0].governingLaw).toBe("URDG 758"); }); it("validates cleanly", () => { const terms = buildIssueInstrumentObligation({ instrument, payor: "A", payee: "B", authorizedParticipants, }); expect(validateObligationTerms(terms).ok).toBe(true); }); }); });