diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipReadWindowTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipReadWindowTests.cs
new file mode 100644
index 00000000..45cd3489
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/AbCipReadWindowTests.cs
@@ -0,0 +1,96 @@
+using Shouldly;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;
+
+///
+/// Review 07 P8 / P7: the bounded-parallel read window. On the native transport every concurrent tag read of a
+/// device rides ONE shared request scheduler that packs whatever is queued into a Multiple Service Packet, so a
+/// window of 8 capped MSP coalescing at 8 services per round trip; it is widened to at least 64 there (the
+/// scheduler's reply budget already prevents over-packing). And a cold create waiting on a device's connect gate
+/// must not occupy a window slot, or one dead device's backoff-expiry attempt stalls every other device's reads.
+///
+[Trait("Category", "Unit")]
+public sealed class AbCipReadWindowTests
+{
+ private static AbCipDriverOptions Options(int maxConcurrentReads, AbCipTransport? transport = null) => new()
+ {
+ Devices = [new AbCipDeviceOptions("ab://10.0.0.5/1,0")],
+ Probe = new AbCipProbeOptions { Enabled = false },
+ MaxConcurrentReads = maxConcurrentReads,
+ Transport = transport,
+ };
+
+ /// The window per transport: native ≥ 64; libplctag and injected factories honour the option exactly.
+ [Fact]
+ public void The_native_window_is_at_least_64_and_other_transports_keep_the_option()
+ {
+ new AbCipDriver(Options(8, AbCipTransport.Native), "a").ReadWindow.ShouldBe(AbCipDriver.NativeMinReadWindow);
+ AbCipDriver.NativeMinReadWindow.ShouldBeGreaterThanOrEqualTo(64);
+ new AbCipDriver(Options(1, AbCipTransport.Native), "a").ReadWindow.ShouldBe(AbCipDriver.NativeMinReadWindow);
+ new AbCipDriver(Options(200, AbCipTransport.Native), "a").ReadWindow.ShouldBe(200);
+ new AbCipDriver(Options(8), "a", defaultTransport: AbCipTransport.Native).ReadWindow.ShouldBe(AbCipDriver.NativeMinReadWindow);
+
+ // Positive controls: libplctag (per-handle concurrency) and an injected test factory keep the option.
+ new AbCipDriver(Options(8, AbCipTransport.Libplctag), "a").ReadWindow.ShouldBe(8);
+ new AbCipDriver(Options(0, AbCipTransport.Libplctag), "a").ReadWindow.ShouldBe(1);
+ new AbCipDriver(Options(3, AbCipTransport.Native), "a", new FakeAbCipTagFactory()).ReadWindow.ShouldBe(3);
+ }
+
+ /// A tag whose initialisation blocks until released (a cold connect to a slow / dead device).
+ private sealed class BlockingInitTag(AbCipTagCreateParams p, Task release) : FakeAbCipTag(p)
+ {
+ public override async Task InitializeAsync(CancellationToken cancellationToken)
+ {
+ await base.InitializeAsync(cancellationToken);
+ await release.WaitAsync(cancellationToken);
+ }
+ }
+
+ ///
+ /// With a window of ONE, device B's blocked cold create (and its queued sibling) must not stop device A's tag
+ /// from being read: cold waiters sit on B's connect gate, not in the window.
+ ///
+ [Fact]
+ public async Task A_blocked_cold_create_on_one_device_does_not_hold_the_window_against_another()
+ {
+ var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var factory = new FakeAbCipTagFactory
+ {
+ Customise = p => p.Gateway == "10.0.0.6"
+ ? new BlockingInitTag(p, release.Task) { Value = 2 }
+ : new FakeAbCipTag(p) { Value = 1 },
+ };
+ const string a = "ab://10.0.0.5/1,0", b = "ab://10.0.0.6/1,0";
+ var drv = new AbCipDriver(new AbCipDriverOptions
+ {
+ Devices = [new AbCipDeviceOptions(a), new AbCipDeviceOptions(b)],
+ RawTags = AbCipRawTags.From(
+ new AbCipTagDefinition("B1", b, "B1", AbCipDataType.DInt),
+ new AbCipTagDefinition("B2", b, "B2", AbCipDataType.DInt),
+ new AbCipTagDefinition("A1", a, "A1", AbCipDataType.DInt)),
+ Probe = new AbCipProbeOptions { Enabled = false },
+ MaxConcurrentReads = 1,
+ }, "abcip-window", factory);
+ await drv.InitializeAsync("{}", CancellationToken.None);
+
+ var read = drv.ReadAsync(["B1", "B2", "A1"], CancellationToken.None);
+ var deadline = DateTime.UtcNow.AddSeconds(5);
+ while (!(factory.Tags.TryGetValue("A1", out var tag) && tag.ReadCount > 0))
+ {
+ if (DateTime.UtcNow > deadline)
+ {
+ release.TrySetResult();
+ await read;
+ throw new ShouldAssertException("device A's read waited behind device B's blocked cold create");
+ }
+ await Task.Delay(5);
+ }
+ read.IsCompleted.ShouldBeFalse(); // B is still blocked — A was read regardless
+
+ release.SetResult();
+ var r = await read;
+ r.Select(x => x.Value).ShouldBe([2, 2, 1]); // positive control: B's reads complete once it connects
+ await drv.DisposeAsync();
+ }
+}