test(transport): row 74 — gauge asserts become delta-over-scoped-action, not absolute

BundleSessionObservabilityTests' two gauge facts asserted absolute values
(Assert.Equal(0, baseline), Assert.Equal(2, ReadGauge())) against what is a
single process-global provider slot — the same latent shape as the row-32
QueueDepthGaugeTests flake, correct only while this file is the slot's sole
registrant. Converted to the delta-over-scoped-action pattern the row-66
rejection-counter facts in the same file already use: a new ReadGaugeDelta
helper measures how far the instrument MOVED across an action driven by this
test's own store, so an inherited non-zero baseline cancels out.

Two assertions got stronger in the process. The post-clear check was
Assert.Equal(0, ReadGauge()) — "the instrument reports zero", which any
unregistered slot satisfies; it is now "opening more sessions on the detached
store moves the gauge by nothing", which is the actual claim. And the
compare-and-clear fact gained a negative control: the superseded store's opens
must NOT reach the instrument, so a slot still pointing at oldStore can no
longer satisfy the assertion by coincidence when both stores hold equal counts.

Fail-before (temporary probe, not committed): with a foreign provider returning
7 registered before the service starts, the OLD body fails
`Assert.Equal(0, baseline)` with `Expected: 0 / Actual: 7`; the NEW delta body
passes the identical scenario.

Honest residual documented on the helper: a gauge has one slot and
last-writer-wins, so a registrant that clobbers it BETWEEN the two reads is
unrecoverable read-side — that needs row-32's mutual exclusion. It cannot arise
today (this is the only file in the assembly constructing a
BundleSessionEvictionService, and test assemblies are separate processes); a
second such file should bring the collection treatment with it.

Transport.Tests: 180/180 green.
This commit is contained in:
Joseph Doherty
2026-08-15 21:16:55 -04:00
parent 3dc38fb674
commit 98c9458830
@@ -215,30 +215,39 @@ public sealed class BundleSessionObservabilityTests
public async Task EvictionService_RegistersTheGaugeProvider_AndClearsItOnStop()
{
// The eviction service is Transport's only lifecycle seam, so it owns the
// process-global gauge slot. The gauge is read as a delta against a baseline
// because the instrument is a process-wide static.
// process-global gauge slot. Every assertion here is a DELTA over a scoped action
// against this test's own store (register row 74) — see ReadGaugeDelta.
var clock = new TestClock(DateTimeOffset.UtcNow);
var store = new BundleSessionStore(Options.Create(new TransportOptions()), clock, NullLogger<BundleSessionStore>.Instance);
var service = new BundleSessionEvictionService(store, NullLogger<BundleSessionEvictionService>.Instance);
var baseline = ReadGauge();
await service.StartAsync(CancellationToken.None);
try
{
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
// While registered, this store's opens are what the gauge reports.
var whileRegistered = ReadGaugeDelta(() =>
{
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
});
Assert.Equal(2, ReadGauge());
Assert.Equal(2, whileRegistered);
}
finally
{
await service.StopAsync(CancellationToken.None);
}
// After the clear the gauge falls back to reporting 0, not through a dead store.
Assert.Equal(0, ReadGauge());
Assert.Equal(0, baseline);
// After the clear the gauge no longer reads through this store: opening more
// sessions on it moves the instrument by nothing. Stated as a delta this is a
// STRONGER claim than the old `Assert.Equal(0, ReadGauge())` — that one merely
// said "the instrument reports zero", which any unregistered slot satisfies,
// and which a foreign registrant would have failed for reasons unrelated to
// the clear.
var afterClear = ReadGaugeDelta(() =>
store.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))));
Assert.Equal(0, afterClear);
}
[Fact]
@@ -257,29 +266,81 @@ public sealed class BundleSessionObservabilityTests
await newService.StartAsync(CancellationToken.None);
try
{
newStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30)));
// Late stop of the superseded instance.
// Late stop of the superseded instance, then prove the NEWER store is still
// the one driving the gauge — by moving it and measuring the movement.
await oldService.StopAsync(CancellationToken.None);
Assert.Equal(1, ReadGauge());
var newStoreStillDrivesIt = ReadGaugeDelta(() =>
newStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))));
Assert.Equal(1, newStoreStillDrivesIt);
// Negative control on the same window: the superseded store is detached, so
// its opens must not reach the instrument. Without this, a provider slot left
// pointing at oldStore would still satisfy the assertion above by coincidence
// whenever both stores happened to hold equal counts.
var oldStoreIsDetached = ReadGaugeDelta(() =>
oldStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))));
Assert.Equal(0, oldStoreIsDetached);
}
finally
{
await newService.StopAsync(CancellationToken.None);
}
Assert.Equal(0, ReadGauge());
var afterClear = ReadGaugeDelta(() =>
newStore.Open(SessionExpiringAt(clock.GetUtcNow().AddMinutes(30))));
Assert.Equal(0, afterClear);
}
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
/// <summary>
/// Runs <paramref name="act"/> between two observations of the open-import-sessions
/// gauge and returns how far the instrument MOVED across it.
/// </summary>
/// <remarks>
/// <para>
/// Register row 74. These tests used to assert absolute gauge values —
/// <c>Assert.Equal(0, baseline)</c>, <c>Assert.Equal(2, ReadGauge())</c> — against
/// what is a single process-global provider slot
/// (<c>SetImportSessionCountProvider</c> / <c>ClearImportSessionCountProvider</c>).
/// That is the same shape as the register row 32 flake in
/// <c>QueueDepthGaugeTests</c>: correct only for as long as this file is the slot's
/// sole registrant, and silently wrong the day a sibling suite writes it. A delta
/// carries no assumption about what the slot held on entry, so a non-zero inherited
/// baseline cancels out.
/// </para>
/// <para>
/// Honest residual, and the reason this is a delta rather than a full fix: unlike
/// <see cref="ReadRejectionCounter"/> — where every measurement in the window is
/// driven synchronously by this test's own <c>Open</c> call, making it genuinely
/// immune — a gauge has ONE slot and last-writer-wins. If a concurrent registrant
/// overwrote the slot BETWEEN the two reads, no read-side technique could recover;
/// that case needs mutual exclusion on the slot (row 32's serialized collection).
/// It cannot arise today: this is the only file in the assembly that constructs a
/// <c>BundleSessionEvictionService</c>, and test assemblies run in separate
/// processes. A second such file should bring the collection treatment with it.
/// </para>
/// </remarks>
/// <param name="act">The action to measure across.</param>
/// <returns>The gauge's movement while <paramref name="act"/> ran.</returns>
private static long ReadGaugeDelta(Action act)
{
var before = ReadGauge();
act();
return ReadGauge() - before;
}
/// <summary>
/// Forces one synchronous observation of the open-import-sessions gauge and returns
/// the measured value. Mirrors the MeterListener helper used by the Communication
/// and StoreAndForward telemetry suites.
/// and StoreAndForward telemetry suites. Private to <see cref="ReadGaugeDelta"/> by
/// convention — see its remarks for why no test reads this absolutely.
/// </summary>
/// <returns>The gauge's current value, or 0 if it never reported.</returns>
private static long ReadGauge()