PR B: VALIDATING phase + unified ExceptionManager (arch steps 3, 7)
- services/exceptionManager.ts: single taxonomy (timing/data/control/ business/system) with §12 codes, deterministic route() table, and handle() dispatch to retry/DLQ/escalate - services/execution.ts: refactor executePlan to drive the full 12-state machine (DRAFT -> INITIATED -> ... -> VALIDATING -> COMMITTED -> CLOSED) via stateMachine.transition(), with a new validatePhase() that reconciles DLT tx hash + bank message id + per-step amounts before COMMIT; SoD-gated edges use distinct synthetic actors by default - api/plans.ts + index.ts: GET /api/plans/:planId/state returning current transaction_state + full audit trail of transitions - tests/unit/exceptionManager.test.ts: 14 tests for classification + routing matrix 59 tests pass. tsc clean.
This commit is contained in:
69
orchestrator/tests/unit/exceptionManager.test.ts
Normal file
69
orchestrator/tests/unit/exceptionManager.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import {
|
||||
Business,
|
||||
Control,
|
||||
Data,
|
||||
SettlementException,
|
||||
Timing,
|
||||
classify,
|
||||
route,
|
||||
} from "../../src/services/exceptionManager";
|
||||
|
||||
describe("ExceptionManager — architecture note §12", () => {
|
||||
describe("classification taxonomy", () => {
|
||||
it("builds the four §12 classes via factory functions", () => {
|
||||
expect(Timing.dispatch().exceptionClass).toBe("timing");
|
||||
expect(Timing.dispatch().code).toBe("dispatch_timeout");
|
||||
|
||||
expect(Data.valueMismatch().exceptionClass).toBe("data");
|
||||
expect(Data.documentHashMismatch().code).toBe("document_hash_mismatch");
|
||||
|
||||
expect(Control.unauthorized("nobody").exceptionClass).toBe("control");
|
||||
expect(Control.duplicate("ev-1").code).toBe("duplicate_event");
|
||||
|
||||
expect(Business.manualStop("operator halted").exceptionClass).toBe("business");
|
||||
expect(Business.policyViolation({ rule: "LTV" }).code).toBe("policy_rule_violation");
|
||||
});
|
||||
|
||||
it("classify() tags network/timeout errors as system/network_error", () => {
|
||||
const ex = classify(new Error("ETIMEDOUT connect"));
|
||||
expect(ex.exceptionClass).toBe("system");
|
||||
expect(ex.code).toBe("network_error");
|
||||
});
|
||||
|
||||
it("classify() tags postgres errors as system/database_error", () => {
|
||||
const ex = classify(new Error("postgres connection refused"));
|
||||
expect(ex.exceptionClass).toBe("system");
|
||||
expect(ex.code).toBe("database_error");
|
||||
});
|
||||
|
||||
it("classify() is idempotent for SettlementException inputs", () => {
|
||||
const original = Data.valueMismatch({ field: "amount" });
|
||||
expect(classify(original)).toBe(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deterministic routing", () => {
|
||||
const cases: Array<[SettlementException, string]> = [
|
||||
[Timing.dispatch(), "retry"],
|
||||
[Timing.settlement(), "retry"],
|
||||
[Data.valueMismatch(), "abort_transaction"],
|
||||
[Data.documentHashMismatch(), "abort_transaction"],
|
||||
[Control.missingApproval(), "escalate"],
|
||||
[Control.unauthorized("x"), "escalate"],
|
||||
[Control.duplicate("ev"), "dead_letter"],
|
||||
[Business.manualStop("halt"), "abort_transaction"],
|
||||
[Business.policyViolation({ rule: "LTV" }), "escalate"],
|
||||
];
|
||||
|
||||
it.each(cases)("routes %j to %s", (ex, expected) => {
|
||||
expect(route(ex)).toBe(expected);
|
||||
});
|
||||
|
||||
it("network errors retry; non-network system errors dead-letter", () => {
|
||||
expect(route(classify(new Error("ETIMEDOUT")))).toBe("retry");
|
||||
const dbErr = classify(new Error("postgres broken"));
|
||||
expect(route(dbErr)).toBe("dead_letter");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user