build-flow.api/tests/BuildFlow.Workflow.Tests/Engine/TreeToGraphCompilerTests.cs
xiangning 6c110b579a refactor(dependencies): 移除未使用的域实体引用
- 从文件模块的多部分上传处理器中移除未使用的实体引用
- 从即时通讯模块的群组成员处理器中移除未使用的实体引用
- 从RAG模块的AI模型配置中移除未使用的实体引用
- 从监控模块的告警规则配置中移除未使用的实体引用
- 从平台模块的角色权限处理器中移除未使用的实体引用
- 从脚本模块的执行日志实体引用中进行清理
- 从多个基础设施配置类中移除未使用的依赖项
- 优化测试文件中的导入声明,移除不必要的引用
2026-07-19 14:09:47 +08:00

289 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text.Json;
using FluentAssertions;
using Workflow.Application.Engine;
using Workflow.Domain.Engine;
using Xunit;
namespace BuildFlow.Workflow.Tests.Engine;
/// <summary>
/// TreeToGraphCompiler 单元测试:覆盖顺序/条件/并行/嵌套四种拓扑,
/// 并断言编译产出的图能通过 WorkflowGraphValidator(发布校验)。
/// </summary>
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, TreeNodeType 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, TreeNodeType.Approval, title, new
{
approvalMode = "countersign",
assigneeRules = assignees,
});
private static TreeNode Cc(string id, string title, params string[] recipients) =>
Node(id, TreeNodeType.Cc, title, new { recipients });
private static TreeNode ConditionBranch(string id, string title, params TreeBranch[] branches) =>
new()
{
Id = id,
Type = TreeNodeType.ConditionBranch,
Title = title,
Branches = branches.ToList(),
};
private static TreeNode ParallelBranch(string id, string title, params TreeBranch[] branches) =>
new()
{
Id = id,
Type = TreeNodeType.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 };
/// <summary>把节点链按 child 顺序串起来,返回链头。</summary>
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, TreeNodeType.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, TreeNodeType.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, TreeNodeType.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, TreeNodeType.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, TreeNodeType.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, TreeNodeType.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, TreeNodeType.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");
}
}