using global::BuildFlow.Shared.Domain.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Workflow.Application.Engine;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Domain.Expressions;
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
using InstanceStatus = Workflow.Domain.Enums.InstanceStatus;
namespace BuildFlow.Workflow.Tests.Engine;
using FluentAssertions;
using Xunit;
///
/// 依次审批(Sequential)测试:A → B → C 按顺序逐个审,全部通过才推进,任一驳回中断。
/// 这是钉钉式审批流的核心审批方式,区别于会签(同时)/或签(任一)。
///
public class SequentialApprovalTests
{
private readonly WorkflowDbContext _dbContext;
private readonly ProcessEngine _engine;
public SequentialApprovalTests()
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_dbContext = new WorkflowDbContext(options);
// InMemory 不跑审计拦截器,手动填充非空审计字段
_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());
}
/// 启动依次审批节点:只创建第 0 个审批人的任务,记录完整序列。
[Fact]
public async Task Sequential_Start_CreatesOnlyFirstAssigneeTask()
{
var (approvalNode, instance, endNode) = await SetupSequentialFlow(3);
await _engine.StartAsync(instance);
var tasks = await _dbContext.WorkflowTasks
.Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending)
.ToListAsync();
// 只建第 0 个任务(区别于会签一次性建 3 个)
tasks.Should().HaveCount(1);
tasks[0].SequentialOrder.Should().Be(0);
tasks[0].SequentialAssignees.Should().NotBeNull("应记录完整序列供逐个推进");
tasks[0].SequentialAssignees!.Should().Contain("user:");
}
/// 依次审批:第 1 个通过后建第 2 个任务,token 不推进。
[Fact]
public async Task Sequential_FirstApproved_CreatesSecondTaskWithoutPropagating()
{
var (approvalNode, instance, endNode) = await SetupSequentialFlow(3);
await _engine.StartAsync(instance);
var firstTask = await _dbContext.WorkflowTasks
.FirstAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
// 第 1 个通过
await _engine.CompleteTaskAsync(firstTask, TaskResult.Approved);
// 第 1 个任务已 Approved
firstTask.Status.Should().Be(TaskStatus.Approved);
// 新建了第 2 个任务(SequentialOrder=1),仍是 Pending
var secondTask = await _dbContext.WorkflowTasks
.SingleOrDefaultAsync(t => t.NodeId == approvalNode.Id && t.SequentialOrder == 1 && t.Status == TaskStatus.Pending);
secondTask.Should().NotBeNull("应创建下一个审批人任务");
secondTask!.SequentialAssignees.Should().Be(firstTask.SequentialAssignees);
// token 未推进到 End
var endTokens = await _dbContext.WorkflowTokens.CountAsync(t => t.NodeId == endNode.Id);
endTokens.Should().Be(0, "依次审批未完成,token 不应到达 End");
}
/// 依次审批:全部通过后推进到下游 End。
[Fact]
public async Task Sequential_AllApproved_PropagatesDownstream()
{
var (approvalNode, instance, endNode) = await SetupSequentialFlow(3);
await _engine.StartAsync(instance);
// 逐个通过 3 个审批人
for (var i = 0; i < 3; i++)
{
var current = await _dbContext.WorkflowTasks
.FirstAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
await _engine.CompleteTaskAsync(current, TaskResult.Approved);
}
// 全部通过后 token 推进,实例完成
instance.Status.Should().Be(InstanceStatus.Completed);
}
/// 依次审批:中间任一驳回则中断(走 Rejected 边语义;本测试无 Rejected 边故验证不继续建任务)。
[Fact]
public async Task Sequential_RejectedInMiddle_StopsSequentialChain()
{
var user1 = Guid.NewGuid();
var user2 = Guid.NewGuid();
var approvalNode = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "seq",
CreatedBy = "test",
UpdatedBy = "test",
Config = $$"""{ "approvalMode": "sequential", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
};
// 提供一条 Rejected 边回到自己(避免 No reject edge 异常),再一条 Normal 边到 End
var startNode = CreateNode(NodeType.Start);
var endNode = CreateNode(NodeType.End);
var edgeStart = CreateEdge(startNode, approvalNode);
var edgeReject = CreateEdge(approvalNode, approvalNode);
edgeReject.EdgeType = EdgeType.Rejected;
var edgeApprove = CreateEdge(approvalNode, endNode);
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edgeReject, edgeApprove]);
await _engine.StartAsync(instance);
var firstTask = await _dbContext.WorkflowTasks
.FirstAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
// 第 1 个驳回 → 走 Rejected 边,不建第 2 个任务
await _engine.CompleteTaskAsync(firstTask, TaskResult.Rejected);
firstTask.Status.Should().Be(TaskStatus.Rejected);
var secondTask = await _dbContext.WorkflowTasks
.CountAsync(t => t.NodeId == approvalNode.Id && t.SequentialOrder == 1);
secondTask.Should().Be(0, "驳回后不应继续依次建任务");
}
// ===== Setup helpers(与 MultiApprovalTests 同范式)=====
private async Task<(WorkflowNode ApprovalNode, WorkflowInstance Instance, WorkflowNode EndNode)> SetupSequentialFlow(int assigneeCount)
{
var users = Enumerable.Range(0, assigneeCount).Select(_ => Guid.NewGuid()).ToArray();
var rules = users.Select(u => $"user:{u}").ToArray();
var rulesJson = System.Text.Json.JsonSerializer.Serialize(rules);
var approvalNode = new WorkflowNode
{
Id = Guid.NewGuid(),
NodeType = NodeType.Approval,
Name = "依次审批",
CreatedBy = "test",
UpdatedBy = "test",
Config = $$"""{ "approvalMode": "sequential", "assigneeRules": {{rulesJson}} }""",
};
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]);
// PersistSetup 内部已 SaveChanges;返回前 await 无副作用,保持签名一致
await Task.CompletedTask;
return (approvalNode, instance, endNode);
}
private static WorkflowNode CreateNode(NodeType type) => new()
{
Id = Guid.NewGuid(),
NodeType = type,
Name = type.ToString().ToLowerInvariant(),
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",
};
private (WorkflowDefinition Definition, WorkflowInstance Instance) PersistSetup(
List nodes, List edges)
{
var definition = new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "seq-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 = "seq-test",
Status = InstanceStatus.Running,
Variables = "{}",
CreatedBy = "test",
UpdatedBy = "test",
};
_dbContext.WorkflowInstances.Add(instance);
_dbContext.SaveChanges();
return (definition, instance);
}
}