import { describe, expect, it } from "@suaveplan/testing/runner"; import { SAGA_CODES, SagaError } from "../error-codes/error-codes.js"; import { assertInstanceTransition, assertStepTransition, canTransitionInstance, canTransitionStep, isTerminalInstance, isTerminalStep, } from "./state-machine.js"; describe("state-machine — instance", () => { it("allows the documented forward transitions", { timeout: 5000 }, () => { expect(canTransitionInstance("running", "completed")).toBe(true); expect(canTransitionInstance("running", "compensating")).toBe(true); expect(canTransitionInstance("running", "cancelled")).toBe(true); expect(canTransitionInstance("compensating", "compensated")).toBe(true); expect( canTransitionInstance("compensating", "compensation_failed") ).toBe(true); }); it("accepts idempotent same-state transitions", { timeout: 5000 }, () => { expect(canTransitionInstance("running", "running")).toBe(true); expect(canTransitionInstance("completed", "completed")).toBe(true); }); it("rejects transitions out of terminal states", { timeout: 5000 }, () => { expect(canTransitionInstance("completed", "running")).toBe(false); expect(canTransitionInstance("compensated", "running")).toBe(false); expect(canTransitionInstance("compensation_failed", "completed")).toBe( false ); expect(canTransitionInstance("cancelled", "running")).toBe(false); }); it("assertInstanceTransition throws INVALID_STATE_TRANSITION", { timeout: 5000, }, () => { try { assertInstanceTransition("s_1", "completed", "running"); throw new Error("expected throw"); } catch (err) { expect(err).toBeInstanceOf(SagaError); if (err instanceof SagaError) { expect(err.code).toBe(SAGA_CODES.INVALID_STATE_TRANSITION); expect(err.context).toEqual({ sagaId: "s_1", from: "completed", to: "running", }); } } }); it("assertInstanceTransition accepts a valid transition", { timeout: 5000, }, () => { expect(() => assertInstanceTransition("s_1", "running", "completed") ).not.toThrow(); }); it("isTerminalInstance flags terminal states", { timeout: 5000 }, () => { expect(isTerminalInstance("completed")).toBe(true); expect(isTerminalInstance("compensated")).toBe(true); expect(isTerminalInstance("compensation_failed")).toBe(true); expect(isTerminalInstance("cancelled")).toBe(true); expect(isTerminalInstance("running")).toBe(false); expect(isTerminalInstance("compensating")).toBe(false); }); }); describe("state-machine — step", () => { it("allows pending → in_flight → completed", { timeout: 5000 }, () => { expect(canTransitionStep("pending", "in_flight")).toBe(true); expect(canTransitionStep("in_flight", "completed")).toBe(true); }); it("allows compensation cycle", { timeout: 5000 }, () => { expect(canTransitionStep("completed", "compensating")).toBe(true); expect(canTransitionStep("compensating", "compensated")).toBe(true); expect(canTransitionStep("compensating", "failed")).toBe(true); }); it("rejects skip-ahead and out-of-terminal transitions", { timeout: 5000, }, () => { expect(canTransitionStep("pending", "completed")).toBe(false); expect(canTransitionStep("compensated", "in_flight")).toBe(false); expect(canTransitionStep("failed", "compensating")).toBe(false); }); it("assertStepTransition throws on rejected pair", { timeout: 5000, }, () => { try { assertStepTransition("s_1", "step-a", "completed", "pending"); throw new Error("expected throw"); } catch (err) { expect(err).toBeInstanceOf(SagaError); if (err instanceof SagaError) { expect(err.code).toBe(SAGA_CODES.INVALID_STATE_TRANSITION); expect(err.context).toEqual({ sagaId: "s_1", stepName: "step-a", from: "completed", to: "pending", }); } } }); it("assertStepTransition accepts same-state and valid transitions", { timeout: 5000, }, () => { expect(() => assertStepTransition("s_1", "step-a", "pending", "pending") ).not.toThrow(); expect(() => assertStepTransition("s_1", "step-a", "pending", "in_flight") ).not.toThrow(); }); it("isTerminalStep flags terminal states", { timeout: 5000 }, () => { expect(isTerminalStep("completed")).toBe(true); expect(isTerminalStep("compensated")).toBe(true); expect(isTerminalStep("pending")).toBe(false); expect(isTerminalStep("in_flight")).toBe(false); expect(isTerminalStep("failed")).toBe(false); expect(isTerminalStep("compensating")).toBe(false); }); });