mbproxy: close out the dashboard code-review minor findings

Resolves the remaining Minor items from the 2026-05-15 review so the
web-UI dashboard work has no open follow-ups: a real-HubConnection
end-to-end test for the SignalR feed, stable mbproxy.admin.broadcast.*
log-event names, keyboard/aria accessibility on the fleet table,
frontend JS hardening (URL-decode guard, NaN guards, shared util.js),
reconciler<->capture-registry coverage, throwing-sink and embedded-asset
tests, broadcaster polish, and a soft upper bound on AdminPushIntervalMs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-16 16:36:39 -04:00
parent 374eecd205
commit 0308490aef
21 changed files with 576 additions and 67 deletions
@@ -39,7 +39,7 @@ public sealed class StatusBroadcasterTests
}
}
private static async Task<Harness> BuildAsync()
private static async Task<Harness> BuildAsync(IStatusPushSink? sinkOverride = null)
{
var hostBuilder = Host.CreateApplicationBuilder();
hostBuilder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
@@ -68,7 +68,7 @@ public sealed class StatusBroadcasterTests
var sink = new FakeStatusPushSink();
var broadcaster = new StatusBroadcaster(
sink, builder, tracker, registry, options, NullLogger.Instance);
sinkOverride ?? sink, builder, tracker, registry, options, NullLogger.Instance);
return new Harness(host, broadcaster, sink, builder, registry, tracker);
}
@@ -129,6 +129,31 @@ public sealed class StatusBroadcasterTests
capture.IsArmed.ShouldBeFalse("the broadcaster disarms a capture once its last viewer leaves");
}
[Fact]
public async Task PushOnce_SinkThrowsNonCancellation_FailureIsSwallowed()
{
// A SignalR transport fault on a push must not escape PushOnceAsync — the loop
// has to survive it and retry on the next cycle.
var throwing = new ThrowingStatusPushSink(() => new InvalidOperationException("boom"));
await using var h = await BuildAsync(throwing);
await Should.NotThrowAsync(
() => h.Broadcaster.PushOnceAsync(TestContext.Current.CancellationToken));
}
[Fact]
public async Task PushOnce_SinkThrowsOperationCanceled_Propagates()
{
// The catch filters are `when (ex is not OperationCanceledException)` — a genuine
// cancellation must propagate so the loop unwinds at shutdown instead of being
// swallowed and retried.
var throwing = new ThrowingStatusPushSink(() => new OperationCanceledException());
await using var h = await BuildAsync(throwing);
await Should.ThrowAsync<OperationCanceledException>(
() => h.Broadcaster.PushOnceAsync(TestContext.Current.CancellationToken));
}
[Fact]
public async Task StopAsync_DisarmsEveryCapture()
{