Initial Commit
This commit is contained in:
133
packages/api-stack/api/api-saga/src/state/state-machine.test.ts
Normal file
133
packages/api-stack/api/api-saga/src/state/state-machine.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user