diff --git a/src/Marathon.UI/Services/AnomalyBrowsingService.cs b/src/Marathon.UI/Services/AnomalyBrowsingService.cs index 30f3b5d..12bbc06 100644 --- a/src/Marathon.UI/Services/AnomalyBrowsingService.cs +++ b/src/Marathon.UI/Services/AnomalyBrowsingService.cs @@ -137,14 +137,16 @@ public sealed class AnomalyBrowsingService : IAnomalyBrowsingService var severity = AnomalySeverityRules.FromScore(anomaly.Score); - events.TryGetValue(anomaly.EventId, out var ev); + // Skip orphan anomalies whose event has been pruned — degrade gracefully rather + // than throwing on a sentinel SportCode(0). Anomalies have an FK to events so this + // is defensive; the feed already drops rows whose evidence won't parse. + if (!events.TryGetValue(anomaly.EventId, out var ev) || ev is null) + return false; - var sport = ev?.Sport ?? new SportCode(0); - var country = ev?.CountryCode ?? string.Empty; - var league = ev?.LeagueId ?? string.Empty; - var title = ev is not null - ? ev.Title - : anomaly.EventId.Value; + var sport = ev.Sport; + var country = ev.CountryCode; + var league = ev.LeagueId; + var title = ev.Title; var preSnap = ToSnapshot(dto.PreSuspension); var postSnap = ToSnapshot(dto.PostSuspension); diff --git a/tests/Marathon.UI.Tests/Services/AnomalyBrowsingServiceTests.cs b/tests/Marathon.UI.Tests/Services/AnomalyBrowsingServiceTests.cs index f1c2f3c..4594a42 100644 --- a/tests/Marathon.UI.Tests/Services/AnomalyBrowsingServiceTests.cs +++ b/tests/Marathon.UI.Tests/Services/AnomalyBrowsingServiceTests.cs @@ -100,4 +100,20 @@ public sealed class AnomalyBrowsingServiceTests items.Select(i => i.Score).Should().ContainInOrder(0.80m, 0.60m, 0.40m); } + + [Fact] + public async Task ListAsync_SkipsOrphanAnomaly_When_EventMissing() + { + _anomalies + .ListByDateRangeAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(new[] { Make(AnomalyKind.SuspensionFlip) }); + // Event pruned → not in the lookup. Must not throw on a SportCode(0) fallback. + _events + .GetManyAsync(Arg.Any>(), Arg.Any()) + .Returns(new Dictionary()); + + var items = await CreateSut().ListAsync(new AnomalyFilter(), CancellationToken.None); + + items.Should().BeEmpty(); + } }