fix(anomalies): skip orphan-event rows in the feed instead of crashing
AnomalyBrowsingService.TryProject fell back to `new SportCode(0)` when an anomaly's event was missing — but SportCode throws for 0, which would blow up the whole feed/dashboard for that row. Anomalies have an FK to events so it was dead in practice, but an orphaned row now degrades gracefully (skipped, like a row with unparseable evidence). Closes the flagged latent crash. - TryProject returns false when the event lookup misses; +1 test.
This commit is contained in:
@@ -137,14 +137,16 @@ public sealed class AnomalyBrowsingService : IAnomalyBrowsingService
|
|||||||
|
|
||||||
var severity = AnomalySeverityRules.FromScore(anomaly.Score);
|
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 sport = ev.Sport;
|
||||||
var country = ev?.CountryCode ?? string.Empty;
|
var country = ev.CountryCode;
|
||||||
var league = ev?.LeagueId ?? string.Empty;
|
var league = ev.LeagueId;
|
||||||
var title = ev is not null
|
var title = ev.Title;
|
||||||
? ev.Title
|
|
||||||
: anomaly.EventId.Value;
|
|
||||||
|
|
||||||
var preSnap = ToSnapshot(dto.PreSuspension);
|
var preSnap = ToSnapshot(dto.PreSuspension);
|
||||||
var postSnap = ToSnapshot(dto.PostSuspension);
|
var postSnap = ToSnapshot(dto.PostSuspension);
|
||||||
|
|||||||
@@ -100,4 +100,20 @@ public sealed class AnomalyBrowsingServiceTests
|
|||||||
|
|
||||||
items.Select(i => i.Score).Should().ContainInOrder(0.80m, 0.60m, 0.40m);
|
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<DateTimeOffset?>(), Arg.Any<DateTimeOffset?>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new[] { Make(AnomalyKind.SuspensionFlip) });
|
||||||
|
// Event pruned → not in the lookup. Must not throw on a SportCode(0) fallback.
|
||||||
|
_events
|
||||||
|
.GetManyAsync(Arg.Any<IReadOnlyCollection<EventId>>(), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Dictionary<EventId, Event>());
|
||||||
|
|
||||||
|
var items = await CreateSut().ListAsync(new AnomalyFilter(), CancellationToken.None);
|
||||||
|
|
||||||
|
items.Should().BeEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user