- 从文件模块的多部分上传处理器中移除未使用的实体引用 - 从即时通讯模块的群组成员处理器中移除未使用的实体引用 - 从RAG模块的AI模型配置中移除未使用的实体引用 - 从监控模块的告警规则配置中移除未使用的实体引用 - 从平台模块的角色权限处理器中移除未使用的实体引用 - 从脚本模块的执行日志实体引用中进行清理 - 从多个基础设施配置类中移除未使用的依赖项 - 优化测试文件中的导入声明,移除不必要的引用
203 lines
8.4 KiB
C#
203 lines
8.4 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 钉钉式树定义的端到端发布测试:构造带 DefinitionTree 的草稿定义 →
|
||
/// 发布 → 验证编译产出 DefinitionJson、通过图校验、生成不可变快照、状态变 Published。
|
||
/// 这条链路是「树编辑器保存 → 发布 → 引擎可运行」的核心保障。
|
||
/// </summary>
|
||
public class TreeDefinitionPublishTests
|
||
{
|
||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||
|
||
/// <summary>构造一棵典型的钉钉式树:发起人 → 审批 → 条件分支(大额/默认) → 抄送。</summary>
|
||
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);
|
||
}
|
||
}
|