Resolve Tests-027..031: flake root cause + coverage gaps
Tests-027 GatewayMetrics exposes its internal Meter; the
StreamEvents_WhenEventIsWritten_RecordsSendDuration listener
now filters by ReferenceEquals(instrument.Meter, metrics.Meter)
instead of Meter.Name, so parallel tests with their own
GatewayMetrics no longer cross-contaminate the families list.
Tests-028 FakeWorkerClient.Kill now captures LastKillReason;
SessionManager.KillWorkerAsync tests pin the reason
propagation end-to-end and cover the blank/null guard. The
DashboardSessionAdminService kill test pins the literal
dashboard-admin-kill reason.
Tests-029 Added CloseSessionAsync_BlankSessionId_ReturnsFailure to mirror
the existing KillWorkerAsync blank-id coverage.
Tests-030 DeleteAsync_WhenStoreRefuses_ReportsFriendlyError renamed and
extended to assert the dashboard-delete-key audit row with
Details = not-found-or-active. Added
DeleteAsync_BlankKeyId_ReturnsFailure.
Tests-031 DashboardSnapshotPublisher reconnect test now measures the
gap from the first throw inside the fake (firstThrowAt) to
secondSubscribeAt, isolating Task.Delay from StartAsync /
scheduling overhead.
All resolved at 2026-05-24; 512/512 gateway tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,13 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
/// reconnect delay and then re-open the subscription. Before the fix,
|
||||
/// the publisher exited on the first non-cancellation exception and
|
||||
/// the dashboard's snapshot stream went silent until process restart.
|
||||
///
|
||||
/// <para>Tests-031: the reconnect-gap measurement is bounded between the
|
||||
/// moment the first subscribe actually <c>throw</c>s and the moment the
|
||||
/// second subscribe begins. Measuring from <c>startedAt</c> (pre-<c>StartAsync</c>)
|
||||
/// baselined scheduling overhead into the budget and made the lower bound
|
||||
/// flaky on slow CI; recording <c>firstThrowAt</c> inside the fake removes
|
||||
/// that baseline so only the <c>Task.Delay(reconnectDelay)</c> contributes.</para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenSnapshotServiceThrowsOnce_ReconnectsAfterDelay()
|
||||
@@ -31,7 +38,6 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
reconnectDelay);
|
||||
|
||||
using CancellationTokenSource cts = new();
|
||||
DateTimeOffset startedAt = DateTimeOffset.UtcNow;
|
||||
Task execute = publisher.StartAsync(cts.Token);
|
||||
await execute.WaitAsync(TestTimeout);
|
||||
|
||||
@@ -42,6 +48,8 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
await WaitUntilAsync(() => snapshotService.SubscribeCount >= 2);
|
||||
await WaitUntilAsync(() => hubContext.SendCount >= 1);
|
||||
|
||||
DateTimeOffset firstThrowAt = snapshotService.FirstThrowAt
|
||||
?? throw new InvalidOperationException("First subscribe did not record a throw timestamp.");
|
||||
DateTimeOffset secondSubscribeAt = snapshotService.SecondSubscribeAt
|
||||
?? throw new InvalidOperationException("Second subscribe did not record a timestamp.");
|
||||
|
||||
@@ -52,10 +60,13 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
$"Expected at least 2 subscribe calls, got {snapshotService.SubscribeCount}.");
|
||||
Assert.True(hubContext.SendCount >= 1);
|
||||
|
||||
// The gap between the throw (first subscribe) and the reconnect
|
||||
// (second subscribe) is bounded below by the reconnect delay. We
|
||||
// give a small slack (10ms) for scheduling jitter on slow CI VMs.
|
||||
TimeSpan gap = secondSubscribeAt - startedAt;
|
||||
// Tests-031: the gap is measured from the moment the first subscribe
|
||||
// actually threw (inside the fake) to the moment the second subscribe
|
||||
// began (also inside the fake). This isolates the publisher's
|
||||
// Task.Delay(reconnectDelay) — no StartAsync / scheduling overhead in
|
||||
// the baseline. The 10ms slack absorbs Task.Delay's coarse Windows
|
||||
// timer quantum (~15ms) when the underlying scheduler wakes early.
|
||||
TimeSpan gap = secondSubscribeAt - firstThrowAt;
|
||||
Assert.True(gap >= reconnectDelay - TimeSpan.FromMilliseconds(10),
|
||||
$"Expected reconnect gap >= {reconnectDelay.TotalMilliseconds}ms; got {gap.TotalMilliseconds}ms.");
|
||||
}
|
||||
@@ -100,6 +111,15 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
private sealed class ThrowOnceThenYieldSnapshotService : IDashboardSnapshotService
|
||||
{
|
||||
public int SubscribeCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tests-031: the wall-clock instant the first <c>WatchSnapshotsAsync</c> throws.
|
||||
/// The reconnect-gap assertion is measured against this timestamp (NOT the
|
||||
/// pre-<c>StartAsync</c> wall clock) so scheduling overhead is not baselined
|
||||
/// into the lower bound.
|
||||
/// </summary>
|
||||
public DateTimeOffset? FirstThrowAt { get; private set; }
|
||||
|
||||
public DateTimeOffset? SecondSubscribeAt { get; private set; }
|
||||
|
||||
public DashboardSnapshot GetSnapshot()
|
||||
@@ -118,6 +138,7 @@ public sealed class DashboardSnapshotPublisherTests
|
||||
// First call: throw after a brief yield so the publisher
|
||||
// observes us as a live producer that failed.
|
||||
await Task.Yield();
|
||||
FirstThrowAt = DateTimeOffset.UtcNow;
|
||||
throw new InvalidOperationException("simulated transient snapshot failure");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user