Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 5s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 9s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m23s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 7s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 7s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 18m18s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
A successful fetch returning zero items made existing.Except([]) flag the ENTIRE library FileNotFound in one scan — feeding EmptyTrashHandler's permanent delete and emptying every affected collection (dead channels). Add a shared MediaServerReconciliationGuard that skips (and logs a Warning) the sweep when incoming==0 while items exist, wired into the three library-level sweeps (Television shows / Movie / OtherVideo). An empty incoming set is indistinguishable at scan time from a mid-restore / emptied-upstream error (both report a zero total), so this deliberately overrides #476's degenerate "last item removed => empty incoming => flag" case. #476's cascade still fires for partial deletions (survivors present); its characterization test moves from an empty incoming to a survivor+removed partial-deletion case. Tests: policy table (MediaServerReconciliationGuardTests) + per-scanner integration proving the wiring (empty incoming + non-empty existing flags/reindexes nothing). Proven non-vacuous by neutralizing the guard. Nested TV season/episode sweeps left unguarded (bounded blast radius); ratio-threshold + projection-failure detection deferred to a follow-up. docs/decisions.md updated. Fixes #477 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.2 KiB
C#
70 lines
2.2 KiB
C#
using ErsatzTV.Scanner.Core.Metadata;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Scanner.Tests.Core.Metadata;
|
|
|
|
// #477: the deterministic policy behind the media-server anti-nuke guard. An empty incoming set with
|
|
// existing items present is the only case that skips the sweep (and logs); every other combination
|
|
// reconciles normally.
|
|
public class MediaServerReconciliationGuardTests
|
|
{
|
|
[Test]
|
|
public void Empty_Incoming_With_Existing_Items_Skips_And_Warns()
|
|
{
|
|
var logger = Substitute.For<ILogger>();
|
|
|
|
bool shouldFlag = MediaServerReconciliationGuard.ShouldFlagMissing(logger, "Movies", 0, 5);
|
|
|
|
shouldFlag.ShouldBeFalse();
|
|
logger.Received(1).Log(
|
|
LogLevel.Warning,
|
|
Arg.Any<EventId>(),
|
|
Arg.Any<object>(),
|
|
Arg.Any<Exception>(),
|
|
Arg.Any<Func<object, Exception?, string>>());
|
|
}
|
|
|
|
[Test]
|
|
public void Partial_Deletion_Still_Flags()
|
|
{
|
|
var logger = Substitute.For<ILogger>();
|
|
|
|
MediaServerReconciliationGuard.ShouldFlagMissing(logger, "Movies", 3, 5).ShouldBeTrue();
|
|
|
|
logger.DidNotReceive().Log(
|
|
LogLevel.Warning,
|
|
Arg.Any<EventId>(),
|
|
Arg.Any<object>(),
|
|
Arg.Any<Exception>(),
|
|
Arg.Any<Func<object, Exception?, string>>());
|
|
}
|
|
|
|
[Test]
|
|
public void Empty_Incoming_With_No_Existing_Items_Is_A_Noop_Sweep()
|
|
{
|
|
var logger = Substitute.For<ILogger>();
|
|
|
|
// nothing exists, so an empty incoming set flags nothing either way — allow the (empty) sweep
|
|
// rather than special-casing it, and do not emit the scary warning.
|
|
MediaServerReconciliationGuard.ShouldFlagMissing(logger, "Movies", 0, 0).ShouldBeTrue();
|
|
|
|
logger.DidNotReceive().Log(
|
|
LogLevel.Warning,
|
|
Arg.Any<EventId>(),
|
|
Arg.Any<object>(),
|
|
Arg.Any<Exception>(),
|
|
Arg.Any<Func<object, Exception?, string>>());
|
|
}
|
|
|
|
[Test]
|
|
public void Full_Fetch_Into_Empty_Library_Still_Flags()
|
|
{
|
|
var logger = Substitute.For<ILogger>();
|
|
|
|
MediaServerReconciliationGuard.ShouldFlagMissing(logger, "Movies", 3, 0).ShouldBeTrue();
|
|
}
|
|
}
|