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