134 lines
5.1 KiB
TypeScript
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);
|
|
});
|
|
});
|