using global::BuildFlow.Shared.Domain.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Workflow.Application.Engine; using Workflow.Domain.Entities; using Workflow.Domain.Enums; using Workflow.Domain.Expressions; using TaskStatus = Workflow.Domain.Enums.TaskStatus; using InstanceStatus = Workflow.Domain.Enums.InstanceStatus; namespace BuildFlow.Workflow.Tests.Engine; using FluentAssertions; using Xunit; /// /// 依次审批(Sequential)测试:A → B → C 按顺序逐个审,全部通过才推进,任一驳回中断。 /// 这是钉钉式审批流的核心审批方式,区别于会签(同时)/或签(任一)。 /// public class SequentialApprovalTests { private readonly WorkflowDbContext _dbContext; private readonly ProcessEngine _engine; public SequentialApprovalTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; _dbContext = new WorkflowDbContext(options); // InMemory 不跑审计拦截器,手动填充非空审计字段 _dbContext.SavingChanges += (sender, _) => { if (sender is not DbContext ctx) return; foreach (var entry in ctx.ChangeTracker.Entries()) { if (entry.Entity is IAuditable a) { a.CreatedBy ??= "test"; a.UpdatedBy ??= "test"; } if (entry.Entity is IHasOperatorIP o) { o.OperatorIP ??= "127.0.0.1"; } } }; _engine = new ProcessEngine(_dbContext, new ServiceCollection().BuildServiceProvider(), new ConditionEvaluator()); } /// 启动依次审批节点:只创建第 0 个审批人的任务,记录完整序列。 [Fact] public async Task Sequential_Start_CreatesOnlyFirstAssigneeTask() { var (approvalNode, instance, endNode) = await SetupSequentialFlow(3); await _engine.StartAsync(instance); var tasks = await _dbContext.WorkflowTasks .Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending) .ToListAsync(); // 只建第 0 个任务(区别于会签一次性建 3 个) tasks.Should().HaveCount(1); tasks[0].SequentialOrder.Should().Be(0); tasks[0].SequentialAssignees.Should().NotBeNull("应记录完整序列供逐个推进"); tasks[0].SequentialAssignees!.Should().Contain("user:"); } /// 依次审批:第 1 个通过后建第 2 个任务,token 不推进。 [Fact] public async Task Sequential_FirstApproved_CreatesSecondTaskWithoutPropagating() { var (approvalNode, instance, endNode) = await SetupSequentialFlow(3); await _engine.StartAsync(instance); var firstTask = await _dbContext.WorkflowTasks .FirstAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending); // 第 1 个通过 await _engine.CompleteTaskAsync(firstTask, TaskResult.Approved); // 第 1 个任务已 Approved firstTask.Status.Should().Be(TaskStatus.Approved); // 新建了第 2 个任务(SequentialOrder=1),仍是 Pending var secondTask = await _dbContext.WorkflowTasks .SingleOrDefaultAsync(t => t.NodeId == approvalNode.Id && t.SequentialOrder == 1 && t.Status == TaskStatus.Pending); secondTask.Should().NotBeNull("应创建下一个审批人任务"); secondTask!.SequentialAssignees.Should().Be(firstTask.SequentialAssignees); // token 未推进到 End var endTokens = await _dbContext.WorkflowTokens.CountAsync(t => t.NodeId == endNode.Id); endTokens.Should().Be(0, "依次审批未完成,token 不应到达 End"); } /// 依次审批:全部通过后推进到下游 End。 [Fact] public async Task Sequential_AllApproved_PropagatesDownstream() { var (approvalNode, instance, endNode) = await SetupSequentialFlow(3); await _engine.StartAsync(instance); // 逐个通过 3 个审批人 for (var i = 0; i < 3; i++) { var current = await _dbContext.WorkflowTasks .FirstAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending); await _engine.CompleteTaskAsync(current, TaskResult.Approved); } // 全部通过后 token 推进,实例完成 instance.Status.Should().Be(InstanceStatus.Completed); } /// 依次审批:中间任一驳回则中断(走 Rejected 边语义;本测试无 Rejected 边故验证不继续建任务)。 [Fact] public async Task Sequential_RejectedInMiddle_StopsSequentialChain() { var user1 = Guid.NewGuid(); var user2 = Guid.NewGuid(); var approvalNode = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "seq", CreatedBy = "test", UpdatedBy = "test", Config = $$"""{ "approvalMode": "sequential", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""", }; // 提供一条 Rejected 边回到自己(避免 No reject edge 异常),再一条 Normal 边到 End var startNode = CreateNode(NodeType.Start); var endNode = CreateNode(NodeType.End); var edgeStart = CreateEdge(startNode, approvalNode); var edgeReject = CreateEdge(approvalNode, approvalNode); edgeReject.EdgeType = EdgeType.Rejected; var edgeApprove = CreateEdge(approvalNode, endNode); var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edgeReject, edgeApprove]); await _engine.StartAsync(instance); var firstTask = await _dbContext.WorkflowTasks .FirstAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending); // 第 1 个驳回 → 走 Rejected 边,不建第 2 个任务 await _engine.CompleteTaskAsync(firstTask, TaskResult.Rejected); firstTask.Status.Should().Be(TaskStatus.Rejected); var secondTask = await _dbContext.WorkflowTasks .CountAsync(t => t.NodeId == approvalNode.Id && t.SequentialOrder == 1); secondTask.Should().Be(0, "驳回后不应继续依次建任务"); } // ===== Setup helpers(与 MultiApprovalTests 同范式)===== private async Task<(WorkflowNode ApprovalNode, WorkflowInstance Instance, WorkflowNode EndNode)> SetupSequentialFlow(int assigneeCount) { var users = Enumerable.Range(0, assigneeCount).Select(_ => Guid.NewGuid()).ToArray(); var rules = users.Select(u => $"user:{u}").ToArray(); var rulesJson = System.Text.Json.JsonSerializer.Serialize(rules); var approvalNode = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "依次审批", CreatedBy = "test", UpdatedBy = "test", Config = $$"""{ "approvalMode": "sequential", "assigneeRules": {{rulesJson}} }""", }; var startNode = CreateNode(NodeType.Start); var endNode = CreateNode(NodeType.End); var edgeStart = CreateEdge(startNode, approvalNode); var edge = CreateEdge(approvalNode, endNode); var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]); // PersistSetup 内部已 SaveChanges;返回前 await 无副作用,保持签名一致 await Task.CompletedTask; return (approvalNode, instance, endNode); } private static WorkflowNode CreateNode(NodeType type) => new() { Id = Guid.NewGuid(), NodeType = type, Name = type.ToString().ToLowerInvariant(), CreatedBy = "test", UpdatedBy = "test", }; private static WorkflowEdge CreateEdge(WorkflowNode source, WorkflowNode target) => new() { Id = Guid.NewGuid(), SourceNodeId = source.Id, TargetNodeId = target.Id, EdgeType = EdgeType.Normal, CreatedBy = "test", UpdatedBy = "test", }; private (WorkflowDefinition Definition, WorkflowInstance Instance) PersistSetup( List nodes, List edges) { var definition = new WorkflowDefinition { Id = Guid.NewGuid(), Name = "seq-test-flow", Status = DefinitionStatus.Published, CreatedBy = "test", UpdatedBy = "test", }; foreach (var n in nodes) { n.DefinitionId = definition.Id; _dbContext.WorkflowNodes.Add(n); } foreach (var e in edges) { e.DefinitionId = definition.Id; _dbContext.WorkflowEdges.Add(e); } _dbContext.WorkflowDefinitions.Add(definition); var instance = new WorkflowInstance { Id = Guid.NewGuid(), DefinitionId = definition.Id, Title = "seq-test", Status = InstanceStatus.Running, Variables = "{}", CreatedBy = "test", UpdatedBy = "test", }; _dbContext.WorkflowInstances.Add(instance); _dbContext.SaveChanges(); return (definition, instance); } }