diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GatewayGalaxyLiveReopenAndWriteTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GatewayGalaxyLiveReopenAndWriteTests.cs index e0e7208f..f2a3cb1e 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GatewayGalaxyLiveReopenAndWriteTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/Runtime/GatewayGalaxyLiveReopenAndWriteTests.cs @@ -124,6 +124,55 @@ public sealed class GatewayGalaxyLiveReopenAndWriteTests $"write-persist smoke: {WriteRef} {current} -> wrote {target} -> fresh-session read-back {persisted}"); } + /// + /// Borrow — the writer reuses a live MXAccess item handle the subscription registry + /// already holds, so the first write to an already-subscribed tag skips a redundant AddItem. + /// Proves the load-bearing premise (a subscription handle is usable for a no-login supervisory + /// write that commits) AND the optimization (zero AddItem on the borrow path). Control: a writer + /// with no registry source on the same session AddItems exactly once. + /// + [Fact] + public async Task Live_writer_borrows_subscription_handle_and_skips_AddItem() + { + var (endpoint, apiKey) = RequireLiveGatewayOrSkip(); + var ct = TestContext.Current.CancellationToken; + var clientOptions = BuildClientOptions(endpoint, apiKey); + + await using var session = new GalaxyMxSession(new GalaxyMxAccessOptions(ClientName: "OtOpcUaBorrowSmoke")); + await session.ConnectAsync(clientOptions, ct); + + // Subscribe the tag so the registry holds the gateway's REAL item handle for it. + var subscriber = new GatewayGalaxySubscriber(session); + var subResults = await subscriber.SubscribeBulkAsync([WriteRef], bufferedUpdateIntervalMs: 0, ct); + var match = subResults.SingleOrDefault(r => + string.Equals(r.TagAddress, WriteRef, StringComparison.OrdinalIgnoreCase) && r.WasSuccessful); + match.ShouldNotBeNull($"the gateway should accept a subscription on {WriteRef}"); + match!.ItemHandle.ShouldBeGreaterThan(0, "a successful subscribe must return a positive item handle"); + + var registry = new SubscriptionRegistry(); + registry.Register(1, [new TagBinding(WriteRef, match.ItemHandle)]); + + // Borrow case: a writer wired to the registry must commit the write WITHOUT a fresh AddItem, + // and must NOT cache the borrowed handle (the registry owns its lifecycle). + var borrowingWriter = new GatewayGalaxyDataWriter( + session, writeUserId: 0, logger: null, subscribedHandleSource: registry.TryResolveItemHandle); + var borrowed = await borrowingWriter.WriteAsync( + [new WriteRequest(WriteRef, 2727.0f)], _ => SecurityClassification.FreeAccess, ct); + borrowed.ShouldHaveSingleItem().StatusCode.ShouldBe(0u, "the borrowed-handle write should commit Good (0)"); + borrowingWriter.AddItemCallCount.ShouldBe(0, "the writer should reuse the subscription handle, not AddItem"); + borrowingWriter.CachedItemHandleCount.ShouldBe(0, "a borrowed handle must not be cached in the writer"); + + // Control: a writer with NO source on the same session must AddItem exactly once for the same tag. + var plainWriter = new GatewayGalaxyDataWriter(session, writeUserId: 0); + var control = await plainWriter.WriteAsync( + [new WriteRequest(WriteRef, 3838.0f)], _ => SecurityClassification.FreeAccess, ct); + control.ShouldHaveSingleItem().StatusCode.ShouldBe(0u, "the control write should commit Good (0)"); + plainWriter.AddItemCallCount.ShouldBe(1, "with no borrow source the writer must AddItem once"); + + TestContext.Current.SendDiagnosticMessage( + $"borrow smoke: subscribed {WriteRef} -> handle {match.ItemHandle}; borrowed write Good with AddItemCallCount=0; control AddItemCallCount=1"); + } + private static (string Endpoint, string ApiKey) RequireLiveGatewayOrSkip() { var endpoint = Environment.GetEnvironmentVariable("MXGW_ENDPOINT");