diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs new file mode 100644 index 000000000..1c26eed0d --- /dev/null +++ b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs @@ -0,0 +1,291 @@ +using Elsa.Testing.Shared; +using Elsa.Workflows; +using Elsa.Workflows.Activities.Flowchart.Activities; +using Elsa.Workflows.Activities.Flowchart.Models; + +namespace Elsa.Activities.UnitTests.Branching; + +/// +/// Unit tests for FlowJoin activity covering both token flow and counter flow modes. +/// +public class FlowJoinTests +{ + [Theory] + [InlineData(FlowJoinMode.WaitAny)] + [InlineData(FlowJoinMode.WaitAll)] + public async Task Should_Complete_Activity_In_Token_Flow_Mode(FlowJoinMode joinMode) + { + // Arrange + var originalValue = Flowchart.UseTokenFlow; + Flowchart.UseTokenFlow = true; + + try + { + var flowJoin = new FlowJoin { Mode = new(joinMode) }; + + // Act + var context = await ExecuteAsync(flowJoin); + + // Assert + Assert.True(context.IsCompleted); + } + finally + { + Flowchart.UseTokenFlow = originalValue; + } + } + + [Theory] + [InlineData(FlowJoinMode.WaitAny)] + [InlineData(FlowJoinMode.WaitAll)] + public async Task Should_Complete_Activity_In_Counter_Flow_Mode(FlowJoinMode joinMode) + { + // Arrange + var originalValue = Flowchart.UseTokenFlow; + Flowchart.UseTokenFlow = false; + + try + { + var flowJoin = new FlowJoin { Mode = new(joinMode) }; + + // Act - Execute without parent context (simple case) + var context = await ExecuteAsync(flowJoin); + + // Assert + Assert.True(context.IsCompleted); + } + finally + { + Flowchart.UseTokenFlow = originalValue; + } + } + + [Fact] + public async Task Should_Execute_In_Flowchart_Context() + { + // Arrange + var originalValue = Flowchart.UseTokenFlow; + Flowchart.UseTokenFlow = false; + + try + { + var flowJoin = new FlowJoin { Mode = new(FlowJoinMode.WaitAny) }; + var flowchart = new Flowchart + { + Start = flowJoin, + Activities = { flowJoin } + }; + + // Act + var context = await ExecuteFlowchartAsync(flowchart); + + // Assert + Assert.NotNull(context); + Assert.True(context.HasScheduledActivity(flowJoin)); + } + finally + { + Flowchart.UseTokenFlow = originalValue; + } + } + + // Comprehensive tests covering all flow mode combinations + [Theory] + [InlineData(true, FlowJoinMode.WaitAny)] + [InlineData(true, FlowJoinMode.WaitAll)] + [InlineData(false, FlowJoinMode.WaitAny)] + [InlineData(false, FlowJoinMode.WaitAll)] + public async Task Should_Complete_In_All_Flow_Mode_Combinations(bool useTokenFlow, FlowJoinMode joinMode) + { + // Arrange + var originalValue = Flowchart.UseTokenFlow; + Flowchart.UseTokenFlow = useTokenFlow; + + try + { + var flowJoin = new FlowJoin { Mode = new(joinMode) }; + + // Act + var context = await ExecuteAsync(flowJoin); + + // Assert + Assert.True(context.IsCompleted); + + // Document expected behavior based on flow mode + if (useTokenFlow) + { + // In token flow mode, FlowJoin acts as a no-op and always completes + // regardless of join mode (WaitAny or WaitAll) + Assert.True(context.IsCompleted, "Token flow mode should always complete"); + } + else + { + // In counter flow mode, behavior depends on join mode and flowchart state + // Without parent context, both WaitAny and WaitAll should complete + Assert.True(context.IsCompleted, "Counter flow mode should complete when no parent context"); + } + } + finally + { + Flowchart.UseTokenFlow = originalValue; + } + } + + [Theory] + [InlineData(true, FlowJoinMode.WaitAny)] + [InlineData(true, FlowJoinMode.WaitAll)] + [InlineData(false, FlowJoinMode.WaitAny)] + [InlineData(false, FlowJoinMode.WaitAll)] + public async Task Should_Execute_In_Flowchart_Context_For_All_Combinations(bool useTokenFlow, FlowJoinMode joinMode) + { + // Test all combinations within a flowchart context + // Arrange + var originalValue = Flowchart.UseTokenFlow; + Flowchart.UseTokenFlow = useTokenFlow; + + try + { + var flowJoin = new FlowJoin { Mode = new(joinMode) }; + var flowchart = new Flowchart + { + Start = flowJoin, + Activities = { flowJoin } + }; + + // Act + var context = await ExecuteFlowchartAsync(flowchart); + + // Assert + Assert.NotNull(context); + + if (useTokenFlow) + { + // In token flow mode, the join should be scheduled regardless of join mode + Assert.True(context.HasScheduledActivity(flowJoin), + $"Token flow mode with {joinMode} should schedule the join activity"); + } + else + { + // In counter flow mode, scheduling depends on join mode and flowchart state + // Since this is a simple flowchart with just the join as start activity, + // it should be scheduled + Assert.True(context.HasScheduledActivity(flowJoin), + $"Counter flow mode with {joinMode} should schedule the join activity as start"); + } + } + finally + { + Flowchart.UseTokenFlow = originalValue; + } + } + + [Theory] + [InlineData(true, FlowJoinMode.WaitAny)] + [InlineData(true, FlowJoinMode.WaitAll)] + [InlineData(false, FlowJoinMode.WaitAny)] + [InlineData(false, FlowJoinMode.WaitAll)] + public async Task Should_Handle_Complex_Flowchart_Scenarios(bool useTokenFlow, FlowJoinMode joinMode) + { + // Test with a more complex flowchart that has multiple activities + // Arrange + var originalValue = Flowchart.UseTokenFlow; + Flowchart.UseTokenFlow = useTokenFlow; + + try + { + var startActivity = new WriteLine("Start"); + var flowJoin = new FlowJoin { Mode = new(joinMode) }; + var afterJoin = new WriteLine("AfterJoin"); + + var flowchart = new Flowchart + { + Start = startActivity, + Activities = { startActivity, flowJoin, afterJoin }, + Connections = + { + new() { Source = new(startActivity, "Done"), Target = new(flowJoin) }, + new() { Source = new(flowJoin, "Done"), Target = new(afterJoin) } + } + }; + + // Act + var context = await ExecuteFlowchartAsync(flowchart); + + // Assert + Assert.NotNull(context); + + // The start activity should always be scheduled first + Assert.True(context.HasScheduledActivity(startActivity), + "Start activity should be scheduled"); + + // Document expected behavior for different combinations + if (useTokenFlow) + { + // In token flow mode, both WaitAny and WaitAll act as no-ops + // The exact scheduling depends on the flowchart execution engine + } + else + { + // In counter flow mode: + // - WaitAny should allow continuation after first inbound connection + // - WaitAll should wait for all inbound connections (in this case just one) + // Since there's only one inbound connection, both should behave similarly + } + } + finally + { + Flowchart.UseTokenFlow = originalValue; + } + } + + [Fact] + public async Task Should_Demonstrate_Token_vs_Counter_Flow_Differences() + { + // This test demonstrates the key differences between token and counter flow modes + var joinMode = FlowJoinMode.WaitAll; // Use WaitAll to highlight differences + + // Test Token Flow Mode + var originalValue = Flowchart.UseTokenFlow; + Flowchart.UseTokenFlow = true; + + try + { + var tokenFlowJoin = new FlowJoin { Mode = new(joinMode) }; + var tokenContext = await ExecuteAsync(tokenFlowJoin); + + // Token flow: FlowJoin acts as no-op, always completes + Assert.True(tokenContext.IsCompleted, "Token flow should always complete"); + } + finally + { + Flowchart.UseTokenFlow = originalValue; + } + + // Test Counter Flow Mode + Flowchart.UseTokenFlow = false; + + try + { + var counterFlowJoin = new FlowJoin { Mode = new(joinMode) }; + var counterContext = await ExecuteAsync(counterFlowJoin); + + // Counter flow: Behavior depends on CanWaitAllProceed for WaitAll mode + // Without parent context, it should complete + Assert.True(counterContext.IsCompleted, "Counter flow should complete without parent context"); + } + finally + { + Flowchart.UseTokenFlow = originalValue; + } + } + + private static async Task ExecuteAsync(IActivity activity) + { + return await new ActivityTestFixture(activity).ExecuteAsync(); + } + + private static async Task ExecuteFlowchartAsync(Flowchart flowchart) + { + return await new ActivityTestFixture(flowchart).ExecuteAsync(); + } +}