test(#259): cover id-mode subtype-change (delete+insert) with a same-type sibling
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m52s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m2s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m52s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m2s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Review-caught coverage gap: the existing Subtype_Change test runs the POSITIONAL path (id-less payload). Add a handler-level test for the id-mode branch — one id-matched item changes subtype (One->Duration: delete+insert, new id) while a sibling id-matched item keeps its subtype (Multiple: in place, id + fill-group state preserved) in the same payload. Proves the delete pass + match-pass Remove/Add don't double-handle and the survivor's state is retained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+64
@@ -461,6 +461,70 @@ public class ReplaceProgramScheduleItemsReconcileTests
|
||||
await AssertUnchanged(scheduleId, idA, versionBefore);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Subtype_Change_ById_Should_Replace_That_Item_But_Preserve_A_SameType_Sibling()
|
||||
{
|
||||
// id-mode counterpart of the positional Subtype_Change test: in ONE payload, one id-matched item
|
||||
// changes subtype (One -> Duration: delete+insert, new id, state resets) while a sibling id-matched
|
||||
// item keeps its subtype (Multiple: in-place, id + fill-group state preserved). This is the riskiest
|
||||
// reconcile branch — it exercises the delete pass + match-pass Remove/Add without double-handling.
|
||||
int scheduleId = await SeedSchedule();
|
||||
var handler = new ReplaceProgramScheduleItemsHandler(_db.Factory, _worker);
|
||||
|
||||
var seed = new List<ReplaceProgramScheduleItem>
|
||||
{
|
||||
MakeItem(0, PlayoutMode.One, "a"),
|
||||
MakeItem(1, PlayoutMode.Multiple, "b") with { FillWithGroupMode = FillWithGroupMode.FillWithShuffledGroups }
|
||||
};
|
||||
(await handler.Handle(new ReplaceProgramScheduleItems(scheduleId, seed), CancellationToken.None))
|
||||
.IsRight.ShouldBeTrue();
|
||||
|
||||
int idA, idB;
|
||||
await using (TvContext ctx = _db.CreateContext())
|
||||
{
|
||||
List<ProgramScheduleItem> p = await ctx.ProgramScheduleItems
|
||||
.Where(i => i.ProgramScheduleId == scheduleId).OrderBy(i => i.Index).ToListAsync();
|
||||
idA = p[0].Id;
|
||||
idB = p[1].Id;
|
||||
AddFillGroup(ctx, idB, seed: 500); // fill-group state on the same-type sibling
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// A (idA) changes subtype One -> Duration; B (idB) stays Multiple.
|
||||
var changed = new List<ReplaceProgramScheduleItem>
|
||||
{
|
||||
MakeItem(0, PlayoutMode.Duration, "a", id: idA) with
|
||||
{
|
||||
PlayoutDuration = TimeSpan.FromMinutes(30), DiscardToFillAttempts = 0
|
||||
},
|
||||
MakeItem(1, PlayoutMode.Multiple, "b", id: idB) with { FillWithGroupMode = FillWithGroupMode.FillWithShuffledGroups }
|
||||
};
|
||||
(await handler.Handle(new ReplaceProgramScheduleItems(scheduleId, changed), CancellationToken.None))
|
||||
.IsRight.ShouldBeTrue();
|
||||
|
||||
await using (TvContext ctx = _db.CreateContext())
|
||||
{
|
||||
List<ProgramScheduleItem> after = await ctx.ProgramScheduleItems
|
||||
.Where(i => i.ProgramScheduleId == scheduleId).ToListAsync();
|
||||
after.Count.ShouldBe(2);
|
||||
|
||||
// A was replaced: old id gone, a NEW Duration row (distinct id) sits at index 0
|
||||
after.ShouldNotContain(i => i.Id == idA);
|
||||
ProgramScheduleItem duration = after.Single(i => i is ProgramScheduleItemDuration);
|
||||
duration.Id.ShouldNotBe(idA);
|
||||
duration.Id.ShouldNotBe(idB);
|
||||
duration.Index.ShouldBe(0);
|
||||
|
||||
// B was updated in place: same id, still Multiple, its fill-group state intact
|
||||
ProgramScheduleItem b = after.Single(i => i.Id == idB);
|
||||
b.ShouldBeOfType<ProgramScheduleItemMultiple>();
|
||||
b.Index.ShouldBe(1);
|
||||
PlayoutScheduleItemFillGroupIndex bState = await ctx.Set<PlayoutScheduleItemFillGroupIndex>()
|
||||
.Include(x => x.EnumeratorState).SingleAsync(g => g.ProgramScheduleItemId == idB);
|
||||
bState.EnumeratorState.Seed.ShouldBe(500);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddFillGroup(TvContext ctx, int itemId, int seed) =>
|
||||
ctx.Add(new PlayoutScheduleItemFillGroupIndex
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user