154 lines
5.4 KiB
C#
154 lines
5.4 KiB
C#
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Worker.Prime;
|
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Prime;
|
|
|
|
public class PrimeSchedulerTests : IDisposable
|
|
{
|
|
private readonly DbFixture _db = new();
|
|
public void Dispose() => _db.Dispose();
|
|
|
|
private sealed class FakeClock : IPrimeClock
|
|
{
|
|
public DateTimeOffset Now { get; set; }
|
|
}
|
|
|
|
private sealed class FakeRunner : IPrimeRunner
|
|
{
|
|
public List<Guid> FiredIds { get; } = new();
|
|
public Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto s, CancellationToken ct)
|
|
{
|
|
FiredIds.Add(s.Id);
|
|
return Task.FromResult(new PrimeRunOutcome(true, "ok"));
|
|
}
|
|
}
|
|
|
|
private sealed class FakeBroadcaster : IPrimeBroadcaster
|
|
{
|
|
public List<(Guid id, bool ok, string msg)> Calls { get; } = new();
|
|
public Task PrimeFiredAsync(Guid id, bool ok, string msg, DateTimeOffset firedAt)
|
|
{
|
|
Calls.Add((id, ok, msg));
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Fires_Schedule_When_Within_CatchUp_On_Startup()
|
|
{
|
|
var id = Guid.NewGuid();
|
|
using (var ctx = _db.CreateContext())
|
|
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
|
{
|
|
Id = id,
|
|
StartDate = new DateOnly(2026, 5, 5),
|
|
EndDate = new DateOnly(2026, 5, 5),
|
|
TimeOfDay = new TimeSpan(7, 0, 0),
|
|
WorkdaysOnly = false,
|
|
Enabled = true,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
});
|
|
|
|
var clock = new FakeClock { Now = new DateTimeOffset(2026, 5, 5, 7, 10, 0, TimeSpan.FromHours(2)) };
|
|
var runner = new FakeRunner();
|
|
var broadcaster = new FakeBroadcaster();
|
|
var signal = new PrimeScheduleSignal();
|
|
var scheduler = new PrimeScheduler(
|
|
_db.CreateFactory(), runner, clock, signal, broadcaster,
|
|
PrimeSchedulerOptions.Default, NullLogger<PrimeScheduler>.Instance);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
var run = scheduler.StartAsync(cts.Token);
|
|
|
|
await WaitFor(() => runner.FiredIds.Count >= 1, TimeSpan.FromSeconds(3));
|
|
|
|
cts.Cancel();
|
|
await scheduler.StopAsync(CancellationToken.None);
|
|
|
|
Assert.Single(runner.FiredIds);
|
|
Assert.Equal(id, runner.FiredIds[0]);
|
|
Assert.Single(broadcaster.Calls);
|
|
Assert.True(broadcaster.Calls[0].ok);
|
|
|
|
using var read = _db.CreateContext();
|
|
var row = await new PrimeScheduleRepository(read).GetAsync(id);
|
|
Assert.NotNull(row!.LastRunAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Does_Not_Fire_Past_CatchUp_Window()
|
|
{
|
|
var id = Guid.NewGuid();
|
|
using (var ctx = _db.CreateContext())
|
|
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
|
{
|
|
Id = id,
|
|
StartDate = new DateOnly(2026, 5, 5),
|
|
EndDate = new DateOnly(2026, 5, 5),
|
|
TimeOfDay = new TimeSpan(7, 0, 0),
|
|
WorkdaysOnly = false,
|
|
Enabled = true,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
});
|
|
|
|
var clock = new FakeClock { Now = new DateTimeOffset(2026, 5, 5, 9, 0, 0, TimeSpan.FromHours(2)) };
|
|
var runner = new FakeRunner();
|
|
var signal = new PrimeScheduleSignal();
|
|
var scheduler = new PrimeScheduler(
|
|
_db.CreateFactory(), runner, clock, signal, new FakeBroadcaster(),
|
|
PrimeSchedulerOptions.Default, NullLogger<PrimeScheduler>.Instance);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
await scheduler.StartAsync(cts.Token);
|
|
await Task.Delay(200);
|
|
cts.Cancel();
|
|
await scheduler.StopAsync(CancellationToken.None);
|
|
|
|
Assert.Empty(runner.FiredIds);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Signal_Recomputes_Mid_Wait()
|
|
{
|
|
var clock = new FakeClock { Now = new DateTimeOffset(2026, 5, 5, 7, 10, 0, TimeSpan.FromHours(2)) };
|
|
var runner = new FakeRunner();
|
|
var signal = new PrimeScheduleSignal();
|
|
var scheduler = new PrimeScheduler(
|
|
_db.CreateFactory(), runner, clock, signal, new FakeBroadcaster(),
|
|
PrimeSchedulerOptions.Default, NullLogger<PrimeScheduler>.Instance);
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
await scheduler.StartAsync(cts.Token);
|
|
|
|
var id = Guid.NewGuid();
|
|
using (var ctx = _db.CreateContext())
|
|
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
|
{
|
|
Id = id,
|
|
StartDate = new DateOnly(2026, 5, 5),
|
|
EndDate = new DateOnly(2026, 5, 5),
|
|
TimeOfDay = new TimeSpan(7, 0, 0),
|
|
WorkdaysOnly = false,
|
|
Enabled = true,
|
|
CreatedAt = DateTimeOffset.UtcNow,
|
|
});
|
|
signal.Signal();
|
|
|
|
await WaitFor(() => runner.FiredIds.Count >= 1, TimeSpan.FromSeconds(3));
|
|
cts.Cancel();
|
|
await scheduler.StopAsync(CancellationToken.None);
|
|
|
|
Assert.Single(runner.FiredIds);
|
|
}
|
|
|
|
private static async Task WaitFor(Func<bool> cond, TimeSpan timeout)
|
|
{
|
|
var deadline = DateTime.UtcNow + timeout;
|
|
while (!cond() && DateTime.UtcNow < deadline)
|
|
await Task.Delay(20);
|
|
}
|
|
}
|