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();
+ }
+}