diff --git a/src/Definition/MachineDefinition.php b/src/Definition/MachineDefinition.php index bb88c09..a1264ee 100644 --- a/src/Definition/MachineDefinition.php +++ b/src/Definition/MachineDefinition.php @@ -9,6 +9,7 @@ use Tarfinlabs\EventMachine\ContextManager; use Tarfinlabs\EventMachine\Enums\BehaviorType; use Tarfinlabs\EventMachine\Enums\InternalEvent; +use Tarfinlabs\EventMachine\StateConfigValidator; use Tarfinlabs\EventMachine\Behavior\EventBehavior; use Tarfinlabs\EventMachine\Enums\TransitionProperty; use Tarfinlabs\EventMachine\Enums\StateDefinitionType; @@ -84,6 +85,8 @@ private function __construct( public ?array $scenarios, public string $delimiter = self::STATE_DELIMITER, ) { + StateConfigValidator::validate($config); + $this->scenariosEnabled = isset($this->config['scenarios_enabled']) && $this->config['scenarios_enabled'] === true; $this->shouldPersist = $this->config['should_persist'] ?? $this->shouldPersist; @@ -221,7 +224,7 @@ public function getInitialState(EventBehavior|array|null $event = null): ?State currentStateDefinition: $this->initialStateDefinition, ); - $initialState = $this->getScenarioStateIfAvailable(state: $initialState, eventBehavior: $eventBehavior ?? null); + $initialState = $this->getScenarioStateIfAvailable(state: $initialState, eventBehavior: $event ?? null); $this->initialStateDefinition = $initialState->currentStateDefinition; // Record the internal machine init event. @@ -295,7 +298,7 @@ public function getScenarioStateIfAvailable(State $state, EventBehavior|array|nu } $scenarioStateKey = str_replace($this->id, $this->id.$this->delimiter.$state->context->get('scenarioType'), $state->currentStateDefinition->id); - if ($state->context->has('scenarioType') && isset($this->idMap[$scenarioStateKey])) { + if (isset($this->idMap[$scenarioStateKey]) && $state->context->has('scenarioType')) { return $state->setCurrentStateDefinition(stateDefinition: $this->idMap[$scenarioStateKey]); } @@ -400,9 +403,9 @@ public function setupContextManager(): void * @param string $behaviorDefinition The behavior definition to look up. * @param BehaviorType $behaviorType The type of the behavior (e.g., guard or action). * - * @return callable|null The invokable behavior instance or callable, or null if not found. + * @return callable|\Tarfinlabs\EventMachine\Behavior\InvokableBehavior|null The invokable behavior instance or callable, or null if not found. */ - public function getInvokableBehavior(string $behaviorDefinition, BehaviorType $behaviorType): ?callable + public function getInvokableBehavior(string $behaviorDefinition, BehaviorType $behaviorType): null|callable|InvokableBehavior { // If the guard definition is an invokable GuardBehavior, create a new instance. if (is_subclass_of($behaviorDefinition, InvokableBehavior::class)) { @@ -630,7 +633,7 @@ public function transition( // Get scenario state if exists $newState = $this->getScenarioStateIfAvailable(state: $newState, eventBehavior: $eventBehavior); - if ($targetStateDefinition !== null && $targetStateDefinition?->id !== $newState->currentStateDefinition->id) { + if ($targetStateDefinition !== null && $targetStateDefinition->id !== $newState->currentStateDefinition->id) { $targetStateDefinition = $newState->currentStateDefinition; } @@ -721,7 +724,7 @@ public function runAction( ); if ($actionBehavior instanceof InvokableBehavior) { - $actionBehavior->validateRequiredContext($state->context); + $actionBehavior::validateRequiredContext($state->context); } // Get the number of events in the queue before the action is executed. diff --git a/src/Definition/TransitionDefinition.php b/src/Definition/TransitionDefinition.php index 86d565d..020f011 100644 --- a/src/Definition/TransitionDefinition.php +++ b/src/Definition/TransitionDefinition.php @@ -167,7 +167,7 @@ public function getFirstValidTransitionBranch( $shouldLog = $guardBehavior?->shouldLog ?? false; if ($guardBehavior instanceof GuardBehavior) { - $guardBehavior->validateRequiredContext($state->context); + $guardBehavior::validateRequiredContext($state->context); } // Inject guard behavior parameters diff --git a/src/StateConfigValidator.php b/src/StateConfigValidator.php new file mode 100644 index 0000000..c398c86 --- /dev/null +++ b/src/StateConfigValidator.php @@ -0,0 +1,359 @@ + $stateConfig) { + self::validateStateConfig($stateConfig, $stateName); + } + } + + // Validate root level transitions if they exist + if (isset($config['on'])) { + self::validateTransitionsConfig(transitionsConfig: $config['on'], path: 'root'); + } + } + + /** + * Validates root level configuration. + * + * @throws InvalidArgumentException + */ + private static function validateRootConfig(array $config): void + { + $invalidRootKeys = array_diff( + array_keys($config), + self::ALLOWED_ROOT_KEYS + ); + + if (!empty($invalidRootKeys)) { + throw new InvalidArgumentException( + message: 'Invalid root level configuration keys: '.implode(separator: ', ', array: $invalidRootKeys). + '. Allowed keys are: '.implode(separator: ', ', array: self::ALLOWED_ROOT_KEYS) + ); + } + } + + /** + * Validates a single state's configuration. + * + * @throws InvalidArgumentException + */ + private static function validateStateConfig(?array $stateConfig, string $path): void + { + if ($stateConfig === null) { + return; + } + + // Check for transitions defined outside 'on' + if (isset($stateConfig['@always']) || array_key_exists(key: '@always', array: $stateConfig)) { + throw new InvalidArgumentException( + message: "State '{$path}' has transitions defined directly. ". + "All transitions including '@always' must be defined under the 'on' key." + ); + } + + // Validate state keys + $invalidKeys = array_diff(array_keys($stateConfig), self::ALLOWED_STATE_KEYS); + if (!empty($invalidKeys)) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid keys: ".implode(separator: ', ', array: $invalidKeys). + '. Allowed keys are: '.implode(separator: ', ', array: self::ALLOWED_STATE_KEYS) + ); + } + + // Validate state type if specified + if (isset($stateConfig['type'])) { + self::validateStateType(stateConfig: $stateConfig, path: $path); + } + + // Validate entry/exit actions + self::validateStateActions(stateConfig: $stateConfig, path: $path); + + // Final state validations + if (isset($stateConfig['type']) && $stateConfig['type'] === 'final') { + self::validateFinalState(stateConfig: $stateConfig, path: $path); + } + + // Validate nested states + if (isset($stateConfig['states'])) { + if (!is_array($stateConfig['states'])) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid states configuration. States must be an array." + ); + } + + foreach ($stateConfig['states'] as $childKey => $childState) { + self::validateStateConfig(stateConfig: $childState, path: "{$path}.{$childKey}"); + } + } + + // Validate transitions under 'on' + if (isset($stateConfig['on'])) { + self::validateTransitionsConfig(transitionsConfig: $stateConfig['on'], path: $path); + } + } + + /** + * Validates state type configuration. + * + * @throws InvalidArgumentException + */ + private static function validateStateType(array $stateConfig, string $path): void + { + if (!in_array($stateConfig['type'], haystack: self::VALID_STATE_TYPES, strict: true)) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid type: {$stateConfig['type']}. ". + 'Allowed types are: '.implode(separator: ', ', array: self::VALID_STATE_TYPES) + ); + } + } + + /** + * Validates final state constraints. + * + * @throws InvalidArgumentException + */ + private static function validateFinalState(array $stateConfig, string $path): void + { + if (isset($stateConfig['on'])) { + throw new InvalidArgumentException( + message: "Final state '{$path}' cannot have transitions" + ); + } + + if (isset($stateConfig['states'])) { + throw new InvalidArgumentException( + message: "Final state '{$path}' cannot have child states" + ); + } + } + + /** + * Validates state entry and exit actions. + * + * @throws InvalidArgumentException + */ + private static function validateStateActions(array $stateConfig, string $path): void + { + foreach (['entry', 'exit'] as $actionType) { + if (isset($stateConfig[$actionType])) { + $actions = $stateConfig[$actionType]; + if (!is_string($actions) && !is_array($actions)) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid entry/exit actions configuration. ". + 'Actions must be an array or string.' + ); + } + } + } + } + + /** + * Validates transitions configuration. + * + * @throws InvalidArgumentException + */ + private static function validateTransitionsConfig(mixed $transitionsConfig, string $path): void + { + if (!is_array($transitionsConfig)) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid 'on' definition. 'on' must be an array of transitions." + ); + } + + foreach ($transitionsConfig as $eventName => $transition) { + self::validateTransition(transition: $transition, path: $path, eventName: $eventName); + } + } + + /** + * Validates a single transition configuration. + */ + private static function validateTransition( + mixed $transition, + string $path, + string $eventName + ): void { + if ($transition === null) { + return; + } + + if (is_string($transition)) { + return; + } + + if (!is_array($transition)) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid transition for event '{$eventName}'. ". + 'Transition must be a string (target state) or an array (transition config).' + ); + } + + // If it's an array of conditions (guarded transitions) + if (array_is_list($transition)) { + self::validateGuardedTransitions($transition, $path, $eventName); + foreach ($transition as &$condition) { + self::validateTransitionConfig(transitionConfig: $condition, path: $path, eventName: $eventName); + } + + return; + } + + self::validateTransitionConfig(transitionConfig: $transition, path: $path, eventName: $eventName); + } + + /** + * Validates the configuration of a single transition. + */ + private static function validateTransitionConfig( + array &$transitionConfig, + string $path, + string $eventName + ): void { + // Validate allowed keys + $invalidKeys = array_diff(array_keys($transitionConfig), self::ALLOWED_TRANSITION_KEYS); + if (!empty($invalidKeys)) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid keys in transition config for event '{$eventName}': ". + implode(separator: ', ', array: $invalidKeys). + '. Allowed keys are: '.implode(separator: ', ', array: self::ALLOWED_TRANSITION_KEYS) + ); + } + + // Normalize and validate behaviors + self::validateTransitionBehaviors(transitionConfig: $transitionConfig, path: $path, eventName: $eventName); + } + + /** + * Validates and normalizes transition behaviors (guards, actions, calculators). + */ + private static function validateTransitionBehaviors( + array &$transitionConfig, + string $path, + string $eventName + ): void { + $behaviors = [ + 'guards' => 'Guards', + 'actions' => 'Actions', + 'calculators' => 'Calculators', + ]; + + foreach ($behaviors as $behavior => $label) { + if (isset($transitionConfig[$behavior])) { + try { + $transitionConfig[$behavior] = self::normalizeArrayOrString(value: $transitionConfig[$behavior]); + } catch (InvalidArgumentException) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid {$behavior} configuration for event '{$eventName}'. ". + "{$label} must be an array or string." + ); + } + } + } + } + + /** + * Validates guarded transitions with multiple conditions. + */ + private static function validateGuardedTransitions(array $conditions, string $path, string $eventName): void + { + if (empty($conditions)) { + throw new InvalidArgumentException( + message: "State '{$path}' has empty conditions array for event '{$eventName}'. ". + 'Guarded transitions must have at least one condition.' + ); + } + + foreach ($conditions as $index => $condition) { + if (!is_array($condition)) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid condition in transition for event '{$eventName}'. ". + 'Each condition must be an array with target/guards/actions.' + ); + } + + if (!isset($condition['target'])) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid condition at index {$index} for event '{$eventName}'. ". + 'Each condition must have a target.' + ); + } + + // If this is not the last condition + if (!isset($condition['guards']) && $index !== count($conditions) - 1) { + throw new InvalidArgumentException( + message: "State '{$path}' has invalid conditions order for event '{$eventName}'. ". + 'Default condition (no guards) must be the last condition.' + ); + } + } + } +} diff --git a/tests/StateConfigValidatorTest.php b/tests/StateConfigValidatorTest.php new file mode 100644 index 0000000..6ac52c4 --- /dev/null +++ b/tests/StateConfigValidatorTest.php @@ -0,0 +1,421 @@ + MachineDefinition::define([ + 'id' => 'machine', + 'invalid_key' => 'value', + 'another_invalid' => 'value', + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: 'Invalid root level configuration keys: invalid_key, another_invalid. Allowed keys are: id, version, initial, context, states, on, type, meta, entry, exit, description, scenarios_enabled, should_persist, delimiter' + ); +}); + +test('accepts valid root level configuration', function (): void { + // HINT: This test should contain all possible root level configuration keys + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'version' => '1.0.0', + 'initial' => 'state_a', + 'context' => ['some' => 'data'], + 'scenarios_enabled' => true, + 'should_persist' => true, + 'delimiter' => '.', + 'states' => [ + 'state_a' => [], + ], + ]))->not->toThrow(exception: InvalidArgumentException::class); +}); + +test('accepts machine with root level transitions', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'on' => [ + 'GLOBAL_EVENT' => 'state_b', + ], + 'states' => [ + 'state_a' => [], + 'state_b' => [], + ], + ]))->not->toThrow(InvalidArgumentException::class); +}); + +test('transitions must be defined under the on key', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'check', + 'states' => [ + 'check' => [ + '@always' => [ // Transition defined directly under state + 'target' => 'next', + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'check' has transitions defined directly. All transitions including '@always' must be defined under the 'on' key." + ); +}); + +test('validates on property is an array', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'on' => 'invalid_string', // 'on' should be an array + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid 'on' definition. 'on' must be an array of transitions." + ); +}); + +test('validates transition target is either string or array', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => true, // Invalid transition definition + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid transition for event 'EVENT'. Transition must be a string (target state) or an array (transition config)." + ); +}); + +test('validates condition arrays in transitions', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [ + 'not_an_array', // Invalid condition - should be an array + ['target' => 'state_b'], + ], + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid condition in transition for event 'EVENT'. Each condition must be an array with target/guards/actions." + ); +}); + +test('validates guards configuration in transitions', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [ + 'target' => 'state_b', + 'guards' => true, // Guards should be an array or a string + ], + ], + ], + ], + ]))->toThrow( + InvalidArgumentException::class, + "State 'state_a' has invalid guards configuration for event 'EVENT'. Guards must be an array or string." + ); +}); + +test('validates actions configuration in transitions', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [ + 'target' => 'state_b', + 'actions' => true, // Actions should be an array or string + ], + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid actions configuration for event 'EVENT'. Actions must be an array or string." + ); +}); + +test('validates calculators configuration in transitions', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [ + 'target' => 'state_b', + 'calculators' => 123, // Calculators should be an array or string + ], + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid calculators configuration for event 'EVENT'. Calculators must be an array or string." + ); +}); + +test('validates allowed keys in transition config', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [ + 'target' => 'state_b', + 'invalid_key' => 'value', + 'another_invalid' => 'value', + ], + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid keys in transition config for event 'EVENT': invalid_key, another_invalid. Allowed keys are: target, guards, actions, description, calculators" + ); +}); + +test('validates state type values', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'type' => 'invalid_type', // Type should be 'atomic', 'compound', or 'final' + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid type: invalid_type. Allowed types are: atomic, compound, final" + ); +}); + +test('validates final states have no transitions', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'type' => 'final', + 'on' => [ + 'EVENT' => 'state_b', + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "Final state 'state_a' cannot have transitions" + ); +}); + +test('validates final states have no child states', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'type' => 'final', + 'states' => [ + 'child' => [], + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "Final state 'state_a' cannot have child states" + ); +}); + +test('validates entry and exit actions are arrays or strings', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'initial' => 'state_a', + 'states' => [ + 'state_a' => [ + 'entry' => true, // Should be an array or a string + 'exit' => 123, // Should be an array or a string + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid entry/exit actions configuration. Actions must be an array or string." + ); +}); + +test('accepts valid state configuration with all possible features', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'version' => '1.0.0', + 'initial' => 'state_a', + 'context' => TrafficLightsContext::class, + 'states' => [ + 'state_a' => [ + 'type' => 'compound', + 'initial' => 'child_a', + 'entry' => ['entryAction1', 'entryAction2'], + 'exit' => 'exitAction', + 'meta' => ['some' => 'data'], + 'description' => 'A compound state', + 'on' => [ + 'EVENT' => [ + 'target' => 'state_b', + 'guards' => ['guard1', 'guard2'], + 'actions' => ['action1', 'action2'], + 'calculators' => ['calc1', 'calc2'], + 'description' => 'Transition to state B', + ], + '@always' => [ + [ + 'target' => 'state_b', + 'guards' => 'guard1', + 'description' => 'Always transition when guard passes', + ], + [ + 'target' => 'state_c', + 'description' => 'Default always transition', + ], + ], + ], + 'states' => [ + 'child_a' => [ + 'type' => 'atomic', + ], + 'child_b' => [ + 'type' => 'final', + ], + ], + ], + 'state_b' => [], + 'state_c' => [], + ], + ]))->not->toThrow(exception: InvalidArgumentException::class); +}); + +test('normalizes string behaviors to arrays', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [ + 'target' => 'state_b', + 'guards' => 'singleGuard', + 'actions' => 'singleAction', + 'calculators' => 'singleCalculator', + ], + ], + ], + ], + ]))->not->toThrow(exception: InvalidArgumentException::class); +}); + +test('validates empty guarded transitions array', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [], // Empty conditions array + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has empty conditions array for event 'EVENT'. Guarded transitions must have at least one condition." + ); +}); + +test('validates default condition must be last in guarded transitions', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [ + [ + 'target' => 'state_b', // Default condition (no guards) + ], + [ + 'target' => 'state_c', + 'guards' => 'someGuard', + ], + ], + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid conditions order for event 'EVENT'. Default condition (no guards) must be the last condition." + ); +}); + +test('validates target is required in guarded transitions', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [ + [ + 'guards' => 'someGuard', // Missing target + ], + ], + ], + ], + ], + ]))->toThrow( + exception: InvalidArgumentException::class, + exceptionMessage: "State 'state_a' has invalid condition at index 0 for event 'EVENT'. Each condition must have a target." + ); +}); + +test('accepts valid guarded transitions with multiple conditions', function (): void { + expect(fn () => MachineDefinition::define([ + 'id' => 'machine', + 'states' => [ + 'state_a' => [ + 'on' => [ + 'EVENT' => [ + [ + 'target' => 'state_b', + 'guards' => ['guard1', 'guard2'], + 'actions' => 'action1', + 'calculators' => ['calc1', 'calc2'], + ], + [ + 'target' => 'state_c', + 'guards' => 'guard3', + ], + [ + 'target' => 'state_d', // Default condition + ], + ], + ], + ], + ], + ]))->not->toThrow(exception: InvalidArgumentException::class); +}); diff --git a/tests/StateDefinitionTypeTest.php b/tests/StateDefinitionTypeTest.php index 6a1c8ef..28a5c7e 100644 --- a/tests/StateDefinitionTypeTest.php +++ b/tests/StateDefinitionTypeTest.php @@ -4,12 +4,9 @@ use Illuminate\Support\Carbon; use Tarfinlabs\EventMachine\Actor\Machine; -use Tarfinlabs\EventMachine\ContextManager; -use Tarfinlabs\EventMachine\Behavior\EventBehavior; use Tarfinlabs\EventMachine\Enums\StateDefinitionType; use Tarfinlabs\EventMachine\Definition\MachineDefinition; use Tarfinlabs\EventMachine\Tests\Stubs\Results\GreenResult; -use Tarfinlabs\EventMachine\Exceptions\InvalidFinalStateDefinitionException; test('a state definition can be atomic', function (): void { $machine = MachineDefinition::define(config: [ @@ -73,7 +70,7 @@ ], 'yellow' => [ 'type' => 'final', - 'result' => function (ContextManager $context, EventBehavior $event): Carbon { + 'result' => function (): Carbon { return now(); }, ], @@ -96,44 +93,6 @@ '@green', ]); -test('a final state definition can not have child states', function (): void { - MachineDefinition::define(config: [ - 'initial' => 'yellow', - 'states' => [ - 'yellow' => [ - 'type' => 'final', - 'states' => [ - 'a' => [], - 'b' => [], - ], - ], - ], - ]); -})->throws( - exception: InvalidFinalStateDefinitionException::class, - exceptionMessage: 'The final state `machine.yellow` should not have child states. Please revise your state machine definitions to ensure that final states are correctly configured without child states.' -); - -test('a final state definition can not have transitions', function (): void { - MachineDefinition::define(config: [ - 'initial' => 'yellow', - 'states' => [ - 'yellow' => [ - 'type' => 'final', - 'on' => [ - 'EVENT' => [ - 'target' => 'red', - ], - ], - ], - 'red' => [], - ], - ]); -})->throws( - exception: InvalidFinalStateDefinitionException::class, - exceptionMessage: 'The final state `machine.yellow` should not have transitions. Check your state machine configuration to ensure events are not dispatched when in a final state.' -); - test('an initial state of type final triggers machine finish event', function (): void { $machine = Machine::create(definition: [ 'config' => [