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