From d0bfcfc23e6bf08a1c68ab64c0a7cd1798e90a5c Mon Sep 17 00:00:00 2001 From: Joost Molenkamp Date: Wed, 11 Mar 2026 10:57:49 +0100 Subject: [PATCH 1/2] test(state-machine): add failing GetInfo test --- test/Stateless.Tests/GetInfoFixture.cs | 32 ++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/test/Stateless.Tests/GetInfoFixture.cs b/test/Stateless.Tests/GetInfoFixture.cs index 570ee914..e4f24250 100644 --- a/test/Stateless.Tests/GetInfoFixture.cs +++ b/test/Stateless.Tests/GetInfoFixture.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; using Xunit; namespace Stateless.Tests @@ -12,16 +13,16 @@ public void GetInfo_should_return_Entry_action_with_trigger_name() var sm = new StateMachine(State.A); sm.Configure(State.B) .OnEntryFrom(Trigger.X, () => { }); - + // ACT var stateMachineInfo = sm.GetInfo(); - + // ASSERT var stateInfo = Assert.Single(stateMachineInfo.States); var entryActionInfo = Assert.Single(stateInfo.EntryActions); Assert.Equal(Trigger.X.ToString(), entryActionInfo.FromTrigger); } - + [Fact] public void GetInfo_should_return_async_Entry_action_with_trigger_name() { @@ -29,14 +30,31 @@ public void GetInfo_should_return_async_Entry_action_with_trigger_name() var sm = new StateMachine(State.A); sm.Configure(State.B) .OnEntryFromAsync(Trigger.X, () => Task.CompletedTask); - + // ACT var stateMachineInfo = sm.GetInfo(); - + // ASSERT var stateInfo = Assert.Single(stateMachineInfo.States); var entryActionInfo = Assert.Single(stateInfo.EntryActions); Assert.Equal(Trigger.X.ToString(), entryActionInfo.FromTrigger); } + + [Fact] + public void GetInfo_should_include_async_Trigger() + { + // ARRANGE + var sm = new StateMachine(State.A); + sm.Configure(State.A) + .PermitIfAsync(Trigger.X, State.B, () => Task.FromResult(true)); + + // ACT + var stateMachineInfo = sm.GetInfo(); + + // ASSERT + var stateInfo = Assert.Single(stateMachineInfo.States.Where(x => x.UnderlyingState.Equals(State.A))); + var transitionInfo = Assert.Single(stateInfo.Transitions); + Assert.Equal(Trigger.X, transitionInfo.Trigger.UnderlyingTrigger); + } } -} \ No newline at end of file +} From 974b848eeb57d8cfb16d0ee7b0c5b31b1e978dc9 Mon Sep 17 00:00:00 2001 From: Joost Molenkamp Date: Wed, 11 Mar 2026 11:21:30 +0100 Subject: [PATCH 2/2] fix(state-machine-info): include async triggers when getting state info --- src/Stateless/Reflection/FixedTransitionInfo.cs | 12 ++++++++++++ src/Stateless/Reflection/StateInfo.cs | 16 ++++++++++++++++ src/Stateless/StateMachine.cs | 2 ++ 3 files changed, 30 insertions(+) diff --git a/src/Stateless/Reflection/FixedTransitionInfo.cs b/src/Stateless/Reflection/FixedTransitionInfo.cs index acd46896..32ea5222 100644 --- a/src/Stateless/Reflection/FixedTransitionInfo.cs +++ b/src/Stateless/Reflection/FixedTransitionInfo.cs @@ -20,6 +20,18 @@ internal static FixedTransitionInfo Create(StateMachine(StateMachine.TriggerBehaviourAsync behaviour, StateInfo destinationStateInfo) + { + return new FixedTransitionInfo + { + Trigger = new TriggerInfo(behaviour.Trigger), + DestinationState = destinationStateInfo, + GuardConditionsMethodDescriptions = behaviour.Guard == null + ? Array.Empty() : behaviour.Guard.Conditions.Select(c => c.MethodDescription), + IsInternalTransition = behaviour is StateMachine.InternalTriggerBehaviour + }; + } + private FixedTransitionInfo() { } /// diff --git a/src/Stateless/Reflection/StateInfo.cs b/src/Stateless/Reflection/StateInfo.cs index 0f1c1ed8..b4df4429 100644 --- a/src/Stateless/Reflection/StateInfo.cs +++ b/src/Stateless/Reflection/StateInfo.cs @@ -48,6 +48,7 @@ internal static void AddRelationships(StateInfo info, StateMac var fixedTransitions = new List(); var dynamicTransitions = new List(); + // Sync triggers foreach (var triggerBehaviours in stateRepresentation.TriggerBehaviours) { // First add all the deterministic transitions @@ -78,6 +79,21 @@ internal static void AddRelationships(StateInfo info, StateMac } } + // Async triggers + foreach (var triggerBehaviour in stateRepresentation.TriggerBehavioursAsync.Values.SelectMany(x => x)) + { + if (triggerBehaviour is StateMachine.TransitioningTriggerBehaviourAsync transitioningTriggerBehaviourAsync) + { + var destinationInfo = lookupState(transitioningTriggerBehaviourAsync.Destination); + fixedTransitions.Add(FixedTransitionInfo.Create(transitioningTriggerBehaviourAsync, destinationInfo)); + } + else if (triggerBehaviour is StateMachine.ReentryTriggerBehaviourAsync reentryTriggerBehaviourAsync) + { + var destinationInfo = lookupState(reentryTriggerBehaviourAsync.Destination); + fixedTransitions.Add(FixedTransitionInfo.Create(reentryTriggerBehaviourAsync, destinationInfo)); + } + } + info.AddRelationships(superstate, substates, fixedTransitions, dynamicTransitions); } diff --git a/src/Stateless/StateMachine.cs b/src/Stateless/StateMachine.cs index 999297e2..0d11fae9 100644 --- a/src/Stateless/StateMachine.cs +++ b/src/Stateless/StateMachine.cs @@ -163,6 +163,8 @@ public StateMachineInfo GetInfo() var behaviours = _stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehaviours.SelectMany(b => b.Value.OfType().Select(tb => tb.Destination))).ToList(); behaviours.AddRange(_stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehaviours.SelectMany(b => b.Value.OfType().Select(tb => tb.Destination))).ToList()); + behaviours.AddRange(_stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehavioursAsync.SelectMany(b => b.Value.OfType().Select(tb => tb.Destination))).ToList()); + behaviours.AddRange(_stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehavioursAsync.SelectMany(b => b.Value.OfType().Select(tb => tb.Destination))).ToList()); var reachable = behaviours .Distinct()