Enhance GridScheduleGenerator and related components for improved slot handling and cursor management
ci / build-backend (push) Successful in 1m21s
ci / build-frontend (push) Successful in 51s
ci / tests (push) Successful in 1m40s
ci / sonar (push) Successful in 3m46s

Updated the GridScheduleGenerator to support rebuilding future schedules while correctly managing aired positions. Introduced a new AiredPosition record to track the last aired state of slots, ensuring that the cursor rewinds to the correct position during rebuilds. Refactored the BuildInputAsync method to accept aired positions, and modified the LoadAiredPositionsAsync method for accurate retrieval of past entries. Enhanced the SchedulePlanner to utilize shared cursor states between main and background loops, preventing duplicate series plays. Updated documentation to reflect these changes and added integration tests to verify the correct behavior of the new functionality.
This commit is contained in:
Leonid Pershin
2026-07-29 08:57:46 +03:00
parent c67d1f7752
commit fbfc6e677e
11 changed files with 568 additions and 117 deletions
@@ -115,6 +115,54 @@ public sealed class GridScheduleGeneratorIntegrationTests(PostgresFixture fixtur
Assert.Equal(past.Count, survived.Count);
}
/// <summary>
/// Пересборка выбрасывает хвост, который курсор уже прошёл. Без отмотки к отыгранному каждое
/// применение проматывало бы библиотеку на горизонт вперёд, теряя невышедшие серии.
/// </summary>
[SkippableFact]
public async Task Generate_Rebuild_RewindsCursorToWhatActuallyAired()
{
Skip.IfNot(fixture.Available, "Docker недоступен");
await using var seedDb = fixture.CreateContext();
// Серий заведомо больше, чем влезет в горизонт: иначе сериал успеет пойти по кругу,
// и «продолжает с той же серии» проверять станет нечем.
var world = await SeedAsync(seedDb, episodes: 100);
// Первый прогон строит сутки вперёд от момента двухчасовой давности.
await using var first = fixture.CreateContext();
await Generator(first).GenerateAsync(world.ChannelId, Now.AddHours(-2), false, default);
await using var beforeDb = fixture.CreateContext();
var aired = beforeDb
.ScheduleEntries.Where(e =>
e.ChannelId == world.ChannelId
&& e.Kind == ScheduleEntryKind.Program
&& e.StartsAtUtc < Now
)
.OrderByDescending(e => e.StartsAtUtc)
.First();
var beforeRebuild = beforeDb.SlotStates.Single(s => s.SlotId == world.SlotId);
// Курсор ушёл за эфир: он прошёл весь построенный хвост, а не только вышедшее.
Assert.True(beforeRebuild.NextUnitIndex > aired.EpisodeIndex + 1);
await using var rebuild = fixture.CreateContext();
await Generator(rebuild).GenerateAsync(world.ChannelId, Now, true, default);
await using var verify = fixture.CreateContext();
var firstAfterRebuild = verify
.ScheduleEntries.Where(e =>
e.ChannelId == world.ChannelId
&& e.Kind == ScheduleEntryKind.Program
&& e.StartsAtUtc >= Now
)
.OrderBy(e => e.StartsAtUtc)
.First();
// Пересобранный эфир продолжает ровно с той серии, что шла следующей по отыгранному.
Assert.Equal(aired.EpisodeIndex + 1, firstAfterRebuild.EpisodeIndex);
}
[SkippableFact]
public async Task Generate_CollectionInGroup_StampsCollectionOnEntries()
{