Files
KArtSell.Aegis/tests/KArtSell.Integration.Tests/Scheduling/ModelScheduleCasTests.cs
T
kjh2064 00957bf384 test: verify scheduler CAS on PostgreSQL (AEG-V15-036)
Adds a lease-loss/reacquire integration rehearsal and fixes Dapper due-schedule materialization with an explicit row DTO. Evidence: PostgreSQL test 1/1 passed; TRX SHA256 49627FF0180034D2A7A1E4393448C73D337D918E7CE47EA9FC2BDB144FBBA833.
2026-08-09 02:11:23 +09:00

79 lines
3.8 KiB
C#

using Dapper;
using KArtSell.BuildingBlocks.Data;
using KArtSell.Modules.ModelOperations.Infrastructure;
using Npgsql;
using Xunit;
namespace KArtSell.Integration.Tests.Scheduling;
[Collection("Database")]
public sealed class ModelScheduleCasTests : IAsyncLifetime
{
private readonly NpgsqlDataSource _dataSource = new NpgsqlDataSourceBuilder(TestDatabaseConnection.GetConnectionString()).Build();
public async Task InitializeAsync()
{
await using var connection = await _dataSource.OpenConnectionAsync();
await connection.ExecuteAsync("select 1");
}
public Task DisposeAsync() => _dataSource.DisposeAsync().AsTask();
[Fact]
public async Task Stale_dispatch_revision_cannot_advance_a_reacquired_schedule()
{
var scheduleId = Guid.NewGuid();
var scheduledFor = new DateTimeOffset(2026, 8, 9, 0, 0, 0, TimeSpan.Zero);
const string firstOwner = "cas-test:first";
const string secondOwner = "cas-test:second";
var repository = new DapperModelScheduleRepository(new NpgsqlConnectionFactory(_dataSource));
try
{
await using (var connection = await _dataSource.OpenConnectionAsync())
{
await connection.ExecuteAsync("""
insert into evaluation.model_operation_schedule
(schedule_id, operation_code, operation_name, scope_key, cadence, automation_mode,
queue_name, schedule_version, enabled, next_due_at, max_lag, primary_owner, secondary_owner)
values
(@ScheduleId, 'J31', 'CasTest', @ScopeKey, 'DAILY', 'EVALUATION_ONLY',
'q-control', 1, true, @ScheduledFor, interval '1 hour', 'BE', 'QA');
""", new { ScheduleId = scheduleId, ScopeKey = $"CAS-{scheduleId:N}", ScheduledFor = scheduledFor });
}
var first = Assert.Single(await repository.AcquireDueAsync(scheduledFor.AddMinutes(1), firstOwner, TimeSpan.FromMinutes(5), 1, CancellationToken.None));
await using (var connection = await _dataSource.OpenConnectionAsync())
{
await connection.ExecuteAsync("""
update evaluation.model_operation_schedule
set lease_until = @ExpiredAt
where schedule_id = @ScheduleId;
""", new { ScheduleId = scheduleId, ExpiredAt = scheduledFor });
}
var second = Assert.Single(await repository.AcquireDueAsync(scheduledFor.AddMinutes(2), secondOwner, TimeSpan.FromMinutes(5), 1, CancellationToken.None));
Assert.True(second.DispatchRevision > first.DispatchRevision);
await Assert.ThrowsAsync<InvalidOperationException>(() => repository.MarkDispatchedAsync(
scheduleId, firstOwner, first.DispatchRevision, "stale-job", scheduledFor.AddMinutes(3), scheduledFor.AddDays(1), CancellationToken.None));
await using var verify = await _dataSource.OpenConnectionAsync();
var row = await verify.QuerySingleAsync<(DateTimeOffset NextDueAt, string LeaseOwner, int DispatchRevision)>("""
select next_due_at as NextDueAt, lease_owner as LeaseOwner, dispatch_revision as DispatchRevision
from evaluation.model_operation_schedule
where schedule_id = @ScheduleId;
""", new { ScheduleId = scheduleId });
Assert.Equal(scheduledFor, row.NextDueAt);
Assert.Equal(secondOwner, row.LeaseOwner);
Assert.Equal(second.DispatchRevision, row.DispatchRevision);
}
finally
{
await using var cleanup = await _dataSource.OpenConnectionAsync();
await cleanup.ExecuteAsync("delete from evaluation.model_operation_schedule where schedule_id = @ScheduleId", new { ScheduleId = scheduleId });
}
}
}