using Workflow.Application.Engine; using Workflow.Domain.Entities; using Workflow.Domain.Enums; using Workflow.Domain.Expressions; using TaskStatus = Workflow.Domain.Enums.TaskStatus; namespace BuildFlow.Workflow.Tests.Engine; using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using global::BuildFlow.Shared.Domain.Common; using Xunit; /// /// 会签/或签/退回测试(TDD)。 /// /// 设计:在 Approval 节点 config 增加字段(不新增 NodeType,向后兼容): /// - approvalMode: "single"(默认) | "countersign"(会签-全部通过) | "any"(或签-任一通过) /// - assigneeRules: ["user:xxx", "user:yyy", ...] 多受理人 /// - minApprovals: 会签模式下最少通过数(默认=全部) /// public class MultiApprovalTests { private readonly WorkflowDbContext _dbContext; private readonly ProcessEngine _engine; public MultiApprovalTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; _dbContext = new WorkflowDbContext(options); // 模拟 AuditInterceptor:InMemory 不跑拦截器,此处手动填充审计字段, // 使 StartAsync/CompleteTaskAsync 创建的 token/task 能通过非空校验。 _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()); } // ============================================================ // 会签(Countersign):全部通过才推进 // ============================================================ /// /// 会签模式:3 个受理人,第 1 个通过后流程不推进(仍有待办任务)。 /// [Fact] public async Task Countersign_PartialApproval_DoesNotPropagate() { var user1 = Guid.NewGuid(); var user2 = Guid.NewGuid(); var user3 = Guid.NewGuid(); var approvalNode = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "会签节点", CreatedBy = "test", UpdatedBy = "test", Config = $$"""{ "approvalMode": "countersign", "assigneeRules": ["user:{{user1}}", "user:{{user2}}", "user:{{user3}}"] }""", }; 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]); await _engine.StartAsync(instance); // 应为 3 个受理人各创建 1 个 Pending 任务 var tasks = await _dbContext.WorkflowTasks .Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending) .ToListAsync(); tasks.Should().HaveCount(3); // 第 1 个人通过 await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved); // 还有 2 个待办 → 流程不应推进到 End var remainingPending = await _dbContext.WorkflowTasks .CountAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending); remainingPending.Should().Be(2); var endTokens = await _dbContext.WorkflowTokens .CountAsync(t => t.NodeId == endNode.Id); endTokens.Should().Be(0, "会签未完成,token 不应到达 End"); } /// /// 会签模式:全部通过后流程才推进到下游。 /// [Fact] public async Task Countersign_AllApproved_PropagatesDownstream() { var user1 = Guid.NewGuid(); var user2 = Guid.NewGuid(); var approvalNode = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "approval", CreatedBy = "test", UpdatedBy = "test", Config = $$"""{ "approvalMode": "countersign", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""", }; 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]); await _engine.StartAsync(instance); var tasks = await _dbContext.WorkflowTasks .Where(t => t.NodeId == approvalNode.Id) .ToListAsync(); // 全部通过 await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved); await _engine.CompleteTaskAsync(tasks[1], TaskResult.Approved); // 实例应完成(token 到达 End 被消费) instance.Status.Should().Be(InstanceStatus.Completed); } // ============================================================ // 或签(Any):任一通过即推进 // ============================================================ /// /// 或签模式:第 1 个通过后立即推进,其余任务自动关闭。 /// [Fact] public async Task AnyApproval_FirstApproval_PropagatesAndClosesOthers() { var user1 = Guid.NewGuid(); var user2 = Guid.NewGuid(); var approvalNode = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "approval", CreatedBy = "test", UpdatedBy = "test", Config = $$"""{ "approvalMode": "any", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""", }; 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]); await _engine.StartAsync(instance); var tasks = await _dbContext.WorkflowTasks .Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending) .ToListAsync(); tasks.Should().HaveCount(2); // 第 1 个人通过 await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved); // 流程应已推进 → 实例完成 instance.Status.Should().Be(InstanceStatus.Completed); // 其余待办任务应自动关闭(不应残留 Pending) var remainingPending = await _dbContext.WorkflowTasks .CountAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending); remainingPending.Should().Be(0, "或签通过后其余任务应自动关闭"); } // ============================================================ // 向后兼容:无 approvalMode 时保持单审批行为 // ============================================================ [Fact] public async Task NoApprovalMode_DefaultsToSingleApproval() { var approvalNode = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "approval", CreatedBy = "test", UpdatedBy = "test", Config = """{ "assigneeRule": "role:approver" }""", }; 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]); await _engine.StartAsync(instance); var tasks = await _dbContext.WorkflowTasks .CountAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending); tasks.Should().Be(1, "无 approvalMode 默认单审批"); } // ============================================================ // 退回(Return):回到上一审批节点 // ============================================================ /// /// 退回:第 2 个审批节点退回时,token 回到第 1 个审批节点重新创建任务。 /// [Fact] public async Task ReturnTask_GoesBackToPreviousApprovalNode() { var firstApproval = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "approval", CreatedBy = "test", UpdatedBy = "test", Config = """{ "assigneeRule": "role:approver1" }""", }; var secondApproval = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "approval", CreatedBy = "test", UpdatedBy = "test", Config = """{ "assigneeRule": "role:approver2" }""", }; var endNode = CreateNode(NodeType.End); var startNode = CreateNode(NodeType.Start); var edgeStart = CreateEdge(startNode, firstApproval); var edge1 = CreateEdge(firstApproval, secondApproval); var edge2 = CreateEdge(secondApproval, endNode); var (_, instance) = PersistSetup([startNode, firstApproval, secondApproval, endNode], [edgeStart, edge1, edge2]); await _engine.StartAsync(instance); // 第 1 个审批通过 → token 到第 2 个审批 var task1 = await _dbContext.WorkflowTasks.FirstAsync(t => t.NodeId == firstApproval.Id); await _engine.CompleteTaskAsync(task1, TaskResult.Approved); // 第 2 个审批任务存在 var task2 = await _dbContext.WorkflowTasks.FirstAsync(t => t.NodeId == secondApproval.Id); task2.Status.Should().Be(TaskStatus.Pending); // 退回:应回到第 1 个审批节点 await _engine.ReturnTaskAsync(task2); // 第 1 个审批节点应有新的 Pending 任务(重新创建) var returnedTask = await _dbContext.WorkflowTasks .FirstOrDefaultAsync(t => t.NodeId == firstApproval.Id && t.Status == TaskStatus.Pending); returnedTask.Should().NotBeNull("退回应在第 1 个审批节点重新创建任务"); // 第 2 个审批任务应不再是 Pending await _dbContext.Entry(task2).ReloadAsync(); task2.Status.Should().NotBe(TaskStatus.Pending); } // ============================================================ // 端到端组合:会签 × 条件路由(消解"条件求值器没弄好"的疑虑) // ============================================================ /// /// 会签通过后,Condition 节点根据实例变量正确路由到不同分支。 /// 验证:会签引擎与条件求值器的端到端协作(用户担忧的"流程引擎+条件器没弄好")。 /// [Fact] public async Task CountersignApproved_ThenConditionRoutesBasedOnVariable() { var user1 = Guid.NewGuid(); var user2 = Guid.NewGuid(); var approvalNode = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "会签审批", CreatedBy = "test", UpdatedBy = "test", Config = $$"""{ "approvalMode": "countersign", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""", }; var conditionNode = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Condition, Name = "金额判断", CreatedBy = "test", UpdatedBy = "test", Config = "{}", }; // 金额 > 1000 走高额分支 var highAmountEnd = CreateNode(NodeType.End); highAmountEnd.Name = "高额终态"; var normalEnd = CreateNode(NodeType.End); normalEnd.Name = "普通终态"; var startNode = CreateNode(NodeType.Start); var edgeStart = CreateEdge(startNode, approvalNode); var edgeToCondition = CreateEdge(approvalNode, conditionNode); var edgeHigh = new WorkflowEdge { Id = Guid.NewGuid(), DefinitionId = Guid.Empty, // PersistSetup 会回填 SourceNodeId = conditionNode.Id, TargetNodeId = highAmountEnd.Id, EdgeType = EdgeType.Normal, Condition = "amount > 1000", Order = 1, CreatedBy = "test", UpdatedBy = "test", }; var edgeNormal = new WorkflowEdge { Id = Guid.NewGuid(), DefinitionId = Guid.Empty, SourceNodeId = conditionNode.Id, TargetNodeId = normalEnd.Id, EdgeType = EdgeType.Normal, Condition = "amount <= 1000", Order = 2, CreatedBy = "test", UpdatedBy = "test", }; var (_, instance) = PersistSetup( [startNode, approvalNode, conditionNode, highAmountEnd, normalEnd], [edgeStart, edgeToCondition, edgeHigh, edgeNormal]); // 设置实例变量(模拟表单数据回流) instance.Variables = """{ "amount": 5000 }"""; await _dbContext.SaveChangesAsync(); await _engine.StartAsync(instance); // 会签全部通过 var tasks = await _dbContext.WorkflowTasks .Where(t => t.NodeId == approvalNode.Id) .ToListAsync(); await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved); await _engine.CompleteTaskAsync(tasks[1], TaskResult.Approved); // 会签通过 → Condition 求值 → 应路由到高额分支(amount=5000 > 1000) var highEndToken = await _dbContext.WorkflowTokens .AnyAsync(t => t.NodeId == highAmountEnd.Id); var normalEndToken = await _dbContext.WorkflowTokens .AnyAsync(t => t.NodeId == normalEnd.Id); highEndToken.Should().BeTrue("amount=5000 > 1000 应走高额分支"); normalEndToken.Should().BeFalse("不应走普通分支"); } /// /// 或签模式:第 1 个通过即推进,验证其余任务的 Comment 字段不影响判定。 /// 锁定或签的"竞争"语义——只看状态不看评论。 /// [Fact] public async Task AnyApproval_PartialRejectDoesNotAffectAlreadyPropagated() { var user1 = Guid.NewGuid(); var user2 = Guid.NewGuid(); var approvalNode = new WorkflowNode { Id = Guid.NewGuid(), NodeType = NodeType.Approval, Name = "或签", CreatedBy = "test", UpdatedBy = "test", Config = $$"""{ "approvalMode": "any", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""", }; var endNode = CreateNode(NodeType.End); var startNode = CreateNode(NodeType.Start); var edgeStart = CreateEdge(startNode, approvalNode); var edge = CreateEdge(approvalNode, endNode); var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]); await _engine.StartAsync(instance); var tasks = await _dbContext.WorkflowTasks .Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending) .ToListAsync(); // 第 1 个通过 → 立即推进 await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved); instance.Status.Should().Be(InstanceStatus.Completed); // 第 2 个任务已被自动关闭(不再 Pending) await _dbContext.Entry(tasks[1]).ReloadAsync(); tasks[1].Status.Should().NotBe(TaskStatus.Pending, "或签通过后其余任务应自动关闭"); } // ============================================================ // 辅助方法 // ============================================================ private static WorkflowNode CreateNode(NodeType type) => new() { Id = Guid.NewGuid(), NodeType = type, Config = "{}", Name = type.ToString(), 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", }; /// 在审批节点预创建 Active token(填审计字段,绕过 InMemory 不跑拦截器)。 private WorkflowToken SeedApprovalToken(WorkflowInstance instance, WorkflowNode node) { var token = new WorkflowToken { Id = Guid.NewGuid(), InstanceId = instance.Id, NodeId = node.Id, Status = TokenStatus.Active, CreatedBy = "test", UpdatedBy = "test", }; _dbContext.WorkflowTokens.Add(token); _dbContext.SaveChanges(); return token; } private (WorkflowDefinition Definition, WorkflowInstance Instance) PersistSetup( List nodes, List edges) { var definition = new WorkflowDefinition { Id = Guid.NewGuid(), Name = "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 = "test-instance", Status = InstanceStatus.Running, // 直接置 Running,绕过 StartAsync Variables = "{}", CreatedBy = "test", UpdatedBy = "test", }; _dbContext.WorkflowInstances.Add(instance); _dbContext.SaveChanges(); return (definition, instance); } }