diff --git a/ErsatzTV.Tests/Integration/JellyfinMusicVideoLibraryScannerTests.cs b/ErsatzTV.Tests/Integration/JellyfinMusicVideoLibraryScannerTests.cs index 948dbde03..552ae640e 100644 --- a/ErsatzTV.Tests/Integration/JellyfinMusicVideoLibraryScannerTests.cs +++ b/ErsatzTV.Tests/Integration/JellyfinMusicVideoLibraryScannerTests.cs @@ -936,19 +936,35 @@ public class JellyfinMusicVideoLibraryScannerTests Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(ci => - { - var counter = ci.ArgAt(3); - for (var i = 0; i < failures; i++) - { - counter.RecordFailure(); - } - - return Items(items); - }); + .Returns(ci => ItemsWithProjectionFailures( + items, + ci.ArgAt(3), + failures)); return apiClient; } + // Records the failures DURING enumeration, like the real paginator does from ProjectToMusicVideo's catch — + // not eagerly when the enumerable is handed out. The distinction is the point: a scanner that snapshotted + // Count before the enumeration completed would read 0 and sweep anyway, and an eager substitute would keep + // passing while production broke. + private static async IAsyncEnumerable> ItemsWithProjectionFailures( + Func[] items, + MediaServerProjectionFailureCounter projectionFailures, + int failures) + { + for (var i = 0; i < failures; i++) + { + projectionFailures.RecordFailure(); + } + + foreach (Func item in items) + { + yield return new Tuple(item(), items.Length); + } + + await Task.CompletedTask; + } + private static async IAsyncEnumerable> Items(Func[] items) { foreach (Func item in items) diff --git a/docs/decisions.md b/docs/decisions.md index 57430825c..f67a664b5 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -3905,7 +3905,13 @@ owner's sweep **destroyed** the row the other library still served. This is the the protection — which is what this scanner did until the counter was threaded through (`MediaServerProjectionFailureCounter`, one per enumeration, never a field on the singleton api client). The single guard call covers the legacy path diff too, which is if anything more exposed: a legacy row has no etag - to fall back on. + to fall back on. #484's own music-video test asserted the pre-#496 hard delete + (`FindMusicVideoPaths`/`DeleteByPath`) and could not survive this change; it is replaced by + `ScanLibrary_Should_Not_Sweep_When_The_Projection_Reported_Failures` (identity sweep) and + `..._Should_Not_Sweep_Legacy_Rows_When_The_Projection_Reported_Failures` (legacy sweep). Their substitute + records failures **during** enumeration, not when the enumerable is handed out — an eager substitute would keep + passing if a refactor snapshotted `Count` before the enumeration completed, which is precisely the regression + the guard exists to prevent. - **Scope honesty: this is parity, not a total fix.** One file path is still one `MediaItem` row globally (`MediaFileAlreadyExists` is a global path-hash guard), so a second library serving the same file still gets no row of its own — exactly as for movies/TV. What changes is that the first library's sweep now *flags*