fix(ui): drop stale-site poll results on Alarm Summary site switch (plan R2-07 T7)

This commit is contained in:
Joseph Doherty
2026-07-13 10:55:25 -04:00
parent 1fcfa4fb16
commit d33337a834
2 changed files with 43 additions and 0 deletions
@@ -291,6 +291,13 @@
try try
{ {
var result = await AlarmSummaryService.GetSiteAlarmsAsync(siteId); var result = await AlarmSummaryService.GetSiteAlarmsAsync(siteId);
// Stale-site guard (arch-review R2 N4): the operator may have switched sites
// while this fan-out was in flight — drop the result rather than labeling
// site A's alarms under site B's picker. Mirrors OnLiveAlarmsChanged (:374).
if (_selectedSiteId != siteId)
{
return;
}
_rows = result.Alarms; _rows = result.Alarms;
_notReporting = result.NotReportingInstances; _notReporting = result.NotReportingInstances;
_rollup = AlarmSummaryService.ComputeRollup(_rows); _rollup = AlarmSummaryService.ComputeRollup(_rows);
@@ -208,6 +208,42 @@ public class AlarmSummaryRenderTests : BunitContext
Assert.True(_liveCache.DisposeCount >= 1); Assert.True(_liveCache.DisposeCount >= 1);
} }
// ── arch-review R2 N4: stale-site poll guard ───────────────────────────────
[Fact]
public void InFlightPollForPreviousSite_DoesNotOverwriteNewSitesRows()
{
// Site 1's fan-out hangs on a TCS; site 2 answers immediately (arch-review R2 N4).
var site1Tcs = new TaskCompletionSource<AlarmSummaryResult>();
_summary.GetSiteAlarmsAsync(1, Arg.Any<CancellationToken>()).Returns(site1Tcs.Task);
_summary.GetSiteAlarmsAsync(2, Arg.Any<CancellationToken>()).Returns(Task.FromResult(
new AlarmSummaryResult(
new List<AlarmSummaryRow>
{
new("Site2Instance", new AlarmStateChanged("Site2Instance", "S2-alarm", AlarmState.Active, 100, DateTimeOffset.UtcNow)),
},
Array.Empty<string>())));
var cut = Render<AlarmSummaryPage>();
cut.Find("[data-test='alarm-summary-site']").Change("1"); // poll in flight, hung
cut.Find("[data-test='alarm-summary-site']").Change("2"); // site 2 renders
cut.WaitForAssertion(() => Assert.Equal("Site2Instance", FirstRowInstance(cut)));
// The stale site-1 fan-out now completes — it must be dropped, not rendered.
site1Tcs.SetResult(new AlarmSummaryResult(
new List<AlarmSummaryRow>
{
new("StaleSite1", new AlarmStateChanged("StaleSite1", "S1-alarm", AlarmState.Active, 999, DateTimeOffset.UtcNow)),
},
new[] { "StaleNotReporting" }));
cut.WaitForAssertion(() =>
{
Assert.Equal("Site2Instance", FirstRowInstance(cut));
Assert.DoesNotContain("StaleNotReporting", cut.Markup);
});
}
/// <summary> /// <summary>
/// Controllable in-memory fake of <see cref="ISiteAlarmLiveCache"/>: records /// Controllable in-memory fake of <see cref="ISiteAlarmLiveCache"/>: records
/// subscribe/dispose counts and lets a test push a live snapshot (which flips /// subscribe/dispose counts and lets a test push a live snapshot (which flips