diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDeviceRefusalTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDeviceRefusalTests.cs
index 2cb690f5..6803d3ab 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDeviceRefusalTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipDeviceRefusalTests.cs
@@ -2,6 +2,7 @@ using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport;
+using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Tests.Fakes;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
@@ -178,4 +179,50 @@ public sealed class AbCipDeviceRefusalTests
factory.Tags["ReadOnlyTag"].Disposed.ShouldBeFalse();
drv.GetHealth().State.ShouldBe(DriverState.Healthy);
}
+
+ ///
+ /// L4 end to end over the REAL native runtime + a private pool: a renamed tag (CIP 0x04) that is the device's
+ /// ONLY lease holder used to be evicted every poll — the last release, so Forward Close, then a throttled Forward
+ /// Open on the next poll. One runtime and one Forward Open across N polls and no Forward Close proves no churn.
+ ///
+ [Fact]
+ public async Task A_native_NotFound_tag_causes_no_runtime_or_connection_churn()
+ {
+ await using var target = new FakeCipTarget();
+ target.ConnectedHandler = req => Task.FromResult(
+ req[0] == 0x52 ? [0xD2, 0x00, 0x04, 0x00] : target.ServiceReply(req));
+ var host = $"ab://127.0.0.1:{target.Port}/1,0";
+ var creates = 0;
+ var native = new NativeAbCipTagFactory(new DeviceSessionPool());
+ var factory = new CountingFactory(p => { Interlocked.Increment(ref creates); return native.Create(p); });
+ var drv = new AbCipDriver(new AbCipDriverOptions
+ {
+ Devices = [new AbCipDeviceOptions(host, AbCipPlcFamily.ControlLogix)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Renamed", host, "Renamed", AbCipDataType.DInt)),
+ Probe = new AbCipProbeOptions { Enabled = false },
+ Timeout = TimeSpan.FromSeconds(5),
+ }, "abcip-l4-native", factory);
+ await drv.InitializeAsync("{}", CancellationToken.None);
+ try
+ {
+ for (var n = 0; n < 5; n++)
+ (await drv.ReadAsync(["Renamed"], CancellationToken.None))[0].StatusCode
+ .ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
+
+ Volatile.Read(ref creates).ShouldBe(1);
+ target.ForwardOpens.ShouldBe(1);
+ target.ForwardCloses.ShouldBe(0);
+ target.ConnectedRequests.ShouldBeGreaterThanOrEqualTo(5); // positive control: every poll hit the wire
+ drv.GetHealth().State.ShouldBe(DriverState.Healthy);
+ }
+ finally
+ {
+ await drv.DisposeAsync();
+ }
+ }
+
+ private sealed class CountingFactory(Func create) : IAbCipTagFactory
+ {
+ public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) => create(createParams);
+ }
}