elsa-core/test/integration/Elsa.Alterations.IntegrationTests/RetrySequenceTests.cs
Sipke Schoorstra 602ad38afa
fix: preserve Sequence ownership when retrying child activities (#8027)
* fix: preserve Sequence ownership when retrying child activities

* fix: allow FastEndpoints EmptyRequest in invitation wrapper
2026-09-05 18:42:28 -07:00

140 lines
5.8 KiB
C#

using Elsa.Alterations.AlterationTypes;
using Elsa.Alterations.Core.Contracts;
using Elsa.Alterations.Extensions;
using Elsa.Common.Models;
using Elsa.Extensions;
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.IncidentStrategies;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Activities;
using Elsa.Workflows.Runtime.Messages;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Alterations.IntegrationTests;
public sealed class RetrySequenceTests : IAsyncLifetime
{
private readonly IServiceProvider _services;
private readonly CapturingTextWriter _output = new();
private readonly RetryProbe _probe = new();
public RetrySequenceTests(ITestOutputHelper output)
{
_services = new TestApplicationBuilder(output)
.WithCapturingTextWriter(_output)
.ConfigureServices(services => services.AddSingleton(_probe))
.ConfigureElsa(elsa => elsa.UseAlterations())
.AddWorkflow<RetrySequenceWorkflow>()
.AddWorkflow<RetryStandaloneSequenceWorkflow>()
.AddActivitiesFrom<RetryBookmarkActivity>()
.Build();
}
public Task InitializeAsync() => _services.PopulateRegistriesAsync();
public async Task DisposeAsync() => await ((IAsyncDisposable)_services).DisposeAsync();
[Theory]
[InlineData(nameof(RetrySequenceWorkflow))]
[InlineData(nameof(RetryStandaloneSequenceWorkflow))]
public async Task RetriedChildCompletesItsSequenceAndPreservesTheNextBookmark(string definitionId)
{
var runtime = _services.GetRequiredService<IWorkflowRuntime>();
var client = await runtime.CreateClientAsync();
await client.CreateInstanceAsync(new()
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(definitionId, VersionOptions.Published)
});
var firstRun = await client.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
var faultedState = await client.ExportStateAsync();
var incident = Assert.Single(faultedState.Incidents);
Assert.Equal("Retry", incident.ActivityId);
var faultedContext = Assert.Single(faultedState.ActivityExecutionContexts, x => x.Status == ActivityStatus.Faulted);
var sequenceContextId = faultedContext.ParentContextId;
Assert.Equal(1, _probe.Attempts);
Assert.Empty(_output.Lines);
var alterations = new IAlteration[] { new ScheduleActivity { ActivityInstanceId = faultedContext.Id } };
var results = await _services.GetRequiredService<IAlterationRunner>().RunAsync([firstRun.WorkflowInstanceId], alterations);
Assert.True(Assert.Single(results).IsSuccessful);
var alteredState = await client.ExportStateAsync();
var callbacks = alteredState.CompletionCallbacks
.Where(x => x.ChildNodeId == faultedContext.ScheduledActivityNodeId)
.ToList();
Assert.NotEmpty(callbacks);
Assert.All(callbacks, callback => Assert.Equal(sequenceContextId, callback.OwnerInstanceId));
await client.RunInstanceAsync(RunWorkflowInstanceRequest.Empty);
var retriedState = await client.ExportStateAsync();
var retryBookmark = Assert.Single(retriedState.Bookmarks);
Assert.Equal(faultedContext.Id, retryBookmark.ActivityInstanceId);
Assert.Equal(2, _probe.Attempts);
var afterRetry = await client.RunInstanceAsync(new() { BookmarkId = retryBookmark.Id });
var nextState = await client.ExportStateAsync();
var nextBookmark = Assert.Single(nextState.Bookmarks);
Assert.Equal(WorkflowStatus.Running, afterRetry.Status);
Assert.Equal(["Sequence finished"], _output.Lines);
Assert.Equal(faultedState.Incidents.Count, nextState.Incidents.Count);
Assert.NotEqual(retryBookmark.ActivityInstanceId, nextBookmark.ActivityInstanceId);
var finalRun = await client.RunInstanceAsync(new() { BookmarkId = nextBookmark.Id });
Assert.Equal(WorkflowStatus.Finished, finalRun.Status);
Assert.Equal(["Sequence finished", "Done"], _output.Lines);
Assert.Equal(2, _probe.Attempts);
Assert.Empty((await client.ExportStateAsync()).Bookmarks);
}
public sealed class RetryProbe
{
public int Attempts { get; set; }
}
public sealed class RetryBookmarkActivity : Activity
{
protected override void Execute(ActivityExecutionContext context)
{
if (++context.GetRequiredService<RetryProbe>().Attempts == 1)
{
throw new InvalidOperationException("Transient test failure");
}
context.CreateBookmark(new CreateBookmarkArgs());
}
}
public class RetrySequenceWorkflow : WorkflowBase
{
protected virtual bool UseFlowchart => true;
protected override void Build(IWorkflowBuilder builder)
{
builder.WorkflowOptions.IncidentStrategyType = typeof(ContinueWithIncidentsStrategy);
var sequence = new Sequence
{
Activities = { new RetryBookmarkActivity { Id = "Retry" }, new WriteLine("Sequence finished") }
};
var next = new Event("Next");
var end = new WriteLine("Done");
builder.Root = UseFlowchart
? new Flowchart
{
Activities = { sequence, next, end },
Connections = { new Connection(sequence, next), new Connection(next, end) }
}
: new Sequence { Activities = { sequence, next, end } };
}
}
public sealed class RetryStandaloneSequenceWorkflow : RetrySequenceWorkflow
{
protected override bool UseFlowchart => false;
}
}