Files
CurrenciCombo/orchestrator/tests/unit/obligations.test.ts
nsatoshi b77ebce497
Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
PR S: Machine-form obligation layer (terms-as-data) (#23)
2026-04-22 20:30:32 +00:00

285 lines
8.5 KiB
TypeScript

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);
});
});
});