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:
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user