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");
}
}