diff --git a/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs b/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs
new file mode 100644
index 000000000..21778bf96
--- /dev/null
+++ b/test/unit/Elsa.Activities.UnitTests/Flow/ForkTests.cs
@@ -0,0 +1,375 @@
+using Elsa.Testing.Shared;
+using Elsa.Workflows;
+using Elsa.Workflows.Signals;
+
+namespace Elsa.Activities.UnitTests.Flow;
+
+///
+/// Unit tests for the Fork activity covering various join modes, branching scenarios, and signal handling.
+///
+public class ForkTests
+{
+ [Fact]
+ public async Task SchedulesAllBranches()
+ {
+ // Arrange
+ var branches = CreateBranches(3);
+
+ var fork = new Fork
+ {
+ JoinMode = ForkJoinMode.WaitAll,
+ Branches = branches.Cast().ToList()
+ };
+
+ // Act
+ var context = await ExecuteForkAsync(fork);
+
+ // Assert
+ foreach (var branch in branches)
+ {
+ Assert.True(context.HasScheduledActivity(branch), $"{branch.Id} should be scheduled");
+ }
+ }
+
+ [Fact]
+ public async Task ExecutesWithNoBranches()
+ {
+ // Arrange
+ var fork = new Fork
+ {
+ JoinMode = ForkJoinMode.WaitAll
+ };
+
+ // Act
+ var context = await ExecuteForkAsync(fork);
+
+ // Assert
+ Assert.NotNull(context);
+ }
+
+ [Theory]
+ [InlineData(ForkJoinMode.WaitAll)]
+ [InlineData(ForkJoinMode.WaitAny)]
+ public async Task ExecutesWithSpecifiedJoinMode(ForkJoinMode joinMode)
+ {
+ // Arrange
+ var branch = new WriteLine("Test Branch");
+ var fork = new Fork
+ {
+ JoinMode = joinMode,
+ Branches =
+ {
+ branch
+ }
+ };
+
+ // Act
+ var context = await ExecuteForkAsync(fork);
+
+ // Assert
+ Assert.Equal(joinMode, fork.JoinMode);
+ Assert.True(context.HasScheduledActivity(branch));
+ }
+
+ [Theory]
+ [InlineData(ForkJoinMode.WaitAll, 1, 1)] // WaitAll: complete 1 of 2, should track 1
+ [InlineData(ForkJoinMode.WaitAll, 2, 2)] // WaitAll: complete 2 of 2, should track 2
+ [InlineData(ForkJoinMode.WaitAny, 1, 1)] // WaitAny: complete 1 of 2, should track 1
+ public async Task CompletionTrackingWithJoinModes(ForkJoinMode joinMode, int branchesToComplete, int expectedCompleted)
+ {
+ // Arrange
+ var branches = CreateBranches(2);
+ var fork = new Fork
+ {
+ JoinMode = joinMode,
+ Branches = branches.Cast().ToList()
+ };
+ var context = await ExecuteForkAsync(fork);
+
+ // Act
+ HashSet completedSet = null!;
+ for (var i = 0; i < branchesToComplete; i++)
+ {
+ completedSet = await CompleteBranchAsync(fork, context, branches[i]);
+ }
+
+ // Assert
+ Assert.NotNull(completedSet);
+ Assert.Equal(expectedCompleted, completedSet.Count);
+ for (var i = 0; i < branchesToComplete; i++)
+ {
+ Assert.Contains(branches[i].Id, completedSet);
+ }
+
+ for (var i = branchesToComplete; i < branches.Length; i++)
+ {
+ Assert.DoesNotContain(branches[i].Id, completedSet);
+ }
+ }
+
+ [Fact]
+ public async Task HandlesBreakSignal()
+ {
+ // Arrange
+ var fork = new Fork
+ {
+ Branches =
+ {
+ new WriteLine("Branch")
+ }
+ };
+ var context = await ExecuteForkAsync(fork);
+
+ // Act
+ var breakSignal = new BreakSignal();
+ var signalContext = new SignalContext(context, context, CancellationToken.None);
+ await InvokePrivateMethodAsync(fork, "OnBreakSignalReceived", breakSignal, signalContext);
+
+ // Assert
+ Assert.True(context.GetIsBreaking());
+ }
+
+ [Fact]
+ public async Task CompletesImmediatelyWhenBreaking()
+ {
+ // Arrange
+ var branch = new WriteLine("Branch");
+ var fork = new Fork
+ {
+ Branches =
+ {
+ branch
+ }
+ };
+ var context = await ExecuteForkAsync(fork);
+ context.SetIsBreaking();
+
+ // Act
+ await CompleteBranchAsync(fork, context, branch);
+
+ // Assert
+ Assert.True(context.GetIsBreaking());
+ }
+
+ [Fact]
+ public async Task SingleBranchCompletesCorrectly()
+ {
+ // Arrange
+ var branch = new WriteLine("Single Branch");
+ var fork = new Fork
+ {
+ JoinMode = ForkJoinMode.WaitAll,
+ Branches =
+ {
+ branch
+ }
+ };
+ var context = await ExecuteForkAsync(fork);
+
+ // Act
+ var completedSet = await CompleteBranchAsync(fork, context, branch);
+
+ // Assert
+ Assert.NotNull(completedSet);
+ Assert.Single(completedSet);
+ Assert.Contains(branch.Id, completedSet);
+ }
+
+ [Theory]
+ [InlineData(ForkJoinMode.WaitAll, 3, 3)] // WaitAll requires all 3 branches to complete
+ [InlineData(ForkJoinMode.WaitAny, 3, 1)] // WaitAny only needs 1 branch to complete
+ public async Task HandlesMultipleBranchesWithJoinModes(ForkJoinMode joinMode, int totalBranches, int branchesToComplete)
+ {
+ // Arrange
+ var branches = CreateBranches(totalBranches);
+ var fork = new Fork
+ {
+ JoinMode = joinMode,
+ Branches = branches.Cast().ToList()
+ };
+ var context = await ExecuteForkAsync(fork);
+
+ // Act
+ HashSet completedSet = null!;
+ for (var i = 0; i < branchesToComplete; i++)
+ {
+ completedSet = await CompleteBranchAsync(fork, context, branches[i]);
+ }
+
+ // Assert
+ Assert.NotNull(completedSet);
+ Assert.Equal(branchesToComplete, completedSet.Count);
+ for (var i = 0; i < branchesToComplete; i++)
+ {
+ Assert.Contains(branches[i].Id, completedSet);
+ }
+ }
+
+ [Fact]
+ public async Task TracksAllActivityCompletions() // Including non-branch activities
+ {
+ // Arrange
+ var branch = new WriteLine("Valid Branch")
+ {
+ Id = "branch-activity"
+ };
+ var nonBranch = new WriteLine("Non-Branch Activity")
+ {
+ Id = "non-branch-activity"
+ };
+ var fork = new Fork
+ {
+ JoinMode = ForkJoinMode.WaitAll,
+ Branches =
+ {
+ branch
+ }
+ };
+ var context = await ExecuteForkAsync(fork);
+
+ // Act - complete a non-branch activity
+ var completedSet = await CompleteBranchAsync(fork, context, nonBranch);
+
+ // Assert - Fork tracks ALL completions, even non-branch activities
+ Assert.NotNull(completedSet);
+ Assert.Contains(nonBranch.Id, completedSet);
+ Assert.Single(completedSet);
+ }
+
+ [Fact]
+ public async Task JoinLogicOnlyConsidersBranchActivities()
+ {
+ // Arrange
+ var branch = new WriteLine("Valid Branch")
+ {
+ Id = "branch-activity"
+ };
+ var nonBranch = new WriteLine("Non-Branch Activity")
+ {
+ Id = "non-branch-activity"
+ };
+ var fork = new Fork
+ {
+ JoinMode = ForkJoinMode.WaitAll,
+ Branches =
+ {
+ branch
+ }
+ };
+ var context = await ExecuteForkAsync(fork);
+
+ // Act - complete non-branch activity first, then branch activity
+ await CompleteBranchAsync(fork, context, nonBranch);
+ var completedSet = await CompleteBranchAsync(fork, context, branch);
+
+ // Assert - both activities should be tracked
+ Assert.NotNull(completedSet);
+ Assert.Contains(branch.Id, completedSet);
+ Assert.Contains(nonBranch.Id, completedSet);
+ Assert.Equal(2, completedSet.Count);
+ }
+
+ [Fact]
+ public async Task MaintainsSeparateCompletionTracking()
+ {
+ // Arrange
+ var branch1 = new WriteLine("Branch 1")
+ {
+ Id = "branch1-unique"
+ };
+ var branch2 = new WriteLine("Branch 2")
+ {
+ Id = "branch2-unique"
+ };
+ var fork1 = new Fork
+ {
+ Branches =
+ {
+ branch1
+ }
+ };
+ var fork2 = new Fork
+ {
+ Branches =
+ {
+ branch2
+ }
+ };
+ var context1 = await ExecuteForkAsync(fork1);
+ var context2 = await ExecuteForkAsync(fork2);
+
+ // Act
+ var completed1 = await CompleteBranchAsync(fork1, context1, branch1);
+ var completed2 = await CompleteBranchAsync(fork2, context2, branch2);
+
+ // Assert - each fork should track its own completions
+ Assert.NotNull(completed1);
+ Assert.NotNull(completed2);
+ Assert.Contains(branch1.Id, completed1);
+ Assert.DoesNotContain(branch2.Id, completed1);
+ Assert.Contains(branch2.Id, completed2);
+ Assert.DoesNotContain(branch1.Id, completed2);
+ }
+
+ private static async Task ExecuteForkAsync(Fork fork)
+ {
+ var fixture = new ActivityTestFixture(fork);
+ return await fixture.ExecuteAsync();
+ }
+
+ private static WriteLine[] CreateBranches(int count, string namePrefix = "Branch")
+ {
+ return Enumerable
+ .Range(1, count)
+ .Select(i => new WriteLine($"{namePrefix} {i}")
+ {
+ Id = $"{namePrefix.ToLower()}-{i}"
+ })
+ .ToArray();
+ }
+
+ private static async Task CreateChildContextAsync(IActivity childActivity)
+ {
+ var childFixture = new ActivityTestFixture(childActivity);
+ var childContext = await childFixture.ExecuteAsync();
+ return childContext;
+ }
+
+ private static async Task> CompleteBranchAsync(Fork fork, ActivityExecutionContext parentContext, IActivity branch)
+ {
+ var childContext = await CreateChildContextAsync(branch);
+ var completedContext = new ActivityCompletedContext(parentContext, childContext);
+ await InvokeCompleteChildAsync(fork, completedContext);
+ return parentContext.GetProperty>("Completed") ?? new HashSet();
+ }
+
+ private static async Task InvokePrivateMethodAsync(object instance, string methodName, params object[] parameters)
+ {
+ var method = instance.GetType().GetMethod(methodName,
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?? throw new InvalidOperationException($"{methodName} method not found on {instance.GetType().Name} class. This may indicate a breaking change in the implementation.");
+
+ try
+ {
+ var result = method.Invoke(instance, parameters);
+ switch (result)
+ {
+ case ValueTask valueTask:
+ await valueTask;
+ break;
+ case Task task:
+ await task;
+ break;
+ }
+ }
+ catch (System.Reflection.TargetInvocationException ex) when (ex.InnerException != null)
+ {
+ throw ex.InnerException;
+ }
+ }
+
+ private static Task InvokeCompleteChildAsync(Fork fork, ActivityCompletedContext completedContext)
+ {
+ return InvokePrivateMethodAsync(fork, "CompleteChildAsync", completedContext);
+ }
+}
\ No newline at end of file