using System.Collections.Concurrent; using System.Runtime.CompilerServices; using System.Threading.Channels; using Google.Protobuf.WellKnownTypes; using Shouldly; using Xunit; using ZB.MOM.WW.MxGateway.Contracts.Proto; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Health; using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime; /// /// #859 — MXAccess's initial value for a freshly advised item can reach the event stream /// before the SubscribeBulk reply is registered: mxaccessgw frames the reply on a thread-pool /// continuation while the STA is already pumping the OnDataChange, and the reply and the event /// ride separate RPCs. Such an event used to be dropped as "stale after unsubscribe", leaving /// the node at its pre-subscribe value until the next change. These tests deliver the event /// strictly BEFORE the reply and pin that it is parked and replayed in order, while a genuinely /// stale event (no subscribe in flight) is still dropped. /// public sealed class EarlyEventWindowTests { private static MxEvent DataChange(int handle, double value) => new() { Family = MxEventFamily.OnDataChange, ItemHandle = handle, Value = new MxValue { DoubleValue = value }, Quality = 192, SourceTimestamp = Timestamp.FromDateTime(DateTime.UtcNow), }; private static GalaxyDriverOptions Opts() => new( new GalaxyGatewayOptions("https://mxgw.test:5001", "key"), new GalaxyMxAccessOptions("OtOpcUa-A"), new GalaxyRepositoryOptions(), new GalaxyReconnectOptions()); // ===== Registry ===== /// With no subscribe in flight an unknown handle is stale — the pre-#859 drop is kept. [Fact] public void Unknown_handle_without_a_pending_subscribe_is_stale() { var registry = new SubscriptionRegistry(); var epoch = registry.CurrentSession; registry.ResolveOrPark(DataChange(7, 1), epoch, out _, out _).ShouldBe(EarlyEventOutcome.Stale); registry.ParkedEventCount.ShouldBe(0); } /// Parked events are promoted, in arrival order, when the handle is registered. [Fact] public void Parked_events_are_promoted_in_order_on_register() { var registry = new SubscriptionRegistry(); var epoch = registry.CurrentSession; var woken = 0; registry.ParkedEventsReady += () => woken++; using (registry.BeginPendingSubscribe()) { registry.ResolveOrPark(DataChange(7, 1), epoch, out _, out _).ShouldBe(EarlyEventOutcome.Parked); registry.ResolveOrPark(DataChange(7, 2), epoch, out _, out _).ShouldBe(EarlyEventOutcome.Parked); registry.ResolveOrPark(DataChange(8, 9), epoch, out _, out _).ShouldBe(EarlyEventOutcome.Parked); registry.TakeReadyEvents(epoch).ShouldBeEmpty(); registry.Register(1, [new TagBinding("Tank.Level", 7)]); } woken.ShouldBe(1); registry.TakeReadyEvents(epoch).Select(e => e.Value.DoubleValue).ShouldBe([1.0, 2.0]); registry.TakeReadyEvents(epoch).ShouldBeEmpty(); // Handle 8 was never registered: closing the last window discarded it. registry.ParkedEventCount.ShouldBe(0); } /// A reconnect/re-snapshot Rebind promotes the new handles' early events too. [Fact] public void Rebind_promotes_parked_events_for_the_new_handles() { var registry = new SubscriptionRegistry(); var epoch = registry.CurrentSession; registry.Register(1, [new TagBinding("Tank.Level", 7)]); using (registry.BeginPendingSubscribe()) { registry.ResolveOrPark(DataChange(70, 5), epoch, out _, out _).ShouldBe(EarlyEventOutcome.Parked); registry.Rebind(1, [new TagBinding("Tank.Level", 70)]); } registry.TakeReadyEvents(epoch).Single().ItemHandle.ShouldBe(70); } /// Closing one of two overlapping windows keeps the other subscribe's parked events. [Fact] public void Closing_one_of_two_windows_keeps_parked_events() { var registry = new SubscriptionRegistry(); var epoch = registry.CurrentSession; var first = registry.BeginPendingSubscribe(); var second = registry.BeginPendingSubscribe(); registry.ResolveOrPark(DataChange(7, 1), epoch, out _, out _).ShouldBe(EarlyEventOutcome.Parked); first.Dispose(); first.Dispose(); // idempotent — must not close the second window registry.PendingSubscribeCount.ShouldBe(1); registry.ParkedEventCount.ShouldBe(1); second.Dispose(); registry.ParkedEventCount.ShouldBe(0); registry.ResolveOrPark(DataChange(7, 1), epoch, out _, out _).ShouldBe(EarlyEventOutcome.Stale); } /// /// Review #6: a window opening right after the previous one closed never inherits its /// leftovers (the N→0 discard and the 0→1 open are serialised under the park gate). /// [Fact] public void A_new_window_never_inherits_the_previous_windows_leftovers() { var registry = new SubscriptionRegistry(); var epoch = registry.CurrentSession; var first = registry.BeginPendingSubscribe(); registry.ResolveOrPark(DataChange(7, 1), epoch, out _, out _).ShouldBe(EarlyEventOutcome.Parked); first.Dispose(); using var second = registry.BeginPendingSubscribe(); registry.ParkedEventCount.ShouldBe(0); registry.Register(1, [new TagBinding("Reused", 7)]); registry.TakeReadyEvents(epoch).ShouldBeEmpty("the closed window's event must not reach the new owner of handle 7"); } /// Parking is bounded; past the bound the event is dropped as overflow. [Fact] public void Parking_is_bounded() { var registry = new SubscriptionRegistry(); var epoch = registry.CurrentSession; using var window = registry.BeginPendingSubscribe(); var ev = DataChange(7, 1); for (var i = 0; i < SubscriptionRegistry.MaxParkedEvents; i++) { registry.ResolveOrPark(ev, epoch, out _, out _).ShouldBe(EarlyEventOutcome.Parked); } registry.ResolveOrPark(ev, epoch, out _, out _).ShouldBe(EarlyEventOutcome.Overflow); registry.ParkedEventCount.ShouldBe(SubscriptionRegistry.MaxParkedEvents); } /// /// Review #5 + M-2: promoted-but-not-yet-replayed events stay counted until the dispatch thread /// takes them, and the next ResolveOrPark hands them back (in order) before anything else. /// [Fact] public void Ready_events_stay_counted_until_handed_back_by_the_next_resolve() { var registry = new SubscriptionRegistry(); var session = registry.CurrentSession; using var window = registry.BeginPendingSubscribe(); registry.ResolveOrPark(DataChange(7, 1), session, out _, out _); registry.ResolveOrPark(DataChange(7, 2), session, out _, out _); registry.Register(1, [new TagBinding("Tank.Level", 7)]); // promoted to ready registry.ParkedEventCount.ShouldBe(2); registry.ResolveOrPark(DataChange(8, 3), session, out _, out var replayFirst).ShouldBe(EarlyEventOutcome.Parked); replayFirst.Select(e => e.Value.DoubleValue).ShouldBe([1.0, 2.0]); registry.ParkedEventCount.ShouldBe(1, "only the newly parked event for handle 8 remains held"); } /// /// I-2: windows that overlap so the open count never reaches 0 (concurrent reads) must not /// accumulate unclaimed events — each is trimmed once every window that could claim it closed. /// [Fact] public void Overlapping_windows_that_never_all_close_do_not_accumulate_unclaimed_events() { var registry = new SubscriptionRegistry(); var session = registry.CurrentSession; var open = registry.BeginPendingSubscribe(); for (var i = 0; i < 1_000; i++) { // An unknown-handle event nobody will ever claim (e.g. a trailing event after a read's Remove). registry.ResolveOrPark(DataChange(10_000 + i, i), session, out _, out _).ShouldBe(EarlyEventOutcome.Parked); var next = registry.BeginPendingSubscribe(); // overlap: the count never drops to 0 open.Dispose(); open = next; registry.ParkedEventCount.ShouldBe(0, $"iteration {i}: the event's only possible owners have all closed"); } // Positive control: an event parked inside the still-open window survives and is claimable. registry.ResolveOrPark(DataChange(7, 42), session, out _, out _).ShouldBe(EarlyEventOutcome.Parked); var later = registry.BeginPendingSubscribe(); later.Dispose(); // a window opened AFTER the park closing must not trim it registry.ParkedEventCount.ShouldBe(1); registry.Register(1, [new TagBinding("Tank.Level", 7)]); registry.TakeReadyEvents(session).Single().Value.DoubleValue.ShouldBe(42.0); open.Dispose(); } /// I-3 interim: an unsubscribe purges events parked for its handles. [Fact] public void Purge_drops_events_parked_for_unsubscribed_handles() { var registry = new SubscriptionRegistry(); var session = registry.CurrentSession; using var window = registry.BeginPendingSubscribe(); registry.ResolveOrPark(DataChange(7, 1), session, out _, out _); // trailing event of a removed item registry.ResolveOrPark(DataChange(8, 2), session, out _, out _); // positive control registry.PurgeParked([7], session); registry.ParkedEventCount.ShouldBe(1); registry.Register(1, [new TagBinding("Reused", 7), new TagBinding("Other", 8)]); registry.TakeReadyEvents(session).Select(e => e.ItemHandle).ShouldBe([8]); } /// /// M-1: a live event for a RESOLVED handle that (defensively) still has a parked list is never /// parked behind it — the list is promoted and the event resolves, so no trim can lose it. /// [Fact] public void A_resolved_handle_with_a_stranded_parked_list_promotes_it_and_resolves() { var registry = new SubscriptionRegistry(); var session = registry.CurrentSession; var window = registry.BeginPendingSubscribe(); registry.Register(1, [new TagBinding("Tank.Level", 7)]); registry.ForceParkForTests(DataChange(7, 1)); // the "should be impossible" stranded list registry.ResolveOrPark(DataChange(7, 2), session, out var subs, out var replayFirst) .ShouldBe(EarlyEventOutcome.Resolved); subs.Single().FullReference.ShouldBe("Tank.Level"); replayFirst.Select(e => e.Value.DoubleValue).ShouldBe([1.0]); window.Dispose(); registry.ParkedEventCount.ShouldBe(0); } /// A registered handle resolves normally even while a window is open. [Fact] public void Registered_handle_resolves_while_a_window_is_open() { var registry = new SubscriptionRegistry(); var epoch = registry.CurrentSession; registry.Register(1, [new TagBinding("Tank.Level", 7)]); using var window = registry.BeginPendingSubscribe(); registry.ResolveOrPark(DataChange(7, 1), epoch, out var subs, out _).ShouldBe(EarlyEventOutcome.Resolved); subs.Single().FullReference.ShouldBe("Tank.Level"); } /// /// Review #3: held events belong to a session epoch. A new epoch (a new pump after a /// reconnect) discards parked AND promoted-but-unreplayed events, and an older pump can /// neither park nor replay — so a reused handle number never receives an old-session value. /// [Fact] public void A_new_session_discards_held_events_and_fences_the_old_session() { var registry = new SubscriptionRegistry(); var oldSession = registry.CurrentSession; using var window = registry.BeginPendingSubscribe(); registry.ResolveOrPark(DataChange(7, 111), oldSession, out _, out _).ShouldBe(EarlyEventOutcome.Parked); registry.ResolveOrPark(DataChange(8, 112), oldSession, out _, out _).ShouldBe(EarlyEventOutcome.Parked); registry.Register(1, [new TagBinding("Old.A", 8)]); // 8 promoted to ready, never replayed var newSession = registry.StartNewSession(); registry.ParkedEventCount.ShouldBe(0); registry.TakeReadyEvents(newSession).ShouldBeEmpty(); registry.Register(2, [new TagBinding("New.B", 7)]); // new session reuses handle 7 registry.TakeReadyEvents(newSession).ShouldBeEmpty("the old session's value must not reach New.B"); registry.ResolveOrPark(DataChange(7, 1), oldSession, out _, out _).ShouldBe(EarlyEventOutcome.Stale, "a dying old-session pump must not resolve through the map, even for a registered number"); registry.ResolveOrPark(DataChange(9, 1), oldSession, out _, out _).ShouldBe(EarlyEventOutcome.Stale); registry.TakeReadyEvents(oldSession).ShouldBeEmpty(); registry.ResolveOrPark(DataChange(9, 1), newSession, out _, out _).ShouldBe(EarlyEventOutcome.Parked, "positive control: the current session still parks"); } /// /// I-1: a new session invalidates the reverse map but keeps the subscriptions, so a new-session /// event on a number an old binding still holds PARKS (instead of resolving to the old tag) and /// is promoted by the replay's Rebind to the right tag. /// [Fact] public void A_new_session_invalidates_old_handles_so_colliding_numbers_park_until_rebind() { var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Old.A", 7)]); registry.Register(2, [new TagBinding("Old.B", 8)]); var session = registry.StartNewSession(); registry.TrackedSubscriptionCount.ShouldBe(2, "subscriptions survive for the replay to rebind"); using var window = registry.BeginPendingSubscribe(); // New session: Old.B is re-issued as handle 7 — the number Old.A still holds until its own Rebind. registry.ResolveOrPark(DataChange(7, 5), session, out _, out _).ShouldBe(EarlyEventOutcome.Parked); registry.TryRebind(2, [new TagBinding("Old.B", 7)], session).ShouldBe(SubscriptionRegistry.BindOutcome.Bound); registry.TakeReadyEvents(session).ShouldHaveSingleItem().Value.DoubleValue.ShouldBe(5.0); registry.ResolveSubscribers(7).ShouldBe([(2L, "Old.B")]); } /// #861: a registration or rebind carrying a stale session is refused. [Fact] public void Registration_with_a_stale_session_is_refused() { var registry = new SubscriptionRegistry(); var before = registry.CurrentSession; registry.Register(1, [new TagBinding("Tank.Level", 7)]); registry.StartNewSession(); registry.TryRegister(2, [new TagBinding("Late", 9)], before).ShouldBeFalse(); registry.Contains(2).ShouldBeFalse(); registry.TryRebind(1, [new TagBinding("Tank.Level", 9)], before).ShouldBe(SubscriptionRegistry.BindOutcome.SessionChanged); registry.ResolveSubscribers(9).ShouldBeEmpty(); registry.TryRegister(3, [new TagBinding("Fresh", 9)], registry.CurrentSession).ShouldBeTrue("positive control"); registry.Remove(1, out var live).ShouldNotBeNull(); live.ShouldBeFalse("never rebound in the new session — its handle 7 must not be unsubscribed"); registry.Remove(3, out live).ShouldNotBeNull(); live.ShouldBeTrue(); } // ===== EventPump ===== private sealed class ChannelSubscriber : IGalaxySubscriber { private readonly Channel _events = Channel.CreateUnbounded(); public Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) => Task.FromResult>([]); public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) => Task.CompletedTask; public async IAsyncEnumerable StreamEventsAsync([EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var ev in _events.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { yield return ev; } } public void Emit(MxEvent ev) => _events.Writer.TryWrite(ev).ShouldBeTrue(); } /// /// An initial value delivered before registration reaches the handler once the handle is /// registered — with NO further event on the stream (the wake path). /// [Fact] public async Task Pump_replays_an_early_event_once_the_handle_registers() { var subscriber = new ChannelSubscriber(); var registry = new SubscriptionRegistry(); var delivered = new ConcurrentQueue(); await using var pump = new EventPump(subscriber, registry, channelCapacity: 16, clientName: "Early"); pump.OnDataChange += (_, e) => delivered.Enqueue(e); pump.Start(); using (registry.BeginPendingSubscribe()) { subscriber.Emit(DataChange(7, 42)); await TestWait.UntilAsync(() => registry.ParkedEventCount == 1, "the early event must be parked, not dropped"); delivered.ShouldBeEmpty(); registry.Register(1, [new TagBinding("Tank.Level", 7)]); } await TestWait.UntilAsync(() => !delivered.IsEmpty, "the parked initial value must be replayed"); var args = delivered.Single(); args.FullReference.ShouldBe("Tank.Level"); ((double)args.Snapshot.Value!).ShouldBe(42.0); } /// The replayed early value precedes a later live value for the same handle. [Fact] public async Task Pump_replays_early_events_before_later_live_events() { var subscriber = new ChannelSubscriber(); var registry = new SubscriptionRegistry(); var delivered = new ConcurrentQueue(); await using var pump = new EventPump(subscriber, registry, channelCapacity: 16, clientName: "Early"); pump.OnDataChange += (_, e) => delivered.Enqueue((double)e.Snapshot.Value!); pump.Start(); using (registry.BeginPendingSubscribe()) { subscriber.Emit(DataChange(7, 1)); subscriber.Emit(DataChange(7, 2)); await TestWait.UntilAsync(() => registry.ParkedEventCount == 2, "both early events must be parked"); registry.Register(1, [new TagBinding("Tank.Level", 7)]); subscriber.Emit(DataChange(7, 3)); } await TestWait.UntilAsync(() => delivered.Count >= 3, "all three values must be delivered", () => string.Join(",", delivered)); delivered.ShouldBe([1.0, 2.0, 3.0]); } /// /// Review #2: a live event arriving while a registration has written the reverse map but not /// yet promoted (a 50k-binding Register is milliseconds of work) must NOT overtake the parked /// initial value. The hook runs inside that window, emits the newer value and gives the pump /// ample time to (wrongly) dispatch it; the pump must instead wait for the promotion. /// [Fact] public async Task Pump_does_not_let_a_live_event_overtake_a_registration_in_progress() { var subscriber = new ChannelSubscriber(); var registry = new SubscriptionRegistry(); var delivered = new ConcurrentQueue(); await using var pump = new EventPump(subscriber, registry, channelCapacity: 16, clientName: "Early"); pump.OnDataChange += (_, e) => delivered.Enqueue((double)e.Snapshot.Value!); pump.Start(); var deliveredDuringRegistration = -1; using (registry.BeginPendingSubscribe()) { subscriber.Emit(DataChange(7, 1)); await TestWait.UntilAsync(() => registry.ParkedEventCount == 1, "the initial value must be parked"); registry.AfterMapWriteForTests = () => { subscriber.Emit(DataChange(7, 2)); Thread.Sleep(300); deliveredDuringRegistration = delivered.Count; }; registry.Register(1, [new TagBinding("Tank.Level", 7)]); registry.AfterMapWriteForTests = null; } await TestWait.UntilAsync(() => delivered.Count >= 2, "both values must be delivered", () => string.Join(",", delivered)); deliveredDuringRegistration.ShouldBe(0, "nothing may dispatch for the handle mid-registration"); delivered.ShouldBe([1.0, 2.0]); } /// /// A genuinely stale event (no subscribe in flight) is still dropped; the positive control /// that follows it proves the pump processed it rather than not having got to it yet. /// [Fact] public async Task Pump_still_drops_a_stale_event_outside_any_window() { var subscriber = new ChannelSubscriber(); var registry = new SubscriptionRegistry(); registry.Register(1, [new TagBinding("Tank.Level", 7)]); var delivered = new ConcurrentQueue(); await using var pump = new EventPump(subscriber, registry, channelCapacity: 16, clientName: "Early"); pump.OnDataChange += (_, e) => delivered.Enqueue(e); pump.Start(); subscriber.Emit(DataChange(99, 5)); // unsubscribed handle subscriber.Emit(DataChange(7, 6)); // positive control, strictly after await TestWait.UntilAsync(() => !delivered.IsEmpty, "the positive control must arrive"); delivered.Single().FullReference.ShouldBe("Tank.Level"); registry.ParkedEventCount.ShouldBe(0); // Registering 99 now must not resurrect the stale event. registry.Register(2, [new TagBinding("Other", 99)]); registry.ParkedEventCount.ShouldBe(0); } /// /// Review #3, pump level: an event the old session's pump parked is never replayed by the /// next session's pump onto the tag that now owns the reused handle number. /// [Fact] public async Task Pump_after_reconnect_never_replays_an_old_session_event_onto_a_reused_handle() { var registry = new SubscriptionRegistry(); var delivered = new ConcurrentQueue(); using var window = registry.BeginPendingSubscribe(); // e.g. a subscribe hanging across the reconnect var oldStream = new ChannelSubscriber(); var oldPump = new EventPump(oldStream, registry, channelCapacity: 16, clientName: "Old"); oldPump.Start(); oldStream.Emit(DataChange(7, 111)); await TestWait.UntilAsync(() => registry.ParkedEventCount == 1, "the old session's event must be parked"); await oldPump.DisposeAsync(); registry.StartNewSession(); // what GalaxyDriver.ReplayAsync does before recreating the pump var newStream = new ChannelSubscriber(); await using var newPump = new EventPump(newStream, registry, channelCapacity: 16, clientName: "New"); newPump.OnDataChange += (_, e) => delivered.Enqueue(e); newPump.Start(); registry.Register(2, [new TagBinding("New.Tag", 7)]); // new session reuses handle 7 newStream.Emit(DataChange(7, 222)); // positive control on the new session await TestWait.UntilAsync(() => !delivered.IsEmpty, "the new session's value must arrive"); await TestWait.UntilAsync(() => registry.ParkedEventCount == 0, "nothing may stay held"); delivered.Select(e => (double)e.Snapshot.Value!).ShouldBe([222.0]); } // ===== GalaxyDriver / probe watcher end-to-end ===== /// /// A gateway fake that writes each new handle's initial value to the event stream and only /// returns the SubscribeBulk reply once the consumer's pump has parked that event — the /// event deterministically precedes the reply. /// private sealed class EventBeforeReplySubscriber : IGalaxySubscriber { private readonly Channel _events = Channel.CreateUnbounded(); private int _nextHandle = 100; /// Gets or sets the consumer's held-event count, polled before replying. public Func ParkedCount { get; set; } = () => 0; /// Gets the handles unsubscribed so far. public ConcurrentQueue Unsubscribed { get; } = new(); public async Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) { var results = new List(); var before = ParkedCount(); foreach (var fullRef in fullReferences) { var handle = Interlocked.Increment(ref _nextHandle); await _events.Writer.WriteAsync(DataChange(handle, handle * 10.0), cancellationToken); results.Add(new SubscribeResult { TagAddress = fullRef, ItemHandle = handle, WasSuccessful = true }); } await TestWait.UntilAsync(() => ParkedCount() >= before + fullReferences.Count, "the initial values must reach the pump (and be parked) before the reply is returned", () => $"held={ParkedCount()} before={before}"); return results; } public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) { foreach (var h in itemHandles) Unsubscribed.Enqueue(h); return Task.CompletedTask; } public async IAsyncEnumerable StreamEventsAsync([EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var ev in _events.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { yield return ev; } } } /// The driver publishes MXAccess's initial value even when it overtook the subscribe reply. [Fact] public async Task Driver_publishes_an_initial_value_that_arrived_before_the_subscribe_reply() { var subscriber = new EventBeforeReplySubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber); subscriber.ParkedCount = () => driver.ParkedEarlyEventCountForTests; var delivered = new ConcurrentQueue(); driver.OnDataChange += (_, e) => delivered.Enqueue(e); var handle = await driver.SubscribeAsync( ["Tank.Level", "Tank.Temp"], TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); await TestWait.UntilAsync(() => delivered.Count >= 2, "both initial values must be published"); delivered.Select(e => e.FullReference).OrderBy(r => r).ShouldBe(["Tank.Level", "Tank.Temp"]); delivered.ShouldAllBe(e => e.SubscriptionHandle.Equals(handle)); driver.ParkedEarlyEventCountForTests.ShouldBe(0); } /// /// Review #1: the subscribe-once read waits ONLY for the initial value, so an initial value /// that overtakes the reply must still complete the read Good — not BadTimeout. /// [Fact] public async Task Driver_read_returns_an_initial_value_that_arrived_before_the_subscribe_reply() { var subscriber = new EventBeforeReplySubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber); subscriber.ParkedCount = () => driver.ParkedEarlyEventCountForTests; var snapshots = await TestWait.ForAsync( driver.ReadAsync(["Tank.Level"], TestContext.Current.CancellationToken), "the read must complete"); var snapshot = snapshots.ShouldHaveSingleItem(); snapshot.StatusCode.ShouldBe(StatusCodeMap.Good); ((double)snapshot.Value!).ShouldBe(1010.0); driver.ParkedEarlyEventCountForTests.ShouldBe(0); } /// The probe watcher's initial ScanState overtaking its reply still reaches the fan-out. [Fact] public async Task Probe_watcher_initial_value_that_arrived_before_the_reply_is_dispatched() { var registry = new SubscriptionRegistry(); var subscriber = new EventBeforeReplySubscriber { ParkedCount = () => registry.ParkedEventCount }; var delivered = new ConcurrentQueue(); await using var pump = new EventPump(subscriber, registry, channelCapacity: 16, clientName: "Probe"); pump.OnDataChange += (_, e) => delivered.Enqueue(e); pump.Start(); using var watcher = new PerPlatformProbeWatcher(subscriber, new HostStatusAggregator(), registry); await watcher.SyncPlatformsAsync(["PlatformA"], TestContext.Current.CancellationToken); await TestWait.UntilAsync(() => !delivered.IsEmpty, "the initial ScanState must be dispatched"); var args = delivered.Single(); args.FullReference.ShouldBe("PlatformA.ScanState"); ((GalaxySubscriptionHandle)args.SubscriptionHandle).SubscriptionId.ShouldBe(SubscriptionRegistry.ProbeSubscriptionId); } // ===== Reconnect / session (#859 I-1, #861) ===== /// /// A gateway fake whose SubscribeBulk calls follow a per-call script (default: fresh handles). /// Events go on one stream that survives pump restarts. /// private sealed class ScriptedSubscriber : IGalaxySubscriber { private readonly Channel _events = Channel.CreateUnbounded(); private int _nextHandle = 1_000; public ConcurrentQueue, CancellationToken, Task>>> Script { get; } = new(); public ConcurrentQueue Unsubscribed { get; } = new(); public int SubscribeCalls; public Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) { Interlocked.Increment(ref SubscribeCalls); if (Script.TryDequeue(out var step)) return step(fullReferences, cancellationToken); return Task.FromResult>( [.. fullReferences.Select(r => Ok(r, Interlocked.Increment(ref _nextHandle)))]); } public Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) { foreach (var h in itemHandles) Unsubscribed.Enqueue(h); return Task.CompletedTask; } public async IAsyncEnumerable StreamEventsAsync([EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var ev in _events.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { yield return ev; } } public void Emit(MxEvent ev) => _events.Writer.TryWrite(ev).ShouldBeTrue(); } private static SubscribeResult Ok(string tag, int handle) => new() { TagAddress = tag, ItemHandle = handle, WasSuccessful = true }; /// /// I-1 end to end: after a reconnect the new session hands Tank.A the number Tank.B held in the /// old session (and vice versa), and each initial value arrives BEFORE its SubscribeBulk reply. /// Every value must reach its own tag — not the tag that owned the number before the reconnect. /// [Fact] public async Task Replay_with_colliding_handles_and_event_before_reply_delivers_each_value_to_its_own_tag() { var subscriber = new ScriptedSubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber); var registry = driver.SubscriptionsForTests; var delivered = new ConcurrentQueue<(string Ref, double Value)>(); driver.OnDataChange += (_, e) => delivered.Enqueue((e.FullReference, (double)e.Snapshot.Value!)); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 1)])); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 2)])); await driver.SubscribeAsync(["Tank.A"], TimeSpan.Zero, TestContext.Current.CancellationToken); await driver.SubscribeAsync(["Tank.B"], TimeSpan.Zero, TestContext.Current.CancellationToken); // New session: swapped numbers; each initial value is on the stream (and parked) before the reply. var newHandle = new Dictionary { ["Tank.A"] = 2, ["Tank.B"] = 1 }; var newValue = new Dictionary { ["Tank.A"] = 10.0, ["Tank.B"] = 20.0 }; for (var i = 0; i < 2; i++) { subscriber.Script.Enqueue(async (refs, _) => { var tag = refs[0]; // The previous iteration's promoted value stays counted until the pump replays it. await TestWait.UntilAsync(() => registry.ParkedEventCount == 0, "earlier promoted values must be replayed first"); subscriber.Emit(DataChange(newHandle[tag], newValue[tag])); await TestWait.UntilAsync(() => registry.ParkedEventCount > 0, $"{tag}'s new-session initial value must PARK, not resolve to the old owner of handle {newHandle[tag]}"); return [Ok(tag, newHandle[tag])]; }); } await driver.InvokeReplayForTestAsync(TestContext.Current.CancellationToken); await TestWait.UntilAsync(() => delivered.Count >= 2, "both replayed initial values must be published", () => string.Join(", ", delivered)); delivered.OrderBy(d => d.Ref).ShouldBe([("Tank.A", 10.0), ("Tank.B", 20.0)]); registry.ResolveSubscribers(1).Single().FullReference.ShouldBe("Tank.B"); registry.ResolveSubscribers(2).Single().FullReference.ShouldBe("Tank.A"); } /// /// #861: a SubscribeAsync whose reply returns after a reconnect does not register the dead /// old-session handles (nor unsubscribe their numbers) — it re-issues on the new session. /// [Fact] public async Task Subscribe_whose_reply_straddles_a_reconnect_is_reissued_on_the_new_session() { var subscriber = new ScriptedSubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber); var registry = driver.SubscriptionsForTests; var replyGate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); subscriber.Script.Enqueue(async (refs, _) => { await replyGate.Task; return [Ok(refs[0], 500)]; // an old-session handle }); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 600)])); var subscribe = driver.SubscribeAsync(["Tank.X"], TimeSpan.Zero, TestContext.Current.CancellationToken); await TestWait.UntilAsync(() => Volatile.Read(ref subscriber.SubscribeCalls) == 1, "the first SubscribeBulk must be in flight"); await driver.InvokeReplayForTestAsync(TestContext.Current.CancellationToken); // the reconnect replyGate.SetResult(); var handle = await TestWait.ForAsync(subscribe, "the subscribe must complete on the new session"); Volatile.Read(ref subscriber.SubscribeCalls).ShouldBe(2); registry.ResolveSubscribers(500).ShouldBeEmpty("the dead old-session handle must not be registered"); registry.ResolveSubscribers(600).ShouldBe([(((GalaxySubscriptionHandle)handle).SubscriptionId, "Tank.X")]); subscriber.Unsubscribed.ShouldNotContain(500, "its number may belong to a new-session item"); } /// #861: a subscribe-once read whose reply straddles a reconnect reports the lost session. [Fact] public async Task Read_whose_reply_straddles_a_reconnect_returns_bad_communication_and_leaves_old_handles_alone() { var subscriber = new ScriptedSubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber); var registry = driver.SubscriptionsForTests; var replyGate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); subscriber.Script.Enqueue(async (refs, _) => { await replyGate.Task; return [Ok(refs[0], 700)]; }); var read = driver.ReadAsync(["Tank.Y"], TestContext.Current.CancellationToken); await TestWait.UntilAsync(() => Volatile.Read(ref subscriber.SubscribeCalls) == 1, "the read's SubscribeBulk must be in flight"); await driver.InvokeReplayForTestAsync(TestContext.Current.CancellationToken); replyGate.SetResult(); var snapshot = (await TestWait.ForAsync(read, "the read must complete")).ShouldHaveSingleItem(); snapshot.StatusCode.ShouldBe(StatusCodeMap.BadCommunicationError); registry.ResolveSubscribers(700).ShouldBeEmpty(); subscriber.Unsubscribed.ShouldBeEmpty(); registry.PendingSubscribeCount.ShouldBe(0, "the read's early-event window must be closed"); } /// The read's internal-deadline path closes its early-event window. [Fact] public async Task Read_deadline_path_closes_its_window() { var subscriber = new ScriptedSubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber) { ReadViaSubscribeDeadline = TimeSpan.FromMilliseconds(100), }; var never = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); subscriber.Script.Enqueue((_, _) => never.Task); var snapshot = (await TestWait.ForAsync( driver.ReadAsync(["Tank.Z"], TestContext.Current.CancellationToken), "the read must time out")).ShouldHaveSingleItem(); snapshot.StatusCode.ShouldBe(StatusCodeMap.BadTimeout); driver.SubscriptionsForTests.PendingSubscribeCount.ShouldBe(0); never.SetResult([]); // let the abandoned RPC finish cleanly } // ===== Third review (B-1, I-A, I-B, I-C, P-1, M-a) ===== /// /// B-1: a read rebound by a reconnect replay while it waited unsubscribes its NEW handle, not the /// original number (which may by now be another tag's live item). /// [Fact] public async Task Read_rebound_by_a_replay_unsubscribes_the_new_handle_not_the_original() { var subscriber = new ScriptedSubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 1)])); subscriber.Script.Enqueue((refs, _) => { subscriber.Emit(DataChange(9, 99.0)); // the replayed initial value (may overtake the reply) return Task.FromResult>([Ok(refs[0], 9)]); }); var read = driver.ReadAsync(["Tank.R"], TestContext.Current.CancellationToken); await TestWait.UntilAsync(() => driver.TrackedSubscriptionCountForTests == 1, "the read must be registered and waiting"); await driver.InvokeReplayForTestAsync(TestContext.Current.CancellationToken); var snapshot = (await TestWait.ForAsync(read, "the replayed value must complete the read")).ShouldHaveSingleItem(); ((double)snapshot.Value!).ShouldBe(99.0); subscriber.Unsubscribed.ToArray().ShouldBe([9], "the read must release its NEW handle, never the original number 1"); } /// P-1: a reconnect with nothing to replay still replaces the dead pump; a later subscribe gets data. [Fact] public async Task Reconnect_with_no_subscriptions_restarts_the_pump() { var subscriber = new ScriptedSubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber); var delivered = new ConcurrentQueue(); driver.OnDataChange += (_, e) => delivered.Enqueue(e); var first = await driver.SubscribeAsync(["Tank.Old"], TimeSpan.Zero, TestContext.Current.CancellationToken); await driver.UnsubscribeAsync(first, TestContext.Current.CancellationToken); var deadPump = driver.EventPumpInstanceForTests.ShouldNotBeNull(); await driver.InvokeReplayForTestAsync(TestContext.Current.CancellationToken); driver.EventPumpInstanceForTests.ShouldNotBeSameAs(deadPump, "the faulted pump must be replaced even with no entries"); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 42)])); await driver.SubscribeAsync(["Tank.New"], TimeSpan.Zero, TestContext.Current.CancellationToken); subscriber.Emit(DataChange(42, 4.2)); await TestWait.UntilAsync(() => !delivered.IsEmpty, "a subscribe after the reconnect must receive data"); delivered.Single().FullReference.ShouldBe("Tank.New"); } /// /// I-C: the reopen fences the registry around the session swap. An unsubscribe during the /// reopen/backoff never sends an old number; a subscribe issued after the swap registers normally /// and the replay does not re-subscribe it (no double advise). /// [Fact] public async Task Subscribe_after_the_session_swap_but_before_replay_is_live_and_not_replayed() { var subscriber = new ScriptedSubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber) { RecreateOverrideForTests = _ => Task.CompletedTask, StreamRequestWait = TimeSpan.FromMilliseconds(100), // advises in the reopen window on purpose (#865 bound) }; var delivered = new ConcurrentQueue<(string Ref, double Value)>(); driver.OnDataChange += (_, e) => delivered.Enqueue((e.FullReference, (double)e.Snapshot.Value!)); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 1)])); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 3)])); await driver.SubscribeAsync(["Tank.A"], TimeSpan.Zero, TestContext.Current.CancellationToken); var c = await driver.SubscribeAsync(["Tank.C"], TimeSpan.Zero, TestContext.Current.CancellationToken); await driver.InvokeReopenForTestAsync(TestContext.Current.CancellationToken); // gateway session swapped await driver.UnsubscribeAsync(c, TestContext.Current.CancellationToken); subscriber.Unsubscribed.ShouldNotContain(3, "an old-session number must never reach the new session"); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 50)])); await driver.SubscribeAsync(["Tank.B"], TimeSpan.Zero, TestContext.Current.CancellationToken); var callsBeforeReplay = Volatile.Read(ref subscriber.SubscribeCalls); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 51)])); await driver.InvokeReplayForTestAsync(TestContext.Current.CancellationToken); (Volatile.Read(ref subscriber.SubscribeCalls) - callsBeforeReplay).ShouldBe(1, "only Tank.A (old session) is re-subscribed"); var registry = driver.SubscriptionsForTests; registry.ResolveSubscribers(50).Single().FullReference.ShouldBe("Tank.B"); registry.ResolveSubscribers(51).Single().FullReference.ShouldBe("Tank.A"); subscriber.Emit(DataChange(50, 5.0)); await TestWait.UntilAsync(() => !delivered.IsEmpty, "the post-swap subscription must stay live across the replay"); delivered.Single().ShouldBe(("Tank.B", 5.0)); } /// /// M-a: a subscription removed while the replay's SubscribeBulk for it was in flight gets the /// freshly advised handles released (nothing else would ever unsubscribe them). /// [Fact] public async Task Replay_releases_handles_for_a_subscription_removed_mid_replay() { var subscriber = new ScriptedSubscriber(); using var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 1)])); var handle = await driver.SubscribeAsync(["Tank.Gone"], TimeSpan.Zero, TestContext.Current.CancellationToken); subscriber.Script.Enqueue(async (refs, _) => { await driver.UnsubscribeAsync(handle, CancellationToken.None); // removed mid-replay return [Ok(refs[0], 77)]; }); await driver.InvokeReplayForTestAsync(TestContext.Current.CancellationToken); subscriber.Unsubscribed.ToArray().ShouldBe([77], "the re-advised handle is released; the dead number 1 is not sent"); driver.SubscriptionsForTests.ResolveSubscribers(77).ShouldBeEmpty(); } /// /// #865 stress finding: the dispatch fast path looked the handle up, then checked for an open window — /// not atomically. A subscribe that registered the handle AND closed its window in between made the /// event "stale" and its initial value was dropped for good. It must resolve. /// [Fact] public void A_registration_landing_between_the_fast_path_lookup_and_the_window_check_still_resolves() { var registry = new SubscriptionRegistry(); var session = registry.CurrentSession; var window = registry.BeginPendingSubscribe(); registry.AfterFastPathMissForTests = () => { registry.AfterFastPathMissForTests = null; registry.TryRegister(registry.NextSubscriptionId(), [new TagBinding("Tank.A", 7)], session).ShouldBeTrue(); window.Dispose(); // the subscribe's window closes right after its registration }; registry.ResolveOrPark(DataChange(7, 1), session, out var subscribers, out _).ShouldBe(EarlyEventOutcome.Resolved); subscribers.Single().FullReference.ShouldBe("Tank.A"); // Positive control for the Stale branch: an unknown handle with no window open is still dropped. registry.ResolveOrPark(DataChange(8, 1), session, out _, out _).ShouldBe(EarlyEventOutcome.Stale); } /// I-B: a purge that completes after a reconnect does not delete the new session's parked values. [Fact] public void A_purge_for_an_older_session_is_a_no_op() { var registry = new SubscriptionRegistry(); var oldSession = registry.CurrentSession; var newSession = registry.StartNewSession(); using var window = registry.BeginPendingSubscribe(); registry.ResolveOrPark(DataChange(7, 1), newSession, out _, out _).ShouldBe(EarlyEventOutcome.Parked); registry.PurgeParked([7], oldSession); // a slow old-session unsubscribe finishing late registry.ParkedEventCount.ShouldBe(1, "the new session's parked initial value must survive"); registry.PurgeParked([7], newSession); // positive control registry.ParkedEventCount.ShouldBe(0); } /// I-A: a probe seed whose reply straddles a reconnect records nothing dead and is re-issued. [Fact] public async Task Probe_seed_straddling_a_reconnect_is_reissued_and_never_publishes_the_dead_handle() { var registry = new SubscriptionRegistry(); var subscriber = new ScriptedSubscriber(); var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); subscriber.Script.Enqueue(async (refs, _) => { await gate.Task; return [Ok(refs[0], 5)]; // old-session handle }); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 6)])); using var watcher = new PerPlatformProbeWatcher(subscriber, new HostStatusAggregator(), registry); var sync = watcher.SyncPlatformsAsync(["P"], TestContext.Current.CancellationToken); await TestWait.UntilAsync(() => Volatile.Read(ref subscriber.SubscribeCalls) == 1, "the seed's SubscribeBulk must be in flight"); registry.StartNewSession(); gate.SetResult(); await TestWait.ForAsync(sync, "the seed must complete"); Volatile.Read(ref subscriber.SubscribeCalls).ShouldBe(2); watcher.CurrentBindings.Single().ItemHandle.ShouldBe(6); registry.ResolveSubscribers(5).ShouldBeEmpty(); registry.ResolveSubscribers(6).Single().FullReference.ShouldBe("P.ScanState"); subscriber.Unsubscribed.ShouldBeEmpty(); } /// /// I-A: after a reconnect (before the replay rebinds probes), a seed neither re-publishes the old /// session's probe numbers nor unsubscribes a removed platform's old number. /// [Fact] public async Task Probe_watcher_never_publishes_or_unsubscribes_old_session_numbers() { var subscriber = new ScriptedSubscriber(); using var driver = await InitializedDriverAsync(subscriber); var watcher = driver.ProbeWatcherForTests.ShouldNotBeNull(); var registry = driver.SubscriptionsForTests; subscriber.Script.Enqueue((refs, _) => Task.FromResult>( [.. refs.Select((r, i) => Ok(r, i + 1))])); // P → 1, Q → 2 await watcher.SyncPlatformsAsync(["P", "Q"], TestContext.Current.CancellationToken); await driver.InvokeReopenForTestAsync(TestContext.Current.CancellationToken); // gateway session swapped // The new session reuses number 1 for a user tag. subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 1)])); await driver.SubscribeAsync(["User.Tag"], TimeSpan.Zero, TestContext.Current.CancellationToken); await watcher.SyncPlatformsAsync(["P"], TestContext.Current.CancellationToken); // a seed in the gap drops Q subscriber.Unsubscribed.ShouldBeEmpty("Q's handle 2 is an old-session number"); registry.ResolveSubscribers(1).Single().FullReference.ShouldBe("User.Tag", "P's old-session number must not be re-published"); var replayed = new ConcurrentQueue(); subscriber.Script.Enqueue((refs, _) => { foreach (var r in refs) replayed.Enqueue(r); return Task.FromResult>([Ok(refs[0], 7)]); }); await driver.InvokeReplayForTestAsync(TestContext.Current.CancellationToken); // the driver's replay replayed.ToArray().ShouldBe(["P.ScanState"]); watcher.CurrentBindings.Single().ItemHandle.ShouldBe(7); registry.ResolveSubscribers(7).Single().FullReference.ShouldBe("P.ScanState"); } /// /// Final review blocker: a probe seed in the gap between the session swap and the replay /// re-stamps the shared probe registry entry as current. The replay must still re-subscribe the /// platforms whose handles predate the swap (they would otherwise freeze at handle 0) — and only /// those: the platform the seed just advised on the new session is not advised twice. /// [Fact] public async Task Replay_after_a_seed_in_the_gap_resubscribes_exactly_the_stale_platforms() { var subscriber = new ScriptedSubscriber(); using var driver = await InitializedDriverAsync(subscriber); var watcher = driver.ProbeWatcherForTests.ShouldNotBeNull(); var registry = driver.SubscriptionsForTests; subscriber.Script.Enqueue((refs, _) => Task.FromResult>( [.. refs.Select((r, i) => Ok(r, i + 1))])); // P → 1, Q → 2 await watcher.SyncPlatformsAsync(["P", "Q"], TestContext.Current.CancellationToken); await driver.InvokeReopenForTestAsync(TestContext.Current.CancellationToken); subscriber.Script.Enqueue((refs, _) => Task.FromResult>([Ok(refs[0], 20)])); await watcher.SyncPlatformsAsync(["P", "Q", "R"], TestContext.Current.CancellationToken); // seed adds R on the new session registry.ResolveSubscribers(20).Single().FullReference.ShouldBe("R.ScanState"); var replayed = new ConcurrentQueue(); subscriber.Script.Enqueue((refs, _) => { foreach (var r in refs) replayed.Enqueue(r); return Task.FromResult>([.. refs.Select((r, i) => Ok(r, 30 + i))]); }); await driver.InvokeReplayForTestAsync(TestContext.Current.CancellationToken); replayed.OrderBy(r => r).ToArray().ShouldBe(["P.ScanState", "Q.ScanState"], "R was advised on the new session already"); var bindings = watcher.CurrentBindings.ToDictionary(b => b.FullReference, b => b.ItemHandle); bindings["R.ScanState"].ShouldBe(20); bindings["P.ScanState"].ShouldBeGreaterThanOrEqualTo(30); bindings["Q.ScanState"].ShouldBeGreaterThanOrEqualTo(30); foreach (var (reference, handle) in bindings) { registry.ResolveSubscribers(handle).Single().FullReference.ShouldBe(reference); } } private static async Task InitializedDriverAsync(ScriptedSubscriber subscriber) { var driver = new GalaxyDriver( "g", Opts(), hierarchySource: null, dataReader: null, dataWriter: null, subscriber: subscriber) { RecreateOverrideForTests = _ => Task.CompletedTask, // These tests advise in the reopen → replay window on purpose (fencing, not values). #865 makes // such an advise wait for the replay's pump, bounded; keep the bound short so they don't idle. StreamRequestWait = TimeSpan.FromMilliseconds(100), }; await driver.InitializeAsync("{}", TestContext.Current.CancellationToken); return driver; } }