fix(dashboard): alarms feed doc row + monitor-contract comment + mid-truncation attach test

The In-process page feeds table still described the alarms page's subscription as
provider status only, contradicting the three passages updated in 7b6dfba.

The loop's catch-all comment (and the cadence bullet that repeated it) claimed the
monitor completes a subscriber's stream on restart. It does not: ClearCache pushes
snapshot_status(false) through the still-open channel, and a subscriber is only
completed-with-error on a failed TryWrite, or removed by its own disposal.

The SnapshotStatus arm's priming claim had no test behind it — every push test
attached to an untruncated feed and pushed the edge itself. ScriptedAlarmFeed now
replays an optional open sequence, and a new test attaches to a feed already
primed provider_status then snapshot_status(truncated) and asserts the banner
comes up with no edge pushed after render.
This commit is contained in:
Joseph Doherty
2026-08-18 06:49:20 -04:00
parent d3ac52758c
commit f57a6ae5ff
3 changed files with 63 additions and 8 deletions
@@ -258,10 +258,13 @@
}
catch
{
// The monitor completes a subscriber's stream when it falls behind, and
// again when the monitor restarts. Both are recoverable by resubscribing;
// the badge and banner hold their last values in the meantime, and the
// resubscribe is primed with the current ones.
// The monitor drops a subscriber whose queue it cannot write to, completing
// that stream with an error; short of cancellation or disposal that is the
// only way this enumeration ends. A monitor restart is NOT one of them — it
// keeps the channel and pushes the cleared status frames through it — so this
// catch is the fell-behind case, recoverable by resubscribing. The badge and
// banner hold their last values meanwhile, and the resubscribe is primed with
// the current ones.
}
try
@@ -134,6 +134,44 @@ public sealed class AlarmsPageTruncationBannerTests
"banner to clear after a complete snapshot_status frame");
}
/// <summary>
/// Attach while the verdict is already truncated. The page carries no
/// priming logic of its own — it relies on <c>StreamAsync</c> opening every
/// subscription with a <c>snapshot_status</c> baseline — so the caveat has
/// to come up off the open sequence alone, with no edge pushed afterwards.
/// A page that only handled the edge would show an un-caveated alarm list
/// to every operator who opened it after the truncation began.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task AlarmsPage_AttachingToAnAlreadyTruncatedFeed_RaisesTheBannerFromThePriming()
{
// The monitor's open sequence: provider status, then the unconditional
// completeness baseline. Nothing is pushed after this.
ScriptedAlarmFeed feed = new()
{
Priming =
[
new AlarmFeedMessage { ProviderStatus = new AlarmProviderStatus() },
SnapshotStatusFrame(truncated: true),
],
};
await using ServiceProvider provider = BuildPushServices(feed);
await using HtmlRenderer renderer = new(
provider,
provider.GetRequiredService<ILoggerFactory>());
HtmlRootComponent page = await renderer.Dispatcher.InvokeAsync(
() => renderer.RenderComponentAsync<AlarmsPage>());
await WaitForHtmlAsync(
renderer,
page,
html => html.Contains(BannerMarker, StringComparison.Ordinal),
"banner to appear from the feed's open-time snapshot_status baseline");
}
private static AlarmFeedMessage SnapshotStatusFrame(bool truncated)
{
return new AlarmFeedMessage
@@ -280,6 +318,14 @@ public sealed class AlarmsPageTruncationBannerTests
/// <inheritdoc />
public bool SnapshotTruncated { get; set; }
/// <summary>
/// Frames replayed at the head of every subscription, standing in for the
/// monitor's open sequence (provider status, then the unconditional
/// completeness baseline). Empty means the subscriber sees only what the
/// test pushes.
/// </summary>
public IReadOnlyList<AlarmFeedMessage> Priming { get; init; } = [];
/// <summary>Pushes one frame onto the feed the page is subscribed to.</summary>
/// <param name="message">The feed frame to deliver.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
@@ -290,6 +336,12 @@ public sealed class AlarmsPageTruncationBannerTests
string? alarmFilterPrefix,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
foreach (AlarmFeedMessage primed in Priming)
{
cancellationToken.ThrowIfCancellationRequested();
yield return primed;
}
await foreach (AlarmFeedMessage message in _frames.Reader
.ReadAllAsync(cancellationToken)
.ConfigureAwait(false))