From 7af4375f3a4999a0956e371132f993ed0012e89c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 2 Mar 2026 15:31:20 +0000
Subject: [PATCH 1/2] Initial plan
From 5167c654b67597ecbaba494832c8b16fd3b05c2e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 2 Mar 2026 15:46:18 +0000
Subject: [PATCH 2/2] Add integration test for WaitAny join with blocking
branch (schedule-before-cancel regression test)
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
---
.../Flow/FlowchartCounterBasedTests.cs | 64 +++++++++++++++++++
1 file changed, 64 insertions(+)
diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs
index fbcc4094c8..c589343af7 100644
--- a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs
+++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartCounterBasedTests.cs
@@ -1,4 +1,5 @@
using Elsa.Testing.Shared;
+using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Elsa.Workflows.Activities.Flowchart.Models;
@@ -341,4 +342,67 @@ public async Task HandlesUnconnectedActivities()
Assert.Equal("Connected", _output.Lines.ElementAt(0));
Assert.DoesNotContain("Unconnected", _output.Lines);
}
+
+ ///
+ /// Regression test for the ordering fix in :
+ /// the outbound activity must be scheduled before remaining inbound branches are canceled.
+ /// Previously, canceling first could trigger CompleteIfNoPendingWorkAsync while the outbound
+ /// activity was not yet in the scheduler, causing the flowchart to finish prematurely without running
+ /// the activity downstream of the join.
+ ///
+ [Fact(DisplayName = "WaitAny join schedules outbound before canceling blocked branch, preventing premature completion")]
+ public async Task WaitAnyJoin_SchedulesOutboundBeforeCancelingBlockedBranch()
+ {
+ // Arrange
+ // Flowchart structure:
+ // Start
+ // ├─► Branch1 (fast) ─► Join (WaitAny) ─► AfterJoin
+ // └─► Branch2 (blocking, creates a bookmark) ─►┘
+ //
+ // Because the scheduler is LIFO, Branch2 executes first and suspends with a bookmark.
+ // Branch1 then completes and triggers the WaitAny join.
+ // The join must schedule AfterJoin before canceling Branch2; otherwise
+ // CompleteIfNoPendingWorkAsync fires while AfterJoin is not yet queued,
+ // completing the flowchart prematurely without executing AfterJoin.
+ var start = new WriteLine("Start");
+ var branch1 = new WriteLine("Branch1");
+ var branch2 = new BlockingActivity { Id = "BlockingBranch" };
+ var join = new FlowJoin { Mode = new(FlowJoinMode.WaitAny) };
+ var afterJoin = new WriteLine("AfterJoin");
+
+ var flowchart = new Flowchart
+ {
+ Start = start,
+ Activities = { start, branch1, branch2, join, afterJoin },
+ Connections =
+ {
+ CreateConnection(start, branch1),
+ CreateConnection(start, branch2),
+ CreateConnection(branch1, join),
+ CreateConnection(branch2, join),
+ CreateConnection(join, afterJoin)
+ }
+ };
+
+ // Act
+ var result = await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
+
+ // Assert: the outbound path of the join must have executed
+ Assert.Contains("AfterJoin", _output.Lines);
+
+ // Assert: the workflow must have finished, not suspended waiting for the canceled bookmark
+ Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
+
+ // Assert: the blocked branch's bookmark was cleared when it was canceled
+ Assert.Empty(result.WorkflowState.Bookmarks);
+ }
+
+ ///
+ /// A minimal activity that suspends by creating a bookmark, simulating a blocking activity
+ /// such as a Delay or Timer that would normally be resumed by an external stimulus.
+ ///
+ private sealed class BlockingActivity : Activity
+ {
+ protected override void Execute(ActivityExecutionContext context) => context.CreateBookmark();
+ }
}