Files
suaveplan/packages/api-stack/api/api-saga/src/state/state-machine.test.ts
2026-05-27 23:29:53 +01:00

134 lines
5.1 KiB
TypeScript

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