test(galaxy): live-gw smoke — writer borrows subscription handle, skips AddItem

Subscribe a real tag, register its gateway item handle, write via the
registry-wired writer: asserts the borrowed-handle write commits Good with
AddItemCallCount==0 (control with no source: ==1). Proves the subscription
handle is usable for a committing no-login supervisory write. Skip-gated on
MXGW_ENDPOINT + GALAXY_MXGW_API_KEY; verified live vs 10.100.0.48:5120 (3/3).
This commit is contained in:
Joseph Doherty
2026-06-18 04:34:11 -04:00
parent e9da9c29d2
commit 15922d8483
@@ -124,6 +124,55 @@ public sealed class GatewayGalaxyLiveReopenAndWriteTests
$"write-persist smoke: {WriteRef} {current} -> wrote {target} -> fresh-session read-back {persisted}");
}
/// <summary>
/// <b>Borrow</b> — 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.
/// </summary>
[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");