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