diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipRefusalLiveTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipRefusalLiveTests.cs
new file mode 100644
index 00000000..c5aebf8d
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests/AbCipRefusalLiveTests.cs
@@ -0,0 +1,172 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
+
+///
+/// Review 07 (Gitea #737) against a running ab_server on both transports: a controller refusal is a per-tag
+/// config error, not a comms fault (H10 probe, H1/L4 data path), and a whole-array write round-trips (U10).
+/// Skipped when the fixture is unreachable (); the array write additionally
+/// needs AB_SERVER_ALLOW_WRITES=1 ().
+///
+[Trait("Category", "Integration")]
+[Trait("Requires", "AbServer")]
+public sealed class AbCipRefusalLiveTests
+{
+ private static string Device(AbServerFixture fixture) => $"ab://{fixture.Host}:{fixture.Port}/1,0";
+
+ private static async Task WaitUntilAsync(Func condition, TimeSpan timeout, string what)
+ {
+ var deadline = DateTime.UtcNow + timeout;
+ while (!condition())
+ {
+ if (DateTime.UtcNow > deadline) throw new TimeoutException(what);
+ await Task.Delay(50);
+ }
+ }
+
+ ///
+ /// H10: a typo'd probe tag on a reachable controller leaves the host Running, and a COLD data tag created
+ /// after many probe ticks still connects (the backoff window was never opened by the probe's NotFound).
+ ///
+ /// The transport stack the driver runs on.
+ [AbServerTheory]
+ [MemberData(nameof(TransportMatrix.Transports), MemberType = typeof(TransportMatrix))]
+ public async Task A_typod_probe_tag_does_not_black_out_a_reachable_device(AbCipTransport transport)
+ {
+ var device = Device(new AbServerFixture());
+ var drv = new AbCipDriver(new AbCipDriverOptions
+ {
+ Devices = [new AbCipDeviceOptions(device, AbCipPlcFamily.ControlLogix)],
+ RawTags = AbCipRawTags.From(new AbCipTagDefinition("Counter", device, "TestDINT", AbCipDataType.DInt)),
+ Probe = new AbCipProbeOptions
+ {
+ Enabled = true,
+ ProbeTagPath = "NoSuchProbeTag_xyz",
+ Interval = TimeSpan.FromMilliseconds(200),
+ Timeout = TimeSpan.FromSeconds(2),
+ },
+ Timeout = TimeSpan.FromSeconds(5),
+ Transport = transport,
+ }, $"drv-h10-{transport}");
+ await drv.InitializeAsync("{}", CancellationToken.None);
+ try
+ {
+ await WaitUntilAsync(() => drv.GetHostStatuses().Single().State == HostState.Running,
+ TimeSpan.FromSeconds(10), "the host never went Running on a refused probe tag");
+ await Task.Delay(1_000); // several probe ticks — each one used to record a Forward-Open failure
+
+ var r = await drv.ReadAsync(["Counter"], CancellationToken.None); // a cold create
+
+ r[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
+ drv.GetHostStatuses().Single().State.ShouldBe(HostState.Running);
+ }
+ finally
+ {
+ await drv.DisposeAsync();
+ }
+ }
+
+ ///
+ /// H1/L4: a tag the controller does not have reads BadNodeIdUnknown every poll while a sibling reads Good, and
+ /// the driver stays Healthy with LastSuccessfulRead advancing (it used to latch Degraded and, with no other
+ /// tag, trip the comms-loss watchdog against a reachable controller).
+ ///
+ /// The transport stack the driver runs on.
+ [AbServerTheory]
+ [MemberData(nameof(TransportMatrix.Transports), MemberType = typeof(TransportMatrix))]
+ public async Task A_missing_tag_is_per_tag_Bad_and_never_degrades_the_driver(AbCipTransport transport)
+ {
+ var device = Device(new AbServerFixture());
+ var drv = new AbCipDriver(new AbCipDriverOptions
+ {
+ Devices = [new AbCipDeviceOptions(device, AbCipPlcFamily.ControlLogix)],
+ RawTags = AbCipRawTags.From(
+ new AbCipTagDefinition("Typo", device, "NoSuchTag_xyz", AbCipDataType.DInt),
+ new AbCipTagDefinition("Counter", device, "TestDINT", AbCipDataType.DInt)),
+ Probe = new AbCipProbeOptions { Enabled = false },
+ Timeout = TimeSpan.FromSeconds(5),
+ Transport = transport,
+ }, $"drv-h1-{transport}");
+ await drv.InitializeAsync("{}", CancellationToken.None);
+ try
+ {
+ DateTime? last = null;
+ for (var i = 0; i < 3; i++)
+ {
+ var r = await drv.ReadAsync(["Typo", "Counter"], CancellationToken.None);
+ r[0].StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
+ r[1].StatusCode.ShouldBe(AbCipStatusMapper.Good);
+ var health = drv.GetHealth();
+ health.State.ShouldBe(DriverState.Healthy);
+ if (last is { } previous) health.LastSuccessfulRead!.Value.ShouldBeGreaterThanOrEqualTo(previous);
+ last = health.LastSuccessfulRead;
+ }
+
+ // Only the refused tag: still Healthy, still answering (the watchdog conjunction can never trip).
+ var before = drv.GetHealth().LastSuccessfulRead!.Value;
+ await Task.Delay(20);
+ (await drv.ReadAsync(["Typo"], CancellationToken.None))[0].StatusCode.ShouldBe(AbCipStatusMapper.BadNodeIdUnknown);
+ drv.GetHealth().State.ShouldBe(DriverState.Healthy);
+ drv.GetHealth().LastSuccessfulRead!.Value.ShouldBeGreaterThan(before);
+ }
+ finally
+ {
+ await drv.DisposeAsync();
+ }
+ }
+
+ ///
+ /// U10: a whole int[] written to the 16-element TestArray (DINT[16]) reads back element for element
+ /// through a fresh driver; a wrong-length write is refused BadOutOfRange and changes nothing.
+ ///
+ /// The transport stack the driver runs on.
+ [AbServerTheory]
+ [MemberData(nameof(TransportMatrix.Transports), MemberType = typeof(TransportMatrix))]
+ public async Task A_whole_array_write_round_trips(AbCipTransport transport)
+ {
+ IntegrationWriteGate.SkipUnlessWritesAllowed();
+ var device = Device(new AbServerFixture());
+ var seed = Random.Shared.Next(1_000, 1_000_000);
+ var values = Enumerable.Range(0, 16).Select(i => seed + i * 7).ToArray();
+
+ AbCipDriver Make(string id) => new(new AbCipDriverOptions
+ {
+ Devices = [new AbCipDeviceOptions(device, AbCipPlcFamily.ControlLogix)],
+ RawTags = AbCipRawTags.From(
+ new AbCipTagDefinition("Arr", device, "TestArray", AbCipDataType.DInt, ElementCount: 16, IsArray: true)),
+ Probe = new AbCipProbeOptions { Enabled = false },
+ Timeout = TimeSpan.FromSeconds(5),
+ Transport = transport,
+ }, $"drv-u10-{id}-{transport}");
+
+ var writer = Make("writer");
+ await writer.InitializeAsync("{}", CancellationToken.None);
+ try
+ {
+ var w = await writer.WriteAsync([new WriteRequest("Arr", values)], CancellationToken.None);
+ w[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
+ var bad = await writer.WriteAsync([new WriteRequest("Arr", values.Take(15).ToArray())], CancellationToken.None);
+ bad[0].StatusCode.ShouldBe(AbCipStatusMapper.BadOutOfRange);
+ writer.GetHealth().State.ShouldBe(DriverState.Healthy);
+ }
+ finally
+ {
+ await writer.DisposeAsync();
+ }
+
+ var reader = Make("reader");
+ await reader.InitializeAsync("{}", CancellationToken.None);
+ try
+ {
+ var r = await reader.ReadAsync(["Arr"], CancellationToken.None);
+ r[0].StatusCode.ShouldBe(AbCipStatusMapper.Good);
+ r[0].Value.ShouldBe(values);
+ }
+ finally
+ {
+ await reader.DisposeAsync();
+ }
+ }
+}