using System.Text.Json;
using FluentAssertions;
using Workflow.Application.Engine;
using Workflow.Application.Features.WorkflowDefinitions.Commands;
using Workflow.Application.Features.WorkflowDefinitions.Handlers;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Xunit;
using ShapeMap = global::Workflow.Domain.Engine.WorkflowShapeMap;
namespace BuildFlow.Workflow.Tests.Integration;
///
/// 钉钉式树定义的端到端发布测试:构造带 DefinitionTree 的草稿定义 →
/// 发布 → 验证编译产出 DefinitionJson、通过图校验、生成不可变快照、状态变 Published。
/// 这条链路是「树编辑器保存 → 发布 → 引擎可运行」的核心保障。
///
public class TreeDefinitionPublishTests
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
/// 构造一棵典型的钉钉式树:发起人 → 审批 → 条件分支(大额/默认) → 抄送。
private static string BuildTreeJson()
{
var tree = new
{
root = new
{
id = Guid.NewGuid().ToString(),
type = "Initiator",
config = new { title = "发起人" },
child = new
{
id = Guid.NewGuid().ToString(),
type = "Approval",
config = new
{
title = "主管审批",
approvalMode = "countersign",
assigneeRules = new[] { "user:11111111-1111-1111-1111-111111111111" },
},
child = new
{
id = Guid.NewGuid().ToString(),
type = "ConditionBranch",
config = new { title = "金额分支" },
branches = new object[]
{
new
{
id = Guid.NewGuid().ToString(),
label = "大额",
condition = "amount > 5000",
child = new
{
id = Guid.NewGuid().ToString(),
type = "Approval",
config = new
{
title = "总监审批",
approvalMode = "countersign",
assigneeRules = new[] { "user:22222222-2222-2222-2222-222222222222" },
},
},
},
new
{
id = Guid.NewGuid().ToString(),
label = "默认",
condition = (string?)null,
child = (object?)null,
},
},
child = new
{
id = Guid.NewGuid().ToString(),
type = "Cc",
config = new
{
title = "抄送财务",
recipients = new[] { "user:33333333-3333-3333-3333-333333333333" },
},
},
},
},
},
};
return JsonSerializer.Serialize(tree, JsonOptions);
}
[Fact]
public async Task Publish_TreeDefinition_CompilesToValidGraph_AndCreatesSnapshot()
{
// Arrange:构造一个 Draft 草稿,只填 DefinitionTree(不填 DefinitionJson)
await using var db = TestDbFactory.Create();
var definitionId = Guid.NewGuid();
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = definitionId,
Name = "报销审批",
Code = $"expense-{definitionId:N}",
Status = DefinitionStatus.Draft,
Version = 1,
IsEnabled = true,
DefinitionTree = BuildTreeJson(),
});
await db.SaveChangesAsync();
// Act:发布
var handler = new PublishWorkflowDefinitionCommandHandler(db);
await handler.Handle(new PublishWorkflowDefinitionCommand(definitionId), CancellationToken.None);
// Assert:定义状态变 Published、版本自增、DefinitionJson 被编译出来
var updated = await db.WorkflowDefinitions.FindAsync(definitionId);
updated.Should().NotBeNull();
updated!.Status.Should().Be(DefinitionStatus.Published);
updated.Version.Should().Be(2);
updated.DefinitionJson.Should().NotBeNullOrEmpty("树必须被编译成图");
// 编译出的图必须能被引擎解析且通过校验
var graph = WorkflowGraphParser.Parse(updated.DefinitionJson);
graph.Nodes.Should().NotBeEmpty();
var act = () => WorkflowGraphValidator.Validate(graph);
act.Should().NotThrow("编译产物必须通过发布校验");
// 图结构断言:含 Start/End/Approval/Cc/Condition
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.Start);
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.End);
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.Approval);
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.Cc);
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.Condition);
// 快照已生成,且 DefinitionTree/DefinitionJson 都写进快照
var snapshot = db.WorkflowDefinitionSnapshots
.FirstOrDefault(s => s.DefinitionId == definitionId && s.Version == 2);
snapshot.Should().NotBeNull();
snapshot!.DefinitionJson.Should().NotBeNullOrEmpty();
snapshot.DefinitionTree.Should().NotBeNullOrEmpty("快照应同时保存树供审计回显");
}
[Fact]
public async Task Publish_LegacyDefinitionWithoutTree_StillWorks()
{
// 兼容老的自由画布定义(无 DefinitionTree,只有 DefinitionJson)也能发布
await using var db = TestDbFactory.Create();
var definitionId = Guid.NewGuid();
// 手工构造一个最小合法图:Start → End
var minGraph = new
{
nodes = new[]
{
new { id = Guid.NewGuid().ToString(), shape = "wf-start", data = new { title = "开始" } },
new { id = Guid.NewGuid().ToString(), shape = "wf-end", data = new { title = "结束" } },
},
edges = new[]
{
new
{
id = Guid.NewGuid().ToString(),
shape = "wf-edge",
source = new { cell = "" },
target = new { cell = "" },
data = new { edgeType = 0 },
},
},
};
// 填好 source/target 指向上面两个节点
var nodes = minGraph.nodes;
var edges = new[]
{
new
{
id = Guid.NewGuid().ToString(),
shape = "wf-edge",
source = new { cell = nodes[0].id },
target = new { cell = nodes[1].id },
data = new { edgeType = 0 },
},
};
var graphJson = JsonSerializer.Serialize(new { nodes, edges }, JsonOptions);
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = definitionId,
Name = "老流程",
Code = $"legacy-{definitionId:N}",
Status = DefinitionStatus.Draft,
Version = 1,
IsEnabled = true,
DefinitionJson = graphJson,
});
await db.SaveChangesAsync();
var handler = new PublishWorkflowDefinitionCommandHandler(db);
var act = () => handler.Handle(new PublishWorkflowDefinitionCommand(definitionId), CancellationToken.None);
await act.Should().NotThrowAsync("无树的老定义走原 DefinitionJson 路径");
var updated = await db.WorkflowDefinitions.FindAsync(definitionId);
updated!.Status.Should().Be(DefinitionStatus.Published);
}
}