build-flow.api/tests/BuildFlow.Workflow.Tests/Engine/TreeToGraphCompilerTests.cs
xiangning 51eea796e2 refactor(rag): 重构AI模型相关代码结构和数据库迁移逻辑
- 将AI模型相关端点、命令、处理器移动到模块内部统一管理
- 优化数据库迁移脚本,添加对不同环境表状态的兼容处理逻辑
- 为异常处理添加错误码枚举参数,统一错误处理方式
- 重构多处业务逻辑中的异常抛出方式,增强代码一致性
- 移除独立的组件清单分组和项目定义,简化前端物料面板数据结构
- 修复测试代码中实体引用路径错误问题
- 添加新的监控模块测试项目到解决方案文件中
2026-07-20 08:25:18 +08:00

290 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 Workflow.Domain.Enums;
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, 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 };
/// <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, 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");
}
}