elsa-core/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTokenBasedTests.cs
Sipke Schoorstra 730c01d9b0
Refactors and clarifies Flowchart merge modes (#6993)
* Refactors and clarifies Flowchart merge modes

Improves the clarity and functionality of Flowchart merge modes by:

- Renaming `None` to `Stream` for opportunistic execution.
- Introducing `Merge` for waiting on activated branches only.
- Enhancing `Converge` to be the strictest mode, requiring all inbound connections.
- Providing more detailed descriptions for each mode, emphasizing their behavior and use cases in flow-based terminology.
- Updates default merge mode to Stream

This provides better control over synchronization and execution behavior in workflows.

* Refactor `ActivityExtensions` to improve formatting, fix indentation, and align comments for improved readability and consistency

* Update flowchart tests: replace `SetMergeMode(MergeMode.None)` with `SetMergeMode(null)` and remove unused `Elsa.Workflows` imports.

* Restore None value for Flowchart MergeMode enum (#7107)

* Initial plan

* Add None value to MergeMode enum for backward compatibility

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Fix API client GetMergeMode to maintain non-nullable return type

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* [WIP] Address feedback on flowchart merge modes refactor (#7106)

* Initial plan

* Fix misleading documentation for Merge mode to match actual implementation

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Update test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test scenarios for implicit join behavior in `ForkDecisionJoinTests`. Updated file references for merge and stream join modes.

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 21:06:00 +01:00

549 lines
18 KiB
C#

using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Elsa.Workflows.Activities.Flowchart.Extensions;
using Elsa.Workflows.Activities.Flowchart.Models;
using Xunit.Abstractions;
using static Elsa.Activities.IntegrationTests.Flow.FlowchartTestHelpers;
namespace Elsa.Activities.IntegrationTests.Flow;
/// <summary>
/// Integration tests for token-based flowchart execution strategy.
/// </summary>
[Collection("FlowchartTests")]
public class FlowchartTokenBasedTests : IDisposable
{
private readonly IServiceProvider _services;
private readonly CapturingTextWriter _output;
private readonly bool _originalFlowMode;
public FlowchartTokenBasedTests(ITestOutputHelper testOutputHelper)
{
_output = new();
_services = CreateServiceProvider(testOutputHelper, _output);
_originalFlowMode = Flowchart.UseTokenFlow;
Flowchart.UseTokenFlow = true;
}
public void Dispose()
{
Flowchart.UseTokenFlow = _originalFlowMode;
}
[Fact(DisplayName = "Executes simple linear flowchart")]
public async Task ExecutesSimpleLinearFlowchart()
{
// Arrange
var flowchart = CreateSimpleLinearFlowchart(
new WriteLine("First"),
new WriteLine("Second"),
new WriteLine("Third")
);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Equal(3, _output.Lines.Count);
Assert.Equal("First", _output.Lines.ElementAt(0));
Assert.Equal("Second", _output.Lines.ElementAt(1));
Assert.Equal("Third", _output.Lines.ElementAt(2));
}
[Fact(DisplayName = "Executes both branches in parallel flowchart")]
public async Task ExecutesBothBranches()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var flowchart = CreateBranchingFlowchart(start, branch1, branch2);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Equal(3, _output.Lines.Count);
Assert.Contains("Start", _output.Lines);
Assert.Contains("Branch1", _output.Lines);
Assert.Contains("Branch2", _output.Lines);
}
[Fact(DisplayName = "Handles flowchart with no connections")]
public async Task HandlesNoConnections()
{
// Arrange
var activity = new WriteLine("Isolated");
var flowchart = new Flowchart
{
Start = activity,
Activities = { activity }
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Single(_output.Lines);
Assert.Equal("Isolated", _output.Lines.ElementAt(0));
}
[Fact(DisplayName = "Completes when start activity is null")]
public async Task CompletesWhenStartIsNull()
{
// Arrange
var flowchart = new Flowchart
{
Start = null
};
// Act
var result = await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.NotNull(result);
Assert.Empty(_output.Lines);
}
[Fact(DisplayName = "Follows conditional branches with If activity")]
public async Task FollowsConditionalBranches()
{
// Arrange
var ifActivity = new If
{
Condition = new(true),
Then = new WriteLine("Then branch"),
Else = new WriteLine("Else branch")
};
var flowchart = new Flowchart
{
Start = ifActivity,
Activities = { ifActivity }
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Single(_output.Lines);
Assert.Equal("Then branch", _output.Lines.ElementAt(0));
}
[Fact(DisplayName = "Executes Stream merge mode - schedules immediately")]
public async Task ExecutesStreamMergeMode()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var afterJoin = new WriteLine("AfterJoin");
afterJoin.SetMergeMode(MergeMode.Stream);
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, afterJoin },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, afterJoin),
CreateConnection(branch2, afterJoin)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("AfterJoin", _output.Lines);
// In Stream mode, afterJoin executes as soon as first branch arrives
}
[Fact(DisplayName = "Executes Race merge mode - cancels other branches")]
public async Task ExecutesRaceMergeMode()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var afterRace = new WriteLine("AfterRace");
afterRace.SetMergeMode(MergeMode.Race);
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, afterRace },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, afterRace),
CreateConnection(branch2, afterRace)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("AfterRace", _output.Lines);
// In Race mode, afterRace executes on first arrival and blocks others
}
[Fact(DisplayName = "Executes Converge merge mode - waits for all branches")]
public async Task ExecutesConvergeMergeMode()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var converge = new WriteLine("Converge");
converge.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, converge, end },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, converge),
CreateConnection(branch2, converge),
CreateConnection(converge, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Branch1", _output.Lines);
Assert.Contains("Branch2", _output.Lines);
Assert.Contains("Converge", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Executes None merge mode correctly")]
public async Task ExecutesNoneMergeMode()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var noneMode = new WriteLine("NoneMode");
noneMode.SetMergeMode(null);
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, noneMode },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, noneMode),
CreateConnection(branch2, noneMode)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Branch1", _output.Lines);
Assert.Contains("Branch2", _output.Lines);
Assert.Contains("NoneMode", _output.Lines);
}
[Fact(DisplayName = "Handles token consumption correctly")]
public async Task HandlesTokenConsumption()
{
// Arrange
var start = new WriteLine("Start");
var middle = new WriteLine("Middle");
var end = new WriteLine("End");
var flowchart = CreateSimpleLinearFlowchart(start, middle, end);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
// Tokens should be consumed after each activity completes
Assert.Equal(3, _output.Lines.Count);
Assert.Equal("Start", _output.Lines.ElementAt(0));
Assert.Equal("Middle", _output.Lines.ElementAt(1));
Assert.Equal("End", _output.Lines.ElementAt(2));
}
[Fact(DisplayName = "Handles multiple sequential converge nodes")]
public async Task HandlesMultipleSequentialConvergeNodes()
{
// Arrange
var start = new WriteLine("Start");
var a1 = new WriteLine("A1");
var a2 = new WriteLine("A2");
var converge1 = new WriteLine("Converge1");
converge1.SetMergeMode(MergeMode.Converge);
var b1 = new WriteLine("B1");
var b2 = new WriteLine("B2");
var converge2 = new WriteLine("Converge2");
converge2.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, a1, a2, converge1, b1, b2, converge2, end },
Connections =
{
CreateConnection(start, a1),
CreateConnection(start, a2),
CreateConnection(a1, converge1),
CreateConnection(a2, converge1),
CreateConnection(converge1, b1),
CreateConnection(converge1, b2),
CreateConnection(b1, converge2),
CreateConnection(b2, converge2),
CreateConnection(converge2, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("A1", _output.Lines);
Assert.Contains("A2", _output.Lines);
Assert.Contains("Converge1", _output.Lines);
Assert.Contains("B1", _output.Lines);
Assert.Contains("B2", _output.Lines);
Assert.Contains("Converge2", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Handles complex diamond pattern with tokens")]
public async Task HandlesComplexDiamondPattern()
{
// Arrange
var start = new WriteLine("Start");
var left1 = new WriteLine("Left1");
var left2 = new WriteLine("Left2");
var right1 = new WriteLine("Right1");
var right2 = new WriteLine("Right2");
var converge = new WriteLine("Converge");
converge.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, left1, left2, right1, right2, converge, end },
Connections =
{
CreateConnection(start, left1),
CreateConnection(start, right1),
CreateConnection(left1, left2),
CreateConnection(right1, right2),
CreateConnection(left2, converge),
CreateConnection(right2, converge),
CreateConnection(converge, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Left1", _output.Lines);
Assert.Contains("Left2", _output.Lines);
Assert.Contains("Right1", _output.Lines);
Assert.Contains("Right2", _output.Lines);
Assert.Contains("Converge", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Executes activities in correct order for sequential flow")]
public async Task ExecutesInCorrectOrderForSequential()
{
// Arrange
var flowchart = CreateSimpleLinearFlowchart(
new WriteLine("1"),
new WriteLine("2"),
new WriteLine("3"),
new WriteLine("4")
);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Equal(4, _output.Lines.Count);
Assert.Equal("1", _output.Lines.ElementAt(0));
Assert.Equal("2", _output.Lines.ElementAt(1));
Assert.Equal("3", _output.Lines.ElementAt(2));
Assert.Equal("4", _output.Lines.ElementAt(3));
}
[Fact(DisplayName = "Handles nested flowcharts with tokens")]
public async Task HandlesNestedFlowcharts()
{
// Arrange
var innerFlowchart = CreateSimpleLinearFlowchart(
new WriteLine("Inner1"),
new WriteLine("Inner2")
);
var outerFlowchart = CreateSimpleLinearFlowchart(
new WriteLine("Outer1"),
innerFlowchart,
new WriteLine("Outer2")
);
// Act
await RunFlowchartAsync(_services, outerFlowchart);
// Assert
Assert.Contains("Outer1", _output.Lines);
Assert.Contains("Inner1", _output.Lines);
Assert.Contains("Inner2", _output.Lines);
Assert.Contains("Outer2", _output.Lines);
}
[Fact(DisplayName = "Handles unconnected activities in flowchart")]
public async Task HandlesUnconnectedActivities()
{
// Arrange
var connected = new WriteLine("Connected");
var unconnected = new WriteLine("Unconnected");
var flowchart = new Flowchart
{
Start = connected,
Activities = { connected, unconnected }
// No connection to unconnected activity
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Single(_output.Lines);
Assert.Equal("Connected", _output.Lines.ElementAt(0));
Assert.DoesNotContain("Unconnected", _output.Lines);
}
[Fact(DisplayName = "Handles mixed merge modes in complex flow")]
public async Task HandlesMixedMergeModes()
{
// Arrange
var start = new WriteLine("Start");
var branch1 = new WriteLine("Branch1");
var branch2 = new WriteLine("Branch2");
var stream = new WriteLine("Stream");
stream.SetMergeMode(MergeMode.Stream);
var branch3 = new WriteLine("Branch3");
var branch4 = new WriteLine("Branch4");
var converge = new WriteLine("Converge");
converge.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = new Flowchart
{
Start = start,
Activities = { start, branch1, branch2, stream, branch3, branch4, converge, end },
Connections =
{
CreateConnection(start, branch1),
CreateConnection(start, branch2),
CreateConnection(branch1, stream),
CreateConnection(branch2, stream),
CreateConnection(stream, branch3),
CreateConnection(stream, branch4),
CreateConnection(branch3, converge),
CreateConnection(branch4, converge),
CreateConnection(converge, end)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Stream", _output.Lines);
Assert.Contains("Converge", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Handles converge with single inbound connection")]
public async Task HandlesConvergeWithSingleInbound()
{
// Arrange
var start = new WriteLine("Start");
var single = new WriteLine("Single");
single.SetMergeMode(MergeMode.Converge);
var end = new WriteLine("End");
var flowchart = CreateSimpleLinearFlowchart(start, single, end);
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
Assert.Contains("Start", _output.Lines);
Assert.Contains("Single", _output.Lines);
Assert.Contains("End", _output.Lines);
}
[Fact(DisplayName = "Emits and consumes tokens correctly across multiple steps")]
public async Task EmitsAndConsumesTokensCorrectly()
{
// Arrange
var step1 = new WriteLine("Step1");
var step2a = new WriteLine("Step2a");
var step2b = new WriteLine("Step2b");
var step3 = new WriteLine("Step3");
step3.SetMergeMode(MergeMode.Converge);
var step4 = new WriteLine("Step4");
var flowchart = new Flowchart
{
Start = step1,
Activities = { step1, step2a, step2b, step3, step4 },
Connections =
{
CreateConnection(step1, step2a),
CreateConnection(step1, step2b),
CreateConnection(step2a, step3),
CreateConnection(step2b, step3),
CreateConnection(step3, step4)
}
};
// Act
await RunFlowchartAsync(_services, flowchart);
// Assert
// Verify all activities executed in a valid order
Assert.Contains("Step1", _output.Lines);
Assert.Contains("Step2a", _output.Lines);
Assert.Contains("Step2b", _output.Lines);
Assert.Contains("Step3", _output.Lines);
Assert.Contains("Step4", _output.Lines);
// Step3 should only appear once (tokens consumed properly)
Assert.Single(_output.Lines, l => l == "Step3");
}
}