using System.Collections.Generic;
using Bpmn.Semantics;
using Elsa.Bpmn.Activities;
using Elsa.Bpmn.Hosting;
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Models;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Bpmn.UnitTests;
///
/// Covers the root-position refusal in : a StartWork command bound
/// to a nested that (mis)declares itself the workflow's root scope must be refused before
/// any command in the same batch is applied — not mid-list, after earlier commands already mutated and saved scope
/// memory. Under ContinueWithIncidentsStrategy the throw is absorbed into an incident rather than surfaced, so
/// a partial application would otherwise leave the scope silently half-mutated.
///
public class BpmnCommandApplierTests
{
private const string OrdinaryActivityId = "ordinary-activity";
private const string NestedProcessActivityId = "nested-activity";
[Fact]
public async Task ApplyAsync_RefusesTheWholeBatch_WhenALaterCommandBindsARootScopeProcess()
{
var (scopeContext, process) = await BuildScopeAsync();
var memory = BpmnScopeMemory.Load(scopeContext);
var applier = new BpmnCommandApplier(scopeContext, process, memory);
var commands = new BpmnHostCommand[]
{
new BpmnHostCommand.StartWork("ordinary-binding", "ordinary-element", "token-1", "cause", new Dictionary(), null, null),
new BpmnHostCommand.StartWork("nested-binding", "nested-element", "token-2", "cause", new Dictionary(), null, null)
};
var exception = await Assert.ThrowsAsync(() => applier.ApplyAsync(commands).AsTask());
Assert.Contains(NestedProcessActivityId, exception.Message);
// The earlier command in the same batch — the ordinary StartWork — must not have been applied: no work
// record persisted, and no child context created for it.
var reloaded = BpmnScopeMemory.Load(scopeContext);
Assert.Empty(reloaded.Work.Records);
Assert.DoesNotContain(scopeContext.WorkflowExecutionContext.ActivityExecutionContexts, context => context.Activity.Id == OrdinaryActivityId);
}
private static async Task<(ActivityExecutionContext ScopeContext, BpmnProcess Process)> BuildScopeAsync()
{
var ordinaryActivity = new WriteLine("ordinary") { Id = OrdinaryActivityId };
var nestedProcess = new BpmnProcess { Id = NestedProcessActivityId, IsRootScope = true };
var process = new BpmnProcess
{
Id = "process-activity",
Activities = { ordinaryActivity, nestedProcess },
WorkBindings = new Dictionary(StringComparer.Ordinal)
{
["ordinary-binding"] = OrdinaryActivityId,
["nested-binding"] = NestedProcessActivityId
}
};
var fixture = new ActivityTestFixture(process);
fixture.ConfigureServices(services => services.AddSingleton());
var scopeContext = await fixture.BuildAsync();
var workflowExecutionContext = scopeContext.WorkflowExecutionContext;
await workflowExecutionContext.ActivityRegistry.RegisterAsync(typeof(WriteLine));
await workflowExecutionContext.ActivityRegistry.RegisterAsync(typeof(BpmnProcess));
return (scopeContext, process);
}
}