using Workflow.Domain.Enums; using System.Text.Json; using FluentAssertions; using Workflow.Application.Engine; using Workflow.Domain.Engine; using Xunit; namespace BuildFlow.Workflow.Tests.Engine; /// /// TreeToGraphCompiler 单元测试:覆盖顺序/条件/并行/嵌套四种拓扑, /// 并断言编译产出的图能通过 WorkflowGraphValidator(发布校验)。 /// public class TreeToGraphCompilerTests { // 测试用固定 Guid(前端真实环境用 crypto.randomUUID()) private const string RootId = "00000000-0000-0000-0000-000000000001"; private const string A1Id = "00000000-0000-0000-0000-000000000011"; private const string C1Id = "00000000-0000-0000-0000-000000000021"; private const string CondId = "00000000-0000-0000-0000-000000000031"; private const string ParId = "00000000-0000-0000-0000-000000000041"; private const string BigId = "00000000-0000-0000-0000-000000000051"; private const string SmallId = "00000000-0000-0000-0000-000000000061"; private const string LegalId = "00000000-0000-0000-0000-000000000071"; private const string FinId = "00000000-0000-0000-0000-000000000081"; private const string EndCcId = "00000000-0000-0000-0000-000000000091"; private const string LeadId = "00000000-0000-0000-0000-0000000000a1"; private static TreeNode Node(string id, TreeNodeTypeEnum type, string? title = null, object? config = null) => new() { Id = id, Type = type, Title = title, Config = config is null ? null : JsonDocument.Parse(JsonSerializer.Serialize(config)).RootElement.Clone(), }; private static TreeNode Approval(string id, string title, params string[] assignees) => Node(id, TreeNodeTypeEnum.Approval, title, new { approvalMode = "countersign", assigneeRules = assignees, }); private static TreeNode Cc(string id, string title, params string[] recipients) => Node(id, TreeNodeTypeEnum.Cc, title, new { recipients }); private static TreeNode ConditionBranch(string id, string title, params TreeBranch[] branches) => new() { Id = id, Type = TreeNodeTypeEnum.ConditionBranch, Title = title, Branches = branches.ToList(), }; private static TreeNode ParallelBranch(string id, string title, params TreeBranch[] branches) => new() { Id = id, Type = TreeNodeTypeEnum.ParallelBranch, Title = title, Branches = branches.ToList(), }; private static TreeBranch Branch(string id, string label, string? condition, TreeNode? child) => new() { Id = id, Label = label, Condition = condition, Child = child }; /// 把节点链按 child 顺序串起来,返回链头。 private static TreeNode Chain(params TreeNode[] nodes) { for (var i = 0; i < nodes.Length - 1; i++) { nodes[i].Child = nodes[i + 1]; } return nodes[0]; } // ====== 1. 空树 ====== [Fact] public void Compile_EmptyTree_ProducesMinimalStartEndGraph() { var graph = TreeToGraphCompiler.Compile(null); var startNodes = graph.Nodes.Where(n => n.Shape == WorkflowShapeMap.Shapes.Start).ToList(); var endNodes = graph.Nodes.Where(n => n.Shape == WorkflowShapeMap.Shapes.End).ToList(); startNodes.Should().HaveCount(1); endNodes.Should().HaveCount(1); graph.Edges.Should().HaveCount(1); graph.Edges[0].Source.Cell.Should().Be(startNodes[0].Id); graph.Edges[0].Target.Cell.Should().Be(endNodes[0].Id); // 必须能通过校验 WorkflowGraphValidator.Validate(graph); } // ====== 2. 顺序链:发起人 → 审批 → 抄送 ====== [Fact] public void Compile_SequentialChain_SingleStartSingleEnd() { var tree = new WorkflowTree { Root = Chain( Node(RootId, TreeNodeTypeEnum.Initiator, "发起人"), Approval(A1Id, "主管审批", "user:11111111-1111-1111-1111-111111111111"), Cc(C1Id, "抄送财务", "user:22222222-2222-2222-2222-222222222222")), }; var graph = TreeToGraphCompiler.Compile(tree); // 唯一 Start / 唯一 End graph.Nodes.Count(n => n.Shape == WorkflowShapeMap.Shapes.Start).Should().Be(1); graph.Nodes.Count(n => n.Shape == WorkflowShapeMap.Shapes.End).Should().Be(1); // 审批 + 抄送各一个 graph.Nodes.Should().Contain(n => n.Shape == WorkflowShapeMap.Shapes.Approval && n.Id == A1Id); graph.Nodes.Should().Contain(n => n.Shape == WorkflowShapeMap.Shapes.Cc && n.Id == C1Id); // 校验通过(证明是引擎可跑的合法图) var act = () => WorkflowGraphValidator.Validate(graph); act.Should().NotThrow(); } // ====== 3. 条件分支:审批后按金额分流,再汇合到抄送 ====== [Fact] public void Compile_ConditionBranch_ProducesConditionGatewayWithOrderedConditionEdges() { var tree = new WorkflowTree { Root = Chain( Node(RootId, TreeNodeTypeEnum.Initiator, "发起人"), ConditionBranch(CondId, "金额分支", Branch("b1", "大于5000", "amount > 5000", Chain(Approval(BigId, "总监审批", "user:big"))), Branch("b2", "默认", null, Chain(Approval(SmallId, "组长审批", "user:small")))), Cc(C1Id, "抄送财务", "user:fin")), }; var graph = TreeToGraphCompiler.Compile(tree); // 恰好一个 wf-condition 网关 var gateways = graph.Nodes.Where(n => n.Shape == WorkflowShapeMap.Shapes.Condition).ToList(); gateways.Should().HaveCount(1); // 网关的两条出边都有 order;条件分支条件写进 data.condition var outEdges = graph.Edges.Where(e => e.Source.Cell == gateways[0].Id).ToList(); outEdges.Should().HaveCount(2); outEdges.Should().AllSatisfy(e => e.GetOrder().Should().BeGreaterThanOrEqualTo(0)); outEdges.Should().Contain(e => (e.GetCondition() ?? "").Contains("amount")); // 校验通过(条件节点出边必须带 condition,且不全空) var act = () => WorkflowGraphValidator.Validate(graph); act.Should().NotThrow(); } // ====== 4. 并行分支 ====== [Fact] public void Compile_ParallelBranch_ProducesForkAndJoin() { var tree = new WorkflowTree { Root = Chain( Node(RootId, TreeNodeTypeEnum.Initiator, "发起人"), ParallelBranch(ParId, "并行审批", Branch("p1", "分支1", null, Chain(Approval(LegalId, "法务", "user:legal"))), Branch("p2", "分支2", null, Chain(Approval(FinId, "财务", "user:fin")))), Cc(EndCcId, "结束抄送", "user:end")), }; var graph = TreeToGraphCompiler.Compile(tree); // 两个 Parallel 节点:Fork(多出边)+ Join(多入边) var parallels = graph.Nodes.Where(n => n.Shape == WorkflowShapeMap.Shapes.Parallel).ToList(); parallels.Should().HaveCount(2); var fork = parallels.First(p => graph.Edges.Count(e => e.Source.Cell == p.Id) > 1); var join = parallels.First(p => graph.Edges.Count(e => e.Target.Cell == p.Id) > 1); fork.Should().NotBeNull(); join.Should().NotBeNull(); fork.Id.Should().NotBe(join.Id); var act = () => WorkflowGraphValidator.Validate(graph); act.Should().NotThrow(); } // ====== 5. 嵌套:条件分支内再套并行分支 ====== [Fact] public void Compile_NestedBranches_StillValidates() { var tree = new WorkflowTree { Root = Chain( Node(RootId, TreeNodeTypeEnum.Initiator, "发起人"), ConditionBranch(CondId, "金额", Branch("b1", "大额", "amount > 10000", Chain(ParallelBranch(ParId, "并行", Branch("p1", "A", null, Chain(Approval(LegalId, "法务", "user:legal"))), Branch("p2", "B", null, Chain(Approval(FinId, "财务", "user:fin")))))), Branch("b2", "默认", null, Chain(Approval(LeadId, "组长", "user:lead")))), Cc(C1Id, "抄送", "user:fin")), }; var graph = TreeToGraphCompiler.Compile(tree); // 复杂嵌套仍应通过校验 var act = () => WorkflowGraphValidator.Validate(graph); act.Should().NotThrow(); // 且只有一个 Start / 一个 End graph.Nodes.Count(n => n.Shape == WorkflowShapeMap.Shapes.Start).Should().Be(1); graph.Nodes.Count(n => n.Shape == WorkflowShapeMap.Shapes.End).Should().Be(1); } // ====== 6. 节点 id 必须全部是合法 Guid(引擎 ParseGuid 硬约束) ====== [Fact] public void Compile_AllNodeAndEdgeIds_AreGuidStrings() { var tree = new WorkflowTree { Root = Chain( Node(RootId, TreeNodeTypeEnum.Initiator, "发起人"), Approval(A1Id, "审批", "user:u1")), }; var graph = TreeToGraphCompiler.Compile(tree); foreach (var node in graph.Nodes) { Guid.TryParse(node.Id, out _).Should().BeTrue($"节点 id 必须是合法 Guid:{node.Id}"); } foreach (var edge in graph.Edges) { Guid.TryParse(edge.Id, out _).Should().BeTrue($"边 id 必须是合法 Guid:{edge.Id}"); Guid.TryParse(edge.Source.Cell, out _).Should().BeTrue(); Guid.TryParse(edge.Target.Cell, out _).Should().BeTrue(); } } // ====== 7. 只有发起人(空链)也能跑 ====== [Fact] public void Compile_OnlyInitiator_StartConnectsToEndDirectly() { var tree = new WorkflowTree { Root = Node(RootId, TreeNodeTypeEnum.Initiator, "发起人"), }; var graph = TreeToGraphCompiler.Compile(tree); var start = graph.Nodes.Single(n => n.Shape == WorkflowShapeMap.Shapes.Start); var end = graph.Nodes.Single(n => n.Shape == WorkflowShapeMap.Shapes.End); // Initiator 的 id 成为 Start 的 id start.Id.Should().Be(RootId); graph.Edges.Should().ContainSingle(e => e.Source.Cell == start.Id && e.Target.Cell == end.Id); var act = () => WorkflowGraphValidator.Validate(graph); act.Should().NotThrow(); } // ====== 8. 审批节点配置透传到 node.data ====== [Fact] public void Compile_ApprovalConfig_PreservedInNodeData() { var tree = new WorkflowTree { Root = Chain( Node(RootId, TreeNodeTypeEnum.Initiator, "发起人"), Approval(A1Id, "主管审批", "user:abc")), }; var graph = TreeToGraphCompiler.Compile(tree); var approval = graph.Nodes.Single(n => n.Id == A1Id); approval.Data.Should().NotBeNull(); approval.Data!.Value.GetProperty("title").GetString().Should().Be("主管审批"); // assigneeRules 应原样透传 approval.Data.Value.GetProperty("assigneeRules") .EnumerateArray().First().GetString().Should().Be("user:abc"); } }