test(GWC-26): deflake ApplyReconcileBroadcastsAcknowledgeDelta

Seeding the cache with a live Raise transition raced the first reconcile: when
the reconcile snapshot populated the cache first, the still-buffered live Raise
was applied — and broadcast — after the test's feed subscriber had registered,
so the exactly-one-transition assertion saw two. Seed through a reconcile pass
instead (forced by a provider-mode probe, as the acked step already did) so no
live transition is ever in flight. Verified: 5 consecutive full alarm-monitor
runs green, and the test still fails (timeout) with the ApplyReconcile
acked-delta branch removed.
This commit is contained in:
Joseph Doherty
2026-08-07 05:49:34 -04:00
parent 3b6a239ed6
commit 1a75f61ebe
@@ -110,15 +110,19 @@ public sealed class GatewayAlarmMonitorAttachOrderTests
await monitor.StartAsync(cts.Token);
await sessions.WaitForSubscribeStartAsync(WaitTimeout);
// Seed the cache with an active (unacked) alarm and keep the reconcile snapshot in
// agreement, so a periodic reconcile pass cannot clear it out from under the test.
// Seed the cache with an active (unacked) alarm through a reconcile rather than a live
// transition: a buffered live transition could still be in flight when the cache first
// shows the alarm, and would then broadcast to the reader below and pollute the
// exactly-one-transition assertion. A provider-mode event forces the reconcile
// immediately, so the test never waits on the periodic timer.
sessions.SetReconcileSnapshot(Snapshot(AlarmConditionState.Active));
sessions.EmitEvent(Transition(1, AlarmTransitionKind.Raise));
sessions.EmitEvent(ProviderModeProbe(1));
await WaitUntilAsync(
() => monitor.CurrentAlarms.Any(alarm => alarm.AlarmFullReference == AlarmReference),
() => monitor.CurrentAlarms.Any(alarm => alarm.AlarmFullReference == AlarmReference
&& alarm.CurrentState == AlarmConditionState.Active),
WaitTimeout);
// Subscribe AFTER the raise: this reader's snapshot carries the alarm, so every
// Subscribe AFTER the seed: this reader's snapshot carries the alarm, so every
// transition it observes from here on is a reconcile-derived broadcast.
List<AlarmFeedMessage> received = [];
TaskCompletionSource snapshotComplete = new(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -129,17 +133,7 @@ public sealed class GatewayAlarmMonitorAttachOrderTests
// The worker now reports the same alarm acknowledged. A provider-mode event forces an
// immediate reconcile pass so the test does not wait on the periodic timer.
sessions.SetReconcileSnapshot(Snapshot(AlarmConditionState.ActiveAcked));
sessions.EmitEvent(new MxEvent
{
Family = MxEventFamily.OnAlarmProviderModeChanged,
WorkerSequence = 2,
OnAlarmProviderModeChanged = new OnAlarmProviderModeChangedEvent
{
Mode = AlarmProviderMode.Alarmmgr,
Reason = "probe",
At = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow),
},
});
sessions.EmitEvent(ProviderModeProbe(2));
AlarmFeedMessage acknowledge = await WaitForAsync(
received,
@@ -238,6 +232,20 @@ public sealed class GatewayAlarmMonitorAttachOrderTests
},
};
// A no-op provider-mode event. The monitor forces an immediate reconcile after every one,
// which is how these tests drive a reconcile pass without waiting on the periodic timer.
private static MxEvent ProviderModeProbe(ulong sequence) => new()
{
Family = MxEventFamily.OnAlarmProviderModeChanged,
WorkerSequence = sequence,
OnAlarmProviderModeChanged = new OnAlarmProviderModeChangedEvent
{
Mode = AlarmProviderMode.Alarmmgr,
Reason = "probe",
At = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow),
},
};
private static ActiveAlarmSnapshot Snapshot(AlarmConditionState state) => new()
{
AlarmFullReference = AlarmReference,