using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime;
///
/// Tests for .
/// The SDK session types are sealed with internal ctors and cannot be faked, so we
/// drive the cache-seeding path through
/// and verify the
/// handle-count seams — the contract under test is purely that
/// zeroes both dictionaries
/// so the next write is forced to re-AddItem + re-AdviseSupervisory.
///
public sealed class GatewayGalaxyDataWriterTests
{
private static GalaxyMxSession MinimalSession()
=> new(new GalaxyMxAccessOptions(ClientName: "OtOpcUa-Test"));
///
/// Approach (b): seed the item-handle cache directly via the internal test seam,
/// confirm the count is positive, call ,
/// and confirm both caches are cleared.
/// The next write (not simulated here — needs a live gw) would therefore be forced
/// to re-AddItem because the cache is empty.
///
[Fact]
public void InvalidateHandleCaches_clears_item_and_supervised_handle_caches()
{
var session = MinimalSession();
var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);
// Pre-seed both caches via the internal test seam so we can assert the
// "after a write" state without spinning up a real gRPC gateway session.
writer.SeedHandleCachesForTest("TestMachine_001.TestAttr", itemHandle: 42, supervised: true);
writer.CachedItemHandleCount.ShouldBe(1);
writer.CachedSupervisedHandleCount.ShouldBe(1);
writer.InvalidateHandleCaches();
writer.CachedItemHandleCount.ShouldBe(0);
writer.CachedSupervisedHandleCount.ShouldBe(0);
}
///
/// A second seed + invalidate cycle proves the method isn't one-shot — a reconnect
/// followed by writes followed by another reconnect must also start fresh.
///
[Fact]
public void InvalidateHandleCaches_is_repeatable_across_multiple_reconnects()
{
var session = MinimalSession();
var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);
// First session cycle
writer.SeedHandleCachesForTest("Tag.A", itemHandle: 1, supervised: false);
writer.SeedHandleCachesForTest("Tag.B", itemHandle: 2, supervised: true);
writer.CachedItemHandleCount.ShouldBe(2);
writer.InvalidateHandleCaches();
writer.CachedItemHandleCount.ShouldBe(0);
writer.CachedSupervisedHandleCount.ShouldBe(0);
// Second session cycle — handles re-populated after the reconnect's replay
writer.SeedHandleCachesForTest("Tag.A", itemHandle: 99, supervised: true);
writer.CachedItemHandleCount.ShouldBe(1);
writer.InvalidateHandleCaches();
writer.CachedItemHandleCount.ShouldBe(0);
}
///
/// on a fresh (never-used)
/// writer must be a no-op rather than throwing — the reconnect supervisor may call it
/// before any write has occurred.
///
[Fact]
public void InvalidateHandleCaches_on_empty_caches_is_a_noop()
{
var session = MinimalSession();
var writer = new GatewayGalaxyDataWriter(session, writeUserId: 0);
// Caches are empty — must not throw.
writer.CachedItemHandleCount.ShouldBe(0);
writer.CachedSupervisedHandleCount.ShouldBe(0);
writer.InvalidateHandleCaches();
writer.CachedItemHandleCount.ShouldBe(0);
writer.CachedSupervisedHandleCount.ShouldBe(0);
}
// The former subscribedHandleSource / TryResolveCachedOrBorrowed tests are gone with the
// borrow-for-write optimization itself: a subscription's handle is plain-advised, and MXAccess
// both refuses a no-login write through it (SECURITY_ERROR detail 1008) and refuses upgrading
// its advise mode (E_INVALIDARG) — live-captured on the correlated write replies (06/S-1). The
// writer now always AddItems its own supervisory-advised handle, which MXAccess accepts even
// while the same attribute is subscribed (the advise limit is per handle, not per attribute).
}