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;
///
/// 会签/或签/退回测试(TDD)。
///
/// 设计:在 Approval 节点 config 增加字段(不新增 NodeType,向后兼容):
/// - approvalMode: "single"(默认) | "countersign"(会签-全部通过) | "any"(或签-任一通过)
/// - assigneeRules: ["user:xxx", "user:yyy", ...] 多受理人
/// - minApprovals: 会签模式下最少通过数(默认=全部)
///
public class MultiApprovalTests
{
private readonly WorkflowDbContext _dbContext;
private readonly ProcessEngine _engine;
public MultiApprovalTests()
{
var options = new DbContextOptionsBuilder()
.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):全部通过才推进
// ============================================================
///
/// 会签模式:3 个受理人,第 1 个通过后流程不推进(仍有待办任务)。
///
[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");
}
///
/// 会签模式:全部通过后流程才推进到下游。
///
[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):任一通过即推进
// ============================================================
///
/// 或签模式:第 1 个通过后立即推进,其余任务自动关闭。
///
[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):回到上一审批节点
// ============================================================
///
/// 退回:第 2 个审批节点退回时,token 回到第 1 个审批节点重新创建任务。
///
[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);
}
// ============================================================
// 端到端组合:会签 × 条件路由(消解"条件求值器没弄好"的疑虑)
// ============================================================
///
/// 会签通过后,Condition 节点根据实例变量正确路由到不同分支。
/// 验证:会签引擎与条件求值器的端到端协作(用户担忧的"流程引擎+条件器没弄好")。
///
[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("不应走普通分支");
}
///
/// 或签模式:第 1 个通过即推进,验证其余任务的 Comment 字段不影响判定。
/// 锁定或签的"竞争"语义——只看状态不看评论。
///
[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",
};
/// 在审批节点预创建 Active token(填审计字段,绕过 InMemory 不跑拦截器)。
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 nodes, List 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);
}
}