/* Unit tests for effects covering RevertibleEffectHandler; TransactionalEffectHandler; CompensatableEffectHandler. It also guards the documented public contract and regression-sensitive edge cases. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1311: Compass header sweep — declares the effects suite's contract for test-file scaffold coverage. */ import { describe, it, expect } from "vitest"; import { RevertibleEffectHandler, TransactionalEffectHandler, CompensatableEffectHandler, IrreversibleEmissionEffectHandler, createEffectHandler, buildUnwindReport, } from "../effects.ts"; import type { EffectDeclarationV1, CompensationAction, } from "@warpgogol/werkstatt-shared/component"; const _VALID_SHA = "sha256:" + "a".repeat(64); const TEST_COMPENSATION_ACTION: CompensationAction = { compensatingOperation: "test.compensate", verificationProbes: [ { type: "http-status", target: "https://example.com", expected: "200", timeoutMs: 5000 }, ], verificationTimeoutMs: 30000, failureMode: "blocking", }; describe("RevertibleEffectHandler", () => { it("dispose calls disposer and succeeds", async () => { let disposed = false; const handler = new RevertibleEffectHandler(async () => { disposed = true; }); const result = await handler.dispose(); expect(result.ok).toBe(true); expect(result.outcome).toBe("aborted"); expect(disposed).toBe(true); }); it("dispose is idempotent", async () => { let count = 0; const handler = new RevertibleEffectHandler(async () => { count++; }); await handler.dispose(); await handler.dispose(); expect(count).toBe(1); }); it("dispose failure yields failed-rollback", async () => { const handler = new RevertibleEffectHandler(async () => { throw new Error("disposer failed"); }); const result = await handler.dispose(); expect(result.ok).toBe(false); expect(result.outcome).toBe("failed-rollback"); expect(result.error).toBe("disposer failed"); }); it("compensate is not supported", async () => { const handler = new RevertibleEffectHandler(async () => {}); const result = await handler.compensate(); expect(result.ok).toBe(false); }); }); describe("TransactionalEffectHandler", () => { it("prepare then commit succeeds", async () => { let prepared = false, committed = false; const handler = new TransactionalEffectHandler( "tx-1", async () => { prepared = true; }, async () => { committed = true; }, async () => {}, ); expect((await handler.prepare()).ok).toBe(true); expect(prepared).toBe(true); expect((await handler.commit()).ok).toBe(true); expect(committed).toBe(true); }); it("commit before prepare fails", async () => { const handler = new TransactionalEffectHandler( "tx-2", async () => {}, async () => {}, async () => {}, ); const result = await handler.commit(); expect(result.ok).toBe(false); expect(result.outcome).toBe("failed-rollback"); }); it("abort after prepare calls abortFn", async () => { let aborted = false; const handler = new TransactionalEffectHandler( "tx-3", async () => {}, async () => {}, async () => { aborted = true; }, ); await handler.prepare(); const result = await handler.abort(); expect(result.ok).toBe(true); expect(result.outcome).toBe("aborted"); expect(aborted).toBe(true); }); it("commit is idempotent", async () => { let count = 0; const handler = new TransactionalEffectHandler( "tx-4", async () => {}, async () => { count++; }, async () => {}, ); await handler.prepare(); await handler.commit(); await handler.commit(); expect(count).toBe(1); }); }); describe("CompensatableEffectHandler", () => { it("requires at least one verification probe", () => { const emptyAction: CompensationAction = { compensatingOperation: "test.compensate", verificationProbes: [], verificationTimeoutMs: 30000, failureMode: "blocking", }; expect( () => new CompensatableEffectHandler( async () => {}, async () => {}, emptyAction, ), ).toThrow(); }); it("commit then compensate succeeds", async () => { let committed = false, compensated = false; const handler = new CompensatableEffectHandler( async () => { committed = true; }, async () => { compensated = true; }, TEST_COMPENSATION_ACTION, ); expect((await handler.commit()).ok).toBe(true); expect(committed).toBe(true); const result = await handler.compensate(); expect(result.ok).toBe(true); expect(result.outcome).toBe("compensated"); expect(compensated).toBe(true); }); it("compensate without commit is a no-op abort", async () => { const handler = new CompensatableEffectHandler( async () => {}, async () => {}, TEST_COMPENSATION_ACTION, ); const result = await handler.compensate(); expect(result.ok).toBe(true); expect(result.outcome).toBe("aborted"); }); it("compensation failure quarantines", async () => { const handler = new CompensatableEffectHandler( async () => {}, async () => { throw new Error("compensation failed"); }, TEST_COMPENSATION_ACTION, ); await handler.commit(); const result = await handler.compensate(); expect(result.ok).toBe(false); expect(result.outcome).toBe("quarantined"); }); it("exposes compensation action via getter", () => { const handler = new CompensatableEffectHandler( async () => {}, async () => {}, TEST_COMPENSATION_ACTION, ); expect(handler.compensation).toBe(TEST_COMPENSATION_ACTION); }); }); describe("IrreversibleEmissionEffectHandler", () => { it("withholds until commit", async () => { let emitted = false; const handler = new IrreversibleEmissionEffectHandler(async () => { emitted = true; }); expect(handler.isWithheld).toBe(true); const prepResult = await handler.prepare(); expect(prepResult.outcome).toBe("withheld"); expect(emitted).toBe(false); const result = await handler.commit(); expect(result.ok).toBe(true); expect(result.outcome).toBe("committed"); expect(emitted).toBe(true); }); it("abort before emit succeeds", async () => { let emitted = false; const handler = new IrreversibleEmissionEffectHandler(async () => { emitted = true; }); const result = await handler.abort(); expect(result.ok).toBe(true); expect(result.outcome).toBe("aborted"); expect(emitted).toBe(false); }); it("abort after emit quarantines", async () => { const handler = new IrreversibleEmissionEffectHandler(async () => {}); await handler.commit(); const result = await handler.abort(); expect(result.ok).toBe(false); expect(result.outcome).toBe("quarantined"); }); it("compensate after emit quarantines", async () => { const handler = new IrreversibleEmissionEffectHandler(async () => {}); await handler.commit(); const result = await handler.compensate(); expect(result.ok).toBe(false); expect(result.outcome).toBe("quarantined"); }); }); describe("createEffectHandler", () => { function makeDecl(effectClass: EffectDeclarationV1["effectClass"]): EffectDeclarationV1 { return { effectClass, description: "test", recoveryCommand: null, commitMetadata: null, }; } it("creates revertible handler", () => { const h = createEffectHandler(makeDecl("revertible"), { disposer: async () => {} }); expect(h.effectClass).toBe("revertible"); }); it("creates transactional handler", () => { const h = createEffectHandler(makeDecl("transactional"), { prepare: async () => {}, commit: async () => {}, abort: async () => {}, }); expect(h.effectClass).toBe("transactional"); }); it("creates compensatable handler", () => { const h = createEffectHandler(makeDecl("compensatable"), { commit: async () => {}, compensate: async () => {}, compensationAction: TEST_COMPENSATION_ACTION, }); expect(h.effectClass).toBe("compensatable"); }); it("rejects compensatable handler without compensationAction", () => { expect(() => createEffectHandler(makeDecl("compensatable"), { commit: async () => {}, compensate: async () => {}, }), ).toThrow(/EFFECT-02/); }); it("creates irreversible-emission handler", () => { const h = createEffectHandler(makeDecl("irreversible-emission"), { emit: async () => {} }); expect(h.effectClass).toBe("irreversible-emission"); }); }); describe("buildUnwindReport", () => { it("allSucceeded when all entries are clean", () => { const report = buildUnwindReport([ { componentId: "a/b" as const, effectClass: "revertible", outcome: "aborted", error: null }, { componentId: "a/b" as const, effectClass: "transactional", outcome: "committed", error: null, }, ]); expect(report.allSucceeded).toBe(true); expect(report.quarantined).toBe(false); }); it("quarantined when any entry is quarantined", () => { const report = buildUnwindReport([ { componentId: "a/b" as const, effectClass: "revertible", outcome: "aborted", error: null }, { componentId: "a/b" as const, effectClass: "irreversible-emission", outcome: "quarantined", error: "oops", }, ]); expect(report.allSucceeded).toBe(false); expect(report.quarantined).toBe(true); }); it("not allSucceeded when any entry is failed-rollback", () => { const report = buildUnwindReport([ { componentId: "a/b" as const, effectClass: "revertible", outcome: "failed-rollback", error: "fail", }, ]); expect(report.allSucceeded).toBe(false); expect(report.quarantined).toBe(true); }); });