State Machine Test Generator — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited State Machine Test Generator (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are an expert QA engineer specializing in state machine testing and workflow verification. When the user asks you to create, review, or improve state machine tests, follow these detailed instructions to generate comprehensive test suites that verify all states, transitions, guard conditions, entry/exit actions, and invalid transition rejection for workflow-driven features.
tests/
state-machines/
models/
order-machine.ts
payment-machine.ts
auth-machine.ts
generic-machine.ts
generators/
test-path-generator.ts
transition-table-generator.ts
invalid-transition-generator.ts
helpers/
machine-test-runner.ts
state-assertions.ts
action-spy.ts
tests/
order-lifecycle.test.ts
payment-flow.test.ts
auth-flow.test.ts
all-states-coverage.test.ts
all-transitions-coverage.test.ts
guard-conditions.test.ts
invalid-transitions.test.ts
nested-states.test.ts
parallel-states.test.ts
entry-exit-actions.test.ts
coverage/
state-coverage-reporter.ts
transition-matrix.ts
config/
machine-test.config.tsBefore generating tests, define the machine model. This model is the single source of truth for all test generation.
// order-machine.ts
import { createMachine, assign } from 'xstate';
interface OrderContext {
orderId: string;
items: Array<{ id: string; quantity: number; price: number }>;
total: number;
paymentId?: string;
shippingId?: string;
cancellationReason?: string;
refundAmount?: number;
retryCount: number;
}
type OrderEvent =
| { type: 'ADD_ITEM'; item: { id: string; quantity: number; price: number } }
| { type: 'REMOVE_ITEM'; itemId: string }
| { type: 'SUBMIT' }
| { type: 'PAYMENT_RECEIVED'; paymentId: string }
| { type: 'PAYMENT_FAILED'; reason: string }
| { type: 'RETRY_PAYMENT' }
| { type: 'SHIP'; shippingId: string }
| { type: 'DELIVER' }
| { type: 'CANCEL'; reason: string }
| { type: 'REQUEST_RETURN' }
| { type: 'APPROVE_RETURN' }
| { type: 'REJECT_RETURN' }
| { type: 'REFUND_PROCESSED'; amount: number };
const orderMachine = createMachine({
id: 'order',
initial: 'draft',
context: {
orderId: '',
items: [],
total: 0,
retryCount: 0,
} as OrderContext,
states: {
draft: {
on: {
ADD_ITEM: {
actions: assign({
items: ({ context, event }) => [...context.items, event.item],
total: ({ context, event }) => context.total + event.item.price * event.item.quantity,
}),
},
REMOVE_ITEM: {
actions: assign({
items: ({ context, event }) => context.items.filter(i => i.id !== event.itemId),
total: ({ context, event }) => {
const item = context.items.find(i => i.id === event.itemId);
return item ? context.total - item.price * item.quantity : context.total;
},
}),
},
SUBMIT: {
target: 'pending_payment',
guard: ({ context }) => context.items.length > 0,
},
CANCEL: 'cancelled',
},
},
pending_payment: {
on: {
PAYMENT_RECEIVED: {
target: 'confirmed',
actions: assign({ paymentId: ({ event }) => event.paymentId }),
},
PAYMENT_FAILED: [
{
target: 'payment_retry',
guard: ({ context }) => context.retryCount < 3,
},
{
target: 'cancelled',
},
],
CANCEL: {
target: 'cancelled',
actions: assign({ cancellationReason: ({ event }) => event.reason }),
},
},
},
payment_retry: {
entry: assign({ retryCount: ({ context }) => context.retryCount + 1 }),
on: {
RETRY_PAYMENT: 'pending_payment',
CANCEL: 'cancelled',
},
},
confirmed: {
on: {
SHIP: {
target: 'shipped',
actions: assign({ shippingId: ({ event }) => event.shippingId }),
},
CANCEL: {
target: 'refunding',
actions: assign({ cancellationReason: ({ event }) => event.reason }),
},
},
},
shipped: {
on: {
DELIVER: 'delivered',
},
},
delivered: {
on: {
REQUEST_RETURN: 'return_requested',
},
},
return_requested: {
on: {
APPROVE_RETURN: 'refunding',
REJECT_RETURN: 'delivered',
},
},
refunding: {
on: {
REFUND_PROCESSED: {
target: 'refunded',
actions: assign({ refundAmount: ({ event }) => event.amount }),
},
},
},
refunded: {
type: 'final',
},
cancelled: {
type: 'final',
},
},
});
// Export the machine definition for test generation
export { orderMachine, OrderContext, OrderEvent };Generating test paths systematically ensures coverage rather than relying on manual enumeration.
// test-path-generator.ts
interface StateNode {
name: string;
transitions: Transition[];
isFinal: boolean;
}
interface Transition {
event: string;
target: string;
guard?: string;
actions?: string[];
}
interface TestPath {
name: string;
steps: TestStep[];
startState: string;
endState: string;
coversStates: string[];
coversTransitions: string[];
}
interface TestStep {
fromState: string;
event: string;
toState: string;
guard?: string;
expectedActions?: string[];
}
class TestPathGenerator {
private stateGraph: Map<string, StateNode> = new Map();
private allStates: Set<string> = new Set();
private allTransitions: Set<string> = new Set();
constructor(machineDefinition: any) {
this.parseMachineDefinition(machineDefinition);
}
private parseMachineDefinition(definition: any): void {
for (const [stateName, stateConfig] of Object.entries(definition.states)) {
this.allStates.add(stateName);
const transitions: Transition[] = [];
if ((stateConfig as any).on) {
for (const [event, target] of Object.entries((stateConfig as any).on)) {
const targetState = typeof target === 'string' ? target : (target as any).target;
if (targetState) {
transitions.push({
event,
target: targetState,
guard: typeof target === 'object' ? (target as any).guard?.toString() : undefined,
});
this.allTransitions.add(`${stateName}--${event}-->${targetState}`);
}
}
}
this.stateGraph.set(stateName, {
name: stateName,
transitions,
isFinal: (stateConfig as any).type === 'final',
});
}
}
generateAllStatesCoverage(initialState: string): TestPath[] {
const paths: TestPath[] = [];
const uncoveredStates = new Set(this.allStates);
// BFS to find shortest paths to each state
const queue: Array<{ state: string; path: TestStep[] }> = [
{ state: initialState, path: [] },
];
const visited = new Set<string>();
while (queue.length > 0 && uncoveredStates.size > 0) {
const { state, path } = queue.shift()!;
if (visited.has(state)) continue;
visited.add(state);
if (uncoveredStates.has(state)) {
uncoveredStates.delete(state);
paths.push({
name: `reach_${state}`,
steps: [...path],
startState: initialState,
endState: state,
coversStates: path.map(s => s.toState).concat(state),
coversTransitions: path.map(s => `${s.fromState}--${s.event}-->${s.toState}`),
});
}
const node = this.stateGraph.get(state);
if (node && !node.isFinal) {
for (const transition of node.transitions) {
queue.push({
state: transition.target,
path: [...path, {
fromState: state,
event: transition.event,
toState: transition.target,
guard: transition.guard,
}],
});
}
}
}
return paths;
}
generateAllTransitionsCoverage(initialState: string): TestPath[] {
const paths: TestPath[] = [];
const uncoveredTransitions = new Set(this.allTransitions);
// For each uncovered transition, find the shortest path that includes it
for (const transition of uncoveredTransitions) {
const [fromState, rest] = transition.split('-->');
const [source, event] = fromState.split('--');
// Find path from initial to the source state
const pathToSource = this.findShortestPath(initialState, source);
if (pathToSource) {
const fullPath = [...pathToSource, {
fromState: source,
event,
toState: rest,
}];
paths.push({
name: `transition_${source}_${event}_${rest}`,
steps: fullPath,
startState: initialState,
endState: rest,
coversStates: fullPath.map(s => s.toState),
coversTransitions: fullPath.map(s => `${s.fromState}--${s.event}-->${s.toState}`),
});
}
}
return paths;
}
generateInvalidTransitions(): Array<{ state: string; event: string }> {
const invalidTransitions: Array<{ state: string; event: string }> = [];
const allEvents = new Set<string>();
// Collect all events from the machine
for (const node of this.stateGraph.values()) {
for (const t of node.transitions) {
allEvents.add(t.event);
}
}
// For each state, find events that are NOT valid
for (const [stateName, node] of this.stateGraph) {
if (node.isFinal) continue;
const validEvents = new Set(node.transitions.map(t => t.event));
for (const event of allEvents) {
if (!validEvents.has(event)) {
invalidTransitions.push({ state: stateName, event });
}
}
}
return invalidTransitions;
}
private findShortestPath(from: string, to: string): TestStep[] | null {
if (from === to) return [];
const queue: Array<{ state: string; path: TestStep[] }> = [
{ state: from, path: [] },
];
const visited = new Set<string>();
while (queue.length > 0) {
const { state, path } = queue.shift()!;
if (visited.has(state)) continue;
visited.add(state);
const node = this.stateGraph.get(state);
if (!node) continue;
for (const transition of node.transitions) {
const newPath = [...path, {
fromState: state,
event: transition.event,
toState: transition.target,
}];
if (transition.target === to) return newPath;
queue.push({ state: transition.target, path: newPath });
}
}
return null;
}
getCoverageReport(executedTransitions: Set<string>): {
stateCoverage: number;
transitionCoverage: number;
uncoveredStates: string[];
uncoveredTransitions: string[];
} {
const coveredStates = new Set<string>();
for (const t of executedTransitions) {
const parts = t.split('-->');
const source = parts[0].split('--')[0];
coveredStates.add(source);
coveredStates.add(parts[1]);
}
return {
stateCoverage: Math.round((coveredStates.size / this.allStates.size) * 100),
transitionCoverage: Math.round((executedTransitions.size / this.allTransitions.size) * 100),
uncoveredStates: [...this.allStates].filter(s => !coveredStates.has(s)),
uncoveredTransitions: [...this.allTransitions].filter(t => !executedTransitions.has(t)),
};
}
}// all-states-coverage.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createActor } from 'xstate';
import { orderMachine } from '../models/order-machine';
describe('Order Machine -- All States Coverage', () => {
it('should reach the draft state (initial)', () => {
const actor = createActor(orderMachine).start();
expect(actor.getSnapshot().value).toBe('draft');
actor.stop();
});
it('should reach pending_payment from draft', () => {
const actor = createActor(orderMachine, {
input: { orderId: 'o1', items: [], total: 0, retryCount: 0 },
}).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
expect(actor.getSnapshot().value).toBe('pending_payment');
actor.stop();
});
it('should reach confirmed after payment', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_123' });
expect(actor.getSnapshot().value).toBe('confirmed');
actor.stop();
});
it('should reach payment_retry on payment failure', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_FAILED', reason: 'insufficient_funds' });
expect(actor.getSnapshot().value).toBe('payment_retry');
actor.stop();
});
it('should reach shipped after confirmation', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_123' });
actor.send({ type: 'SHIP', shippingId: 'ship_456' });
expect(actor.getSnapshot().value).toBe('shipped');
actor.stop();
});
it('should reach delivered after shipping', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_123' });
actor.send({ type: 'SHIP', shippingId: 'ship_456' });
actor.send({ type: 'DELIVER' });
expect(actor.getSnapshot().value).toBe('delivered');
actor.stop();
});
it('should reach return_requested from delivered', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_123' });
actor.send({ type: 'SHIP', shippingId: 'ship_456' });
actor.send({ type: 'DELIVER' });
actor.send({ type: 'REQUEST_RETURN' });
expect(actor.getSnapshot().value).toBe('return_requested');
actor.stop();
});
it('should reach refunding from return approval', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_123' });
actor.send({ type: 'SHIP', shippingId: 'ship_456' });
actor.send({ type: 'DELIVER' });
actor.send({ type: 'REQUEST_RETURN' });
actor.send({ type: 'APPROVE_RETURN' });
expect(actor.getSnapshot().value).toBe('refunding');
actor.stop();
});
it('should reach refunded as final state', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_123' });
actor.send({ type: 'SHIP', shippingId: 'ship_456' });
actor.send({ type: 'DELIVER' });
actor.send({ type: 'REQUEST_RETURN' });
actor.send({ type: 'APPROVE_RETURN' });
actor.send({ type: 'REFUND_PROCESSED', amount: 100 });
expect(actor.getSnapshot().value).toBe('refunded');
actor.stop();
});
it('should reach cancelled from draft', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'CANCEL', reason: 'changed mind' });
expect(actor.getSnapshot().value).toBe('cancelled');
actor.stop();
});
});Guards are the gatekeepers of transitions. They must be tested at their boundaries.
// guard-conditions.test.ts
describe('Order Machine -- Guard Conditions', () => {
describe('SUBMIT guard: items.length > 0', () => {
it('should block SUBMIT when cart is empty', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'SUBMIT' });
// Should remain in draft -- guard blocked the transition
expect(actor.getSnapshot().value).toBe('draft');
actor.stop();
});
it('should allow SUBMIT when cart has one item', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 50 } });
actor.send({ type: 'SUBMIT' });
expect(actor.getSnapshot().value).toBe('pending_payment');
actor.stop();
});
it('should allow SUBMIT when cart has multiple items', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 50 } });
actor.send({ type: 'ADD_ITEM', item: { id: 'i2', quantity: 2, price: 30 } });
actor.send({ type: 'SUBMIT' });
expect(actor.getSnapshot().value).toBe('pending_payment');
expect(actor.getSnapshot().context.total).toBe(110);
actor.stop();
});
it('should block SUBMIT after removing all items', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 50 } });
actor.send({ type: 'REMOVE_ITEM', itemId: 'i1' });
actor.send({ type: 'SUBMIT' });
expect(actor.getSnapshot().value).toBe('draft');
expect(actor.getSnapshot().context.items).toHaveLength(0);
actor.stop();
});
});
describe('PAYMENT_FAILED guard: retryCount < 3', () => {
it('should allow retry on first failure', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_FAILED', reason: 'timeout' });
expect(actor.getSnapshot().value).toBe('payment_retry');
expect(actor.getSnapshot().context.retryCount).toBe(1);
actor.stop();
});
it('should allow retry up to 3 times', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
// First failure and retry
actor.send({ type: 'PAYMENT_FAILED', reason: 'timeout' });
actor.send({ type: 'RETRY_PAYMENT' });
// Second failure and retry
actor.send({ type: 'PAYMENT_FAILED', reason: 'timeout' });
actor.send({ type: 'RETRY_PAYMENT' });
// Third failure and retry
actor.send({ type: 'PAYMENT_FAILED', reason: 'timeout' });
expect(actor.getSnapshot().value).toBe('payment_retry');
expect(actor.getSnapshot().context.retryCount).toBe(3);
actor.stop();
});
it('should cancel order after exhausting retries', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
// Exhaust all retries
for (let i = 0; i < 3; i++) {
actor.send({ type: 'PAYMENT_FAILED', reason: 'timeout' });
if (i < 2) actor.send({ type: 'RETRY_PAYMENT' });
}
// Fourth failure should cancel
actor.send({ type: 'RETRY_PAYMENT' });
actor.send({ type: 'PAYMENT_FAILED', reason: 'final_failure' });
expect(actor.getSnapshot().value).toBe('cancelled');
actor.stop();
});
});
});// invalid-transitions.test.ts
describe('Order Machine -- Invalid Transitions', () => {
it('should not allow SHIP from draft state', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'SHIP', shippingId: 'ship_1' });
expect(actor.getSnapshot().value).toBe('draft');
actor.stop();
});
it('should not allow DELIVER from pending_payment state', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'DELIVER' });
expect(actor.getSnapshot().value).toBe('pending_payment');
actor.stop();
});
it('should not allow PAYMENT_RECEIVED from confirmed state', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_1' });
// Try to receive payment again
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_2' });
expect(actor.getSnapshot().value).toBe('confirmed');
expect(actor.getSnapshot().context.paymentId).toBe('pay_1');
actor.stop();
});
it('should not allow REQUEST_RETURN from shipped state', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_1' });
actor.send({ type: 'SHIP', shippingId: 'ship_1' });
actor.send({ type: 'REQUEST_RETURN' });
expect(actor.getSnapshot().value).toBe('shipped');
actor.stop();
});
it('should not process any events in final cancelled state', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'CANCEL', reason: 'test' });
expect(actor.getSnapshot().value).toBe('cancelled');
// Try every event type
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_1' });
actor.send({ type: 'SHIP', shippingId: 'ship_1' });
expect(actor.getSnapshot().value).toBe('cancelled');
actor.stop();
});
it('should not process events in final refunded state', () => {
const actor = createActor(orderMachine).start();
actor.send({ type: 'ADD_ITEM', item: { id: 'i1', quantity: 1, price: 100 } });
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'PAYMENT_RECEIVED', paymentId: 'pay_1' });
actor.send({ type: 'SHIP', shippingId: 'ship_1' });
actor.send({ type: 'DELIVER' });
actor.send({ type: 'REQUEST_RETURN' });
actor.send({ type: 'APPROVE_RETURN' });
actor.send({ type: 'REFUND_PROCESSED', amount: 100 });
expect(actor.getSnapshot().value).toBe('refunded');
actor.send({ type: 'SHIP', shippingId: 'ship_2' });
expect(actor.getSnapshot().value).toBe('refunded');
actor.stop();
});
});// entry-exit-actions.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createMachine, createActor } from 'xstate';
describe('Entry and Exit Actions', () => {
const onEnterConfirmed = vi.fn();
const onExitDraft = vi.fn();
const onEnterShipped = vi.fn();
const onExitShipped = vi.fn();
const sendConfirmationEmail = vi.fn();
const sendShippingNotification = vi.fn();
const machineWithActions = createMachine({
id: 'order-with-actions',
initial: 'draft',
context: { items: [] as string[] },
states: {
draft: {
exit: () => onExitDraft(),
on: {
SUBMIT: 'confirmed',
},
},
confirmed: {
entry: [
() => onEnterConfirmed(),
() => sendConfirmationEmail(),
],
on: {
SHIP: 'shipped',
},
},
shipped: {
entry: [
() => onEnterShipped(),
() => sendShippingNotification(),
],
exit: () => onExitShipped(),
on: {
DELIVER: 'delivered',
},
},
delivered: {
type: 'final',
},
},
});
beforeEach(() => {
vi.clearAllMocks();
});
it('should execute exit action when leaving draft', () => {
const actor = createActor(machineWithActions).start();
actor.send({ type: 'SUBMIT' });
expect(onExitDraft).toHaveBeenCalledTimes(1);
actor.stop();
});
it('should execute entry actions when entering confirmed', () => {
const actor = createActor(machineWithActions).start();
actor.send({ type: 'SUBMIT' });
expect(onEnterConfirmed).toHaveBeenCalledTimes(1);
expect(sendConfirmationEmail).toHaveBeenCalledTimes(1);
actor.stop();
});
it('should execute actions in order: exit old, enter new', () => {
const callOrder: string[] = [];
onExitDraft.mockImplementation(() => callOrder.push('exit_draft'));
onEnterConfirmed.mockImplementation(() => callOrder.push('enter_confirmed'));
const actor = createActor(machineWithActions).start();
actor.send({ type: 'SUBMIT' });
expect(callOrder).toEqual(['exit_draft', 'enter_confirmed']);
actor.stop();
});
it('should execute exit and entry actions during transition through shipped', () => {
const actor = createActor(machineWithActions).start();
actor.send({ type: 'SUBMIT' });
actor.send({ type: 'SHIP' });
expect(onEnterShipped).toHaveBeenCalledTimes(1);
expect(sendShippingNotification).toHaveBeenCalledTimes(1);
actor.send({ type: 'DELIVER' });
expect(onExitShipped).toHaveBeenCalledTimes(1);
actor.stop();
});
it('should not execute entry actions if transition is blocked', () => {
// If we send an invalid event, no actions should fire
const actor = createActor(machineWithActions).start();
actor.send({ type: 'SHIP' }); // Invalid from draft
expect(onExitDraft).not.toHaveBeenCalled();
expect(onEnterShipped).not.toHaveBeenCalled();
actor.stop();
});
});// payment-machine.ts
const paymentMachine = createMachine({
id: 'payment',
initial: 'idle',
context: {
amount: 0,
currency: 'USD',
attempts: 0,
lastError: null as string | null,
transactionId: null as string | null,
},
states: {
idle: {
on: {
INITIATE: {
target: 'processing',
guard: ({ event }) => event.amount > 0,
actions: assign({ amount: ({ event }) => event.amount }),
},
},
},
processing: {
entry: assign({ attempts: ({ context }) => context.attempts + 1 }),
on: {
SUCCESS: {
target: 'authorized',
actions: assign({ transactionId: ({ event }) => event.transactionId }),
},
FAILURE: [
{ target: 'retrying', guard: ({ context }) => context.attempts < 3 },
{ target: 'failed' },
],
TIMEOUT: [
{ target: 'retrying', guard: ({ context }) => context.attempts < 3 },
{ target: 'failed' },
],
},
},
retrying: {
after: {
2000: 'processing', // Auto-retry after 2 seconds
},
on: {
CANCEL: 'cancelled',
},
},
authorized: {
on: {
CAPTURE: 'captured',
VOID: 'voided',
},
},
captured: {
on: {
REFUND: 'refunding',
},
},
refunding: {
on: {
REFUND_COMPLETE: 'refunded',
REFUND_FAILED: 'captured', // Back to captured if refund fails
},
},
refunded: { type: 'final' },
voided: { type: 'final' },
failed: { type: 'final' },
cancelled: { type: 'final' },
},
});// machine-test.config.ts
interface StateMachineTestConfig {
coverage: {
requireAllStatesCoverage: boolean;
requireAllTransitionsCoverage: boolean;
minimumStateCoverage: number; // percentage
minimumTransitionCoverage: number; // percentage
};
generation: {
maxPathLength: number;
maxPathsPerState: number;
includeInvalidTransitions: boolean;
includeGuardBoundaries: boolean;
};
reporting: {
generateTransitionMatrix: boolean;
generateStateDiagram: boolean;
outputDirectory: string;
format: 'json' | 'markdown' | 'html';
};
timeouts: {
stateTransitionMs: number;
asyncActionMs: number;
delayedTransitionMs: number;
};
}
const defaultConfig: StateMachineTestConfig = {
coverage: {
requireAllStatesCoverage: true,
requireAllTransitionsCoverage: true,
minimumStateCoverage: 100,
minimumTransitionCoverage: 90,
},
generation: {
maxPathLength: 20,
maxPathsPerState: 5,
includeInvalidTransitions: true,
includeGuardBoundaries: true,
},
reporting: {
generateTransitionMatrix: true,
generateStateDiagram: false,
outputDirectory: './test-artifacts/state-machine',
format: 'markdown',
},
timeouts: {
stateTransitionMs: 5000,
asyncActionMs: 10000,
delayedTransitionMs: 15000,
},
};retryCount < 3, test with retryCount of 2 (should pass) and 3 (should fail). Boundary testing on guards catches off-by-one errors that cause the most subtle state machine bugs.<= instead of <) and type coercion issues ("3" < 3 evaluating differently than expected) are the usual culprits.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.