using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Workflow.Application.Engine;
using Workflow.Application.Notifications;
using Workflow.Application.Scheduler;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Domain.Expressions;
using Workflow.Infrastructure.Persistence;
using Xunit;
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
namespace Workflow.Tests.Scheduler;
///
/// 测试用的 Noop 通知服务:不真正落库通知,仅记录调用情况,便于断言超时调度器是否触发了通知。
///
internal sealed class NoopNotificationService : INotificationService
{
public int TimeoutCallCount { get; private set; }
public List TimeoutTasks { get; } = [];
public List AutoApprovedFlags { get; } = [];
public Task NotifyTaskArrivedAsync(WorkflowTask task, CancellationToken ct = default) => Task.CompletedTask;
public Task NotifyTaskApprovedAsync(WorkflowTask task, CancellationToken ct = default) => Task.CompletedTask;
public Task NotifyTaskRejectedAsync(WorkflowTask task, CancellationToken ct = default) => Task.CompletedTask;
public Task NotifyTaskUrgedAsync(WorkflowTask task, CancellationToken ct = default) => Task.CompletedTask;
public Task NotifyTaskTimeoutAsync(WorkflowTask task, bool autoApproved, CancellationToken ct = default)
{
TimeoutCallCount++;
TimeoutTasks.Add(task);
AutoApprovedFlags.Add(autoApproved);
return Task.CompletedTask;
}
}
///
/// OverdueTaskProcessor 核心逻辑测试:逾期任务自动处理、Suspended 守卫、autoApproveOnTimeout 解析、
/// DueAt=null 跳过、空转。直接构造处理器实例,不依赖 HostedService/DI scope。
///
public class TimeoutSchedulerTests
{
private static WorkflowDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new WorkflowDbContext(options);
}
/// 构造 OverdueTaskProcessor:共享 DbContext + 引擎 + 通知捕获器。
private static (OverdueTaskProcessor processor, WorkflowDbContext db, NoopNotificationService notifier) Build(WorkflowDbContext db)
{
var notifier = new NoopNotificationService();
// ProcessEngine 需要 IServiceProvider 解析 INotificationService;构造一个最小 provider
var services = new ServiceCollection();
services.AddSingleton(notifier);
var provider = services.BuildServiceProvider();
var engine = new ProcessEngine(db, provider, new ConditionEvaluator());
var processor = new OverdueTaskProcessor(db, engine, notifier, NullLogger.Instance);
return (processor, db, notifier);
}
/// 种子一条已逾期、Running 实例的 Pending 任务 + 完整的下游 End 节点(让 CompleteTaskAsync 能推进)。
private static async Task<(WorkflowInstance instance, WorkflowTask task)> SeedOverdueAsync(
WorkflowDbContext db, bool autoApprove, int overdueMinutes = 30, EdgeType edgeType = EdgeType.Approved)
{
var definitionId = Guid.NewGuid();
var instanceId = Guid.NewGuid();
var approvalNodeId = Guid.NewGuid();
var endNodeId = Guid.NewGuid();
var assigneeId = Guid.NewGuid();
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = definitionId, Name = "超时测试流程", Code = "timeout-test-" + Guid.NewGuid(),
Status = DefinitionStatus.Published, IsEnabled = true
});
db.WorkflowNodes.AddRange(
new WorkflowNode { Id = approvalNodeId, DefinitionId = definitionId, NodeType = NodeType.Approval, Name = "审批", Config = "{ \"assigneeRule\": \"user:" + assigneeId + "\", \"autoApproveOnTimeout\": " + (autoApprove ? "true" : "false") + " }" },
new WorkflowNode { Id = endNodeId, DefinitionId = definitionId, NodeType = NodeType.End, Name = "结束" });
db.WorkflowEdges.Add(new WorkflowEdge
{
Id = Guid.NewGuid(), DefinitionId = definitionId,
SourceNodeId = approvalNodeId, TargetNodeId = endNodeId, EdgeType = edgeType
});
var instance = new WorkflowInstance
{
Id = instanceId, DefinitionId = definitionId, Title = "测试实例",
InitiatorId = Guid.NewGuid(), Status = InstanceStatus.Running
};
db.WorkflowInstances.Add(instance);
var tokenId = Guid.NewGuid();
db.WorkflowTokens.Add(new WorkflowToken { Id = tokenId, InstanceId = instanceId, NodeId = approvalNodeId, Status = TokenStatus.Active });
var task = new WorkflowTask
{
Id = Guid.NewGuid(), InstanceId = instanceId, TokenId = tokenId, NodeId = approvalNodeId,
Title = "逾期任务", AssigneeId = assigneeId,
Type = TaskType.Approval, Status = TaskStatus.Pending,
DueAt = DateTime.UtcNow.AddMinutes(-overdueMinutes) // 已逾期
};
db.WorkflowTasks.Add(task);
await db.SaveChangesAsync();
return (instance, task);
}
[Fact]
public async Task Execute_AutoApproveTrue_AutoCompletesAsApproved()
{
var db = CreateDbContext();
var (processor, _, notifier) = Build(db);
var (instance, task) = await SeedOverdueAsync(db, autoApprove: true);
await processor.ExecuteAsync();
var updated = await db.WorkflowTasks.FirstAsync(t => t.Id == task.Id);
updated.Status.Should().Be(TaskStatus.Approved);
updated.Comment.Should().Contain("自动通过");
notifier.TimeoutCallCount.Should().Be(1);
notifier.AutoApprovedFlags.Should().Contain(true);
}
[Fact]
public async Task Execute_AutoApproveFalse_AutoCompletesAsRejected()
{
var db = CreateDbContext();
var (processor, _, notifier) = Build(db);
// 驳回需 Rejected 边
var (instance, task) = await SeedOverdueAsync(db, autoApprove: false, edgeType: EdgeType.Rejected);
await processor.ExecuteAsync();
var updated = await db.WorkflowTasks.FirstAsync(t => t.Id == task.Id);
updated.Status.Should().Be(TaskStatus.Rejected);
updated.Comment.Should().Contain("自动驳回");
notifier.TimeoutCallCount.Should().Be(1);
notifier.AutoApprovedFlags.Should().Contain(false);
}
[Fact]
public async Task Execute_SuspendedInstance_Skipped()
{
// 关键守卫:CompleteTaskAsync 不检查 instance.Status,调度器必须过滤 Suspended 实例
var db = CreateDbContext();
var (processor, _, notifier) = Build(db);
var (instance, task) = await SeedOverdueAsync(db, autoApprove: true);
instance.Status = InstanceStatus.Suspended;
await db.SaveChangesAsync();
await processor.ExecuteAsync();
var unchanged = await db.WorkflowTasks.FirstAsync(t => t.Id == task.Id);
unchanged.Status.Should().Be(TaskStatus.Pending, "已挂起实例的任务不应被超时处理");
notifier.TimeoutCallCount.Should().Be(0);
}
[Fact]
public async Task Execute_NoDueAt_Skipped()
{
var db = CreateDbContext();
var (processor, _, notifier) = Build(db);
var (_, task) = await SeedOverdueAsync(db, autoApprove: true);
task.DueAt = null;
await db.SaveChangesAsync();
await processor.ExecuteAsync();
var unchanged = await db.WorkflowTasks.FirstAsync(t => t.Id == task.Id);
unchanged.Status.Should().Be(TaskStatus.Pending, "无 DueAt 的任务不应被处理");
notifier.TimeoutCallCount.Should().Be(0);
}
[Fact]
public async Task Execute_NotYetOverdue_Skipped()
{
var db = CreateDbContext();
var (processor, _, notifier) = Build(db);
var (_, task) = await SeedOverdueAsync(db, autoApprove: true);
task.DueAt = DateTime.UtcNow.AddMinutes(30); // 还没到期
await db.SaveChangesAsync();
await processor.ExecuteAsync();
var unchanged = await db.WorkflowTasks.FirstAsync(t => t.Id == task.Id);
unchanged.Status.Should().Be(TaskStatus.Pending, "未到期的任务不应被处理");
notifier.TimeoutCallCount.Should().Be(0);
}
[Fact]
public async Task Execute_AlreadyCompleted_Skipped()
{
var db = CreateDbContext();
var (processor, _, notifier) = Build(db);
var (_, task) = await SeedOverdueAsync(db, autoApprove: true);
task.Status = TaskStatus.Approved;
await db.SaveChangesAsync();
await processor.ExecuteAsync();
notifier.TimeoutCallCount.Should().Be(0, "已处理的任务不应再次被超时处理");
}
[Fact]
public async Task Execute_NoOverdueTasks_Noop()
{
var db = CreateDbContext();
var (processor, _, notifier) = Build(db);
await processor.ExecuteAsync(); // 空库,应正常返回不报错
notifier.TimeoutCallCount.Should().Be(0);
}
[Fact]
public void NodeConfigParser_ParsesTimeoutConfig()
{
var config = NodeConfigParser.Parse("""{ "timeoutMinutes": 1440, "autoApproveOnTimeout": true }""");
NodeConfigParser.GetInt(config, "timeoutMinutes").Should().Be(1440);
NodeConfigParser.GetBool(config, "autoApproveOnTimeout").Should().BeTrue();
}
[Fact]
public void NodeConfigParser_MissingKeys_ReturnsNull()
{
var config = NodeConfigParser.Parse("""{ "assigneeRule": "role:manager" }""");
NodeConfigParser.GetInt(config, "timeoutMinutes").Should().BeNull();
NodeConfigParser.GetBool(config, "autoApproveOnTimeout").Should().BeNull();
}
}