xiangning 17b7e9996e chore(dependencies): 移除未使用的 using 语句
- 从多个文件中移除未使用的 System.Linq.Dynamic.Core 引用
- 从多个文件中移除未使用的 Microsoft.EntityFrameworkCore 引用
- 从 IM 模块中移除未使用的 MongoDB.Driver.Linq 引用
- 清理 Handler 文件中的冗余命名空间引用
- 优化代码结构减少不必要的依赖引入
- 提高代码整洁性和编译效率
2026-07-02 14:53:56 +08:00

482 lines
18 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.Application.Engine;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Domain.Expressions;
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
namespace BuildFlow.Workflow.Tests.Engine;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using global::BuildFlow.Shared.Domain.Common;
using Xunit;
/// <summary>
/// 会签/或签/退回测试(TDD)。
///
/// 设计:在 Approval 节点 config 增加字段(不新增 NodeType,向后兼容):
/// - approvalMode: "single"(默认) | "countersign"(会签-全部通过) | "any"(或签-任一通过)
/// - assigneeRules: ["user:xxx", "user:yyy", ...] 多受理人
/// - minApprovals: 会签模式下最少通过数(默认=全部)
/// </summary>
public class MultiApprovalTests
{
private readonly WorkflowDbContext _dbContext;
private readonly ProcessEngine _engine;
public MultiApprovalTests()
{
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_dbContext = new WorkflowDbContext(options);
// 模拟 AuditInterceptor:InMemory 不跑拦截器,此处手动填充审计字段,
// 使 StartAsync/CompleteTaskAsync 创建的 token/task 能通过非空校验。
_dbContext.SavingChanges += (sender, _) =>
{
if (sender is not DbContext ctx) return;
foreach (var entry in ctx.ChangeTracker.Entries())
{
if (entry.Entity is IAuditable a)
{
a.CreatedBy ??= "test";
a.UpdatedBy ??= "test";
}
if (entry.Entity is IHasOperatorIP o)
{
o.OperatorIP ??= "127.0.0.1";
}
}
};
_engine = new ProcessEngine(_dbContext, new ServiceCollection().BuildServiceProvider(), new ConditionEvaluator());
}
// ============================================================
// 会签(Countersign):全部通过才推进
// ============================================================
/// <summary>
/// 会签模式:3 个受理人,第 1 个通过后流程不推进(仍有待办任务)。
/// </summary>
[Fact]
public async Task Countersign_PartialApproval_DoesNotPropagate()
{
var user1 = Guid.NewGuid();
var user2 = Guid.NewGuid();
var user3 = Guid.NewGuid();
var approvalNode = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "会签节点",
CreatedBy = "test",
UpdatedBy = "test",
Config = $$"""{ "approvalMode": "countersign", "assigneeRules": ["user:{{user1}}", "user:{{user2}}", "user:{{user3}}"] }""",
};
var startNode = CreateNode(NodeType.Start);
var endNode = CreateNode(NodeType.End);
var edgeStart = CreateEdge(startNode, approvalNode);
var edge = CreateEdge(approvalNode, endNode);
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
await _engine.StartAsync(instance);
// 应为 3 个受理人各创建 1 个 Pending 任务
var tasks = await _dbContext.WorkflowTasks
.Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending)
.ToListAsync();
tasks.Should().HaveCount(3);
// 第 1 个人通过
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
// 还有 2 个待办 → 流程不应推进到 End
var remainingPending = await _dbContext.WorkflowTasks
.CountAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
remainingPending.Should().Be(2);
var endTokens = await _dbContext.WorkflowTokens
.CountAsync(t => t.NodeId == endNode.Id);
endTokens.Should().Be(0, "会签未完成,token 不应到达 End");
}
/// <summary>
/// 会签模式:全部通过后流程才推进到下游。
/// </summary>
[Fact]
public async Task Countersign_AllApproved_PropagatesDownstream()
{
var user1 = Guid.NewGuid();
var user2 = Guid.NewGuid();
var approvalNode = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "approval",
CreatedBy = "test",
UpdatedBy = "test",
Config = $$"""{ "approvalMode": "countersign", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
};
var startNode = CreateNode(NodeType.Start);
var endNode = CreateNode(NodeType.End);
var edgeStart = CreateEdge(startNode, approvalNode);
var edge = CreateEdge(approvalNode, endNode);
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
await _engine.StartAsync(instance);
var tasks = await _dbContext.WorkflowTasks
.Where(t => t.NodeId == approvalNode.Id)
.ToListAsync();
// 全部通过
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
await _engine.CompleteTaskAsync(tasks[1], TaskResult.Approved);
// 实例应完成(token 到达 End 被消费)
instance.Status.Should().Be(InstanceStatus.Completed);
}
// ============================================================
// 或签(Any):任一通过即推进
// ============================================================
/// <summary>
/// 或签模式:第 1 个通过后立即推进,其余任务自动关闭。
/// </summary>
[Fact]
public async Task AnyApproval_FirstApproval_PropagatesAndClosesOthers()
{
var user1 = Guid.NewGuid();
var user2 = Guid.NewGuid();
var approvalNode = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "approval",
CreatedBy = "test",
UpdatedBy = "test",
Config = $$"""{ "approvalMode": "any", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
};
var startNode = CreateNode(NodeType.Start);
var endNode = CreateNode(NodeType.End);
var edgeStart = CreateEdge(startNode, approvalNode);
var edge = CreateEdge(approvalNode, endNode);
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
await _engine.StartAsync(instance);
var tasks = await _dbContext.WorkflowTasks
.Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending)
.ToListAsync();
tasks.Should().HaveCount(2);
// 第 1 个人通过
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
// 流程应已推进 → 实例完成
instance.Status.Should().Be(InstanceStatus.Completed);
// 其余待办任务应自动关闭(不应残留 Pending)
var remainingPending = await _dbContext.WorkflowTasks
.CountAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
remainingPending.Should().Be(0, "或签通过后其余任务应自动关闭");
}
// ============================================================
// 向后兼容:无 approvalMode 时保持单审批行为
// ============================================================
[Fact]
public async Task NoApprovalMode_DefaultsToSingleApproval()
{
var approvalNode = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "approval",
CreatedBy = "test",
UpdatedBy = "test",
Config = """{ "assigneeRule": "role:approver" }""",
};
var startNode = CreateNode(NodeType.Start);
var endNode = CreateNode(NodeType.End);
var edgeStart = CreateEdge(startNode, approvalNode);
var edge = CreateEdge(approvalNode, endNode);
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
await _engine.StartAsync(instance);
var tasks = await _dbContext.WorkflowTasks
.CountAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
tasks.Should().Be(1, "无 approvalMode 默认单审批");
}
// ============================================================
// 退回(Return):回到上一审批节点
// ============================================================
/// <summary>
/// 退回:第 2 个审批节点退回时,token 回到第 1 个审批节点重新创建任务。
/// </summary>
[Fact]
public async Task ReturnTask_GoesBackToPreviousApprovalNode()
{
var firstApproval = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "approval",
CreatedBy = "test",
UpdatedBy = "test",
Config = """{ "assigneeRule": "role:approver1" }""",
};
var secondApproval = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "approval",
CreatedBy = "test",
UpdatedBy = "test",
Config = """{ "assigneeRule": "role:approver2" }""",
};
var endNode = CreateNode(NodeType.End);
var startNode = CreateNode(NodeType.Start);
var edgeStart = CreateEdge(startNode, firstApproval);
var edge1 = CreateEdge(firstApproval, secondApproval);
var edge2 = CreateEdge(secondApproval, endNode);
var (_, instance) = PersistSetup([startNode, firstApproval, secondApproval, endNode], [edgeStart, edge1, edge2]);
await _engine.StartAsync(instance);
// 第 1 个审批通过 → token 到第 2 个审批
var task1 = await _dbContext.WorkflowTasks.FirstAsync(t => t.NodeId == firstApproval.Id);
await _engine.CompleteTaskAsync(task1, TaskResult.Approved);
// 第 2 个审批任务存在
var task2 = await _dbContext.WorkflowTasks.FirstAsync(t => t.NodeId == secondApproval.Id);
task2.Status.Should().Be(TaskStatus.Pending);
// 退回:应回到第 1 个审批节点
await _engine.ReturnTaskAsync(task2);
// 第 1 个审批节点应有新的 Pending 任务(重新创建)
var returnedTask = await _dbContext.WorkflowTasks
.FirstOrDefaultAsync(t => t.NodeId == firstApproval.Id && t.Status == TaskStatus.Pending);
returnedTask.Should().NotBeNull("退回应在第 1 个审批节点重新创建任务");
// 第 2 个审批任务应不再是 Pending
await _dbContext.Entry(task2).ReloadAsync();
task2.Status.Should().NotBe(TaskStatus.Pending);
}
// ============================================================
// 端到端组合:会签 × 条件路由(消解"条件求值器没弄好"的疑虑)
// ============================================================
/// <summary>
/// 会签通过后,Condition 节点根据实例变量正确路由到不同分支。
/// 验证:会签引擎与条件求值器的端到端协作(用户担忧的"流程引擎+条件器没弄好")。
/// </summary>
[Fact]
public async Task CountersignApproved_ThenConditionRoutesBasedOnVariable()
{
var user1 = Guid.NewGuid();
var user2 = Guid.NewGuid();
var approvalNode = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "会签审批",
CreatedBy = "test",
UpdatedBy = "test",
Config = $$"""{ "approvalMode": "countersign", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
};
var conditionNode = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Condition,
Name = "金额判断",
CreatedBy = "test",
UpdatedBy = "test",
Config = "{}",
};
// 金额 > 1000 走高额分支
var highAmountEnd = CreateNode(NodeType.End);
highAmountEnd.Name = "高额终态";
var normalEnd = CreateNode(NodeType.End);
normalEnd.Name = "普通终态";
var startNode = CreateNode(NodeType.Start);
var edgeStart = CreateEdge(startNode, approvalNode);
var edgeToCondition = CreateEdge(approvalNode, conditionNode);
var edgeHigh = new WorkflowEdge
{
Id = Guid.NewGuid(),
DefinitionId = Guid.Empty, // PersistSetup 会回填
SourceNodeId = conditionNode.Id,
TargetNodeId = highAmountEnd.Id,
EdgeType = EdgeType.Normal,
Condition = "amount > 1000",
Order = 1,
CreatedBy = "test",
UpdatedBy = "test",
};
var edgeNormal = new WorkflowEdge
{
Id = Guid.NewGuid(),
DefinitionId = Guid.Empty,
SourceNodeId = conditionNode.Id,
TargetNodeId = normalEnd.Id,
EdgeType = EdgeType.Normal,
Condition = "amount <= 1000",
Order = 2,
CreatedBy = "test",
UpdatedBy = "test",
};
var (_, instance) = PersistSetup(
[startNode, approvalNode, conditionNode, highAmountEnd, normalEnd],
[edgeStart, edgeToCondition, edgeHigh, edgeNormal]);
// 设置实例变量(模拟表单数据回流)
instance.Variables = """{ "amount": 5000 }""";
await _dbContext.SaveChangesAsync();
await _engine.StartAsync(instance);
// 会签全部通过
var tasks = await _dbContext.WorkflowTasks
.Where(t => t.NodeId == approvalNode.Id)
.ToListAsync();
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
await _engine.CompleteTaskAsync(tasks[1], TaskResult.Approved);
// 会签通过 → Condition 求值 → 应路由到高额分支(amount=5000 > 1000)
var highEndToken = await _dbContext.WorkflowTokens
.AnyAsync(t => t.NodeId == highAmountEnd.Id);
var normalEndToken = await _dbContext.WorkflowTokens
.AnyAsync(t => t.NodeId == normalEnd.Id);
highEndToken.Should().BeTrue("amount=5000 > 1000 应走高额分支");
normalEndToken.Should().BeFalse("不应走普通分支");
}
/// <summary>
/// 或签模式:第 1 个通过即推进,验证其余任务的 Comment 字段不影响判定。
/// 锁定或签的"竞争"语义——只看状态不看评论。
/// </summary>
[Fact]
public async Task AnyApproval_PartialRejectDoesNotAffectAlreadyPropagated()
{
var user1 = Guid.NewGuid();
var user2 = Guid.NewGuid();
var approvalNode = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "或签",
CreatedBy = "test",
UpdatedBy = "test",
Config = $$"""{ "approvalMode": "any", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
};
var endNode = CreateNode(NodeType.End);
var startNode = CreateNode(NodeType.Start);
var edgeStart = CreateEdge(startNode, approvalNode);
var edge = CreateEdge(approvalNode, endNode);
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
await _engine.StartAsync(instance);
var tasks = await _dbContext.WorkflowTasks
.Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending)
.ToListAsync();
// 第 1 个通过 → 立即推进
await _engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
instance.Status.Should().Be(InstanceStatus.Completed);
// 第 2 个任务已被自动关闭(不再 Pending)
await _dbContext.Entry(tasks[1]).ReloadAsync();
tasks[1].Status.Should().NotBe(TaskStatus.Pending, "或签通过后其余任务应自动关闭");
}
// ============================================================
// 辅助方法
// ============================================================
private static WorkflowNode CreateNode(NodeType type) => new()
{
Id = Guid.NewGuid(),
NodeType = type,
Config = "{}",
Name = type.ToString(),
CreatedBy = "test",
UpdatedBy = "test",
};
private static WorkflowEdge CreateEdge(WorkflowNode source, WorkflowNode target) => new()
{
Id = Guid.NewGuid(),
SourceNodeId = source.Id,
TargetNodeId = target.Id,
EdgeType = EdgeType.Normal,
CreatedBy = "test",
UpdatedBy = "test",
};
/// <summary>在审批节点预创建 Active token(填审计字段,绕过 InMemory 不跑拦截器)。</summary>
private WorkflowToken SeedApprovalToken(WorkflowInstance instance, WorkflowNode node)
{
var token = new WorkflowToken
{
Id = Guid.NewGuid(),
InstanceId = instance.Id,
NodeId = node.Id,
Status = TokenStatus.Active,
CreatedBy = "test",
UpdatedBy = "test",
};
_dbContext.WorkflowTokens.Add(token);
_dbContext.SaveChanges();
return token;
}
private (WorkflowDefinition Definition, WorkflowInstance Instance) PersistSetup(
List<WorkflowNode> nodes, List<WorkflowEdge> edges)
{
var definition = new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "test-flow",
Status = DefinitionStatus.Published,
CreatedBy = "test",
UpdatedBy = "test",
};
foreach (var n in nodes) { n.DefinitionId = definition.Id; _dbContext.WorkflowNodes.Add(n); }
foreach (var e in edges) { e.DefinitionId = definition.Id; _dbContext.WorkflowEdges.Add(e); }
_dbContext.WorkflowDefinitions.Add(definition);
var instance = new WorkflowInstance
{
Id = Guid.NewGuid(),
DefinitionId = definition.Id,
Title = "test-instance",
Status = InstanceStatus.Running, // 直接置 Running,绕过 StartAsync
Variables = "{}",
CreatedBy = "test",
UpdatedBy = "test",
};
_dbContext.WorkflowInstances.Add(instance);
_dbContext.SaveChanges();
return (definition, instance);
}
}