diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs
index 925a4fe3..4db8f1ab 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.IntegrationTests/MTConnectAgentIntegrationTests.cs
@@ -165,6 +165,59 @@ public sealed class MTConnectAgentIntegrationTests(MTConnectAgentFixture agent)
result.Latency.ShouldBeNull();
}
+ ///
+ /// Review 2026-10-21 T-7 — the connectivity probe's headers-only ping (no device-model download/parse) answers
+ /// against the live Agent, and fails against a closed port (the falsifiability control).
+ ///
+ [Fact(Timeout = TestTimeoutMs)]
+ public async Task Ping_reaches_the_live_agent_and_not_a_dead_port()
+ {
+ SkipIfDown();
+
+ using (var client = new MTConnectAgentClient(LiveOptions()))
+ {
+ await client.PingAsync(Ct);
+ }
+
+ using var dead = new MTConnectAgentClient(
+ new MTConnectDriverOptions { AgentUri = $"http://127.0.0.1:{ClosedLoopbackPort()}", RequestTimeoutMs = 5_000 });
+ await Should.ThrowAsync(async () => await dead.PingAsync(Ct));
+ }
+
+ ///
+ /// Review 2026-10-21 T-4 / T-5 against the live Agent: once the /sample stream is delivering, a read is
+ /// answered (from the pump's live index) with Good values, and the stream's lag measurement — the real Agent
+ /// sends lastSequence — is on the diagnostics bag beside the effective count.
+ ///
+ [Fact(Timeout = TestTimeoutMs)]
+ public async Task A_delivering_stream_serves_reads_and_reports_its_lag()
+ {
+ SkipIfDown();
+
+ var ids = await NumericSampleIdsAsync();
+ if (ids.Count == 0)
+ {
+ Assert.Skip($"The Agent at {agent.AgentUri} is currently reporting no numeric SAMPLE value.");
+ }
+
+ await using var live = await LiveDriverAsync(ids.Select(id => Tag(id, DriverDataType.Float64)));
+ await live.Driver.SubscribeAsync([.. ids], TimeSpan.FromMilliseconds(200), Ct);
+
+ var deadline = DateTime.UtcNow + StreamWait;
+ while (!live.Driver.GetDiagnostics().ContainsKey("sampleLag") && DateTime.UtcNow < deadline)
+ {
+ await Task.Delay(100, Ct);
+ }
+
+ var diag = live.Driver.GetDiagnostics();
+ diag.ShouldContainKey("sampleLag", "the live stream must have delivered a measured chunk");
+ diag["sampleCount"].ShouldBe("500");
+ long.Parse(diag["sampleLag"], System.Globalization.CultureInfo.InvariantCulture).ShouldBeGreaterThanOrEqualTo(0);
+
+ var results = await live.Driver.ReadAsync([.. ids], Ct);
+ results.Where(r => r.StatusCode == Good).ShouldNotBeEmpty();
+ }
+
// ---- discovery: the /probe device model ----
///
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcUaClientBrowseSessionTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcUaClientBrowseSessionTests.cs
index d84794b6..cabce27e 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcUaClientBrowseSessionTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/OpcUaClientBrowseSessionTests.cs
@@ -63,10 +63,10 @@ public sealed class OpcUaClientBrowseSessionTests
children.ShouldNotBeNull();
}
- /// The OPC UA picker treats variables as terminal leaves, so
- /// AttributesAsync must always be empty for this driver.
+ /// Review 2026-10-21 O-5: an Object (folder) has no value attributes, so AttributesAsync is empty for
+ /// it. (It used to be empty for EVERYTHING — the side-panel was a stub; see the variable test below.)
[Fact]
- public async Task AttributesAsync_is_empty_for_opcuaclient()
+ public async Task AttributesAsync_is_empty_for_an_object_node()
{
OpcPlcProbe.SkipUnlessReachable();
var ct = TestContext.Current.CancellationToken;
@@ -75,4 +75,25 @@ public sealed class OpcUaClientBrowseSessionTests
var attrs = await session.AttributesAsync("nsu=http://opcfoundation.org/UA/;i=85", ct);
attrs.ShouldBeEmpty();
}
+
+ ///
+ /// Review 2026-10-21 O-5 against the live server: a standard variable (Server/ServerStatus/CurrentTime — a
+ /// read-only scalar UtcTime every server exposes) reports its mapped type, shape and access.
+ ///
+ [Fact]
+ public async Task AttributesAsync_reports_a_live_variables_type_shape_and_access()
+ {
+ OpcPlcProbe.SkipUnlessReachable();
+ var ct = TestContext.Current.CancellationToken;
+ var browser = new OpcUaClientDriverBrowser();
+ await using var session = await browser.OpenAsync(ConfigJson, ct);
+
+ var attrs = await session.AttributesAsync("nsu=http://opcfoundation.org/UA/;i=2258", ct);
+
+ var a = attrs.ShouldHaveSingleItem();
+ a.Name.ShouldBe("CurrentTime");
+ a.DriverDataType.ShouldBe("DateTime");
+ a.IsArray.ShouldBeFalse();
+ a.SecurityClass.ShouldBe("ViewOnly");
+ }
}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs
index 34276308..b1d3cc1b 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs
@@ -92,4 +92,61 @@ public sealed class OpcUaClientSmokeTests(OpcPlcFixture sim)
await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
}
+
+ ///
+ /// Review 2026-10-21 O-9 + lifecycle generation, through the real SDK: a shutdown with several live
+ /// subscriptions completes inside the one teardown budget, lands Unknown, and a re-initialise of the same
+ /// instance reconnects and reads.
+ ///
+ [Fact]
+ public async Task Shutdown_with_live_subscriptions_is_bounded_and_the_instance_reinitialises()
+ {
+ if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
+ var ct = TestContext.Current.CancellationToken;
+
+ var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
+ await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-lifecycle");
+ await drv.InitializeAsync("{}", ct);
+ for (var i = 0; i < 3; i++)
+ await drv.SubscribeAsync([OpcPlcProfile.FastUInt1Ref], TimeSpan.FromMilliseconds(250), ct);
+
+ var sw = System.Diagnostics.Stopwatch.StartNew();
+ await drv.ShutdownAsync(ct);
+ sw.Stop();
+
+ sw.Elapsed.ShouldBeLessThan(TimeSpan.FromSeconds(10), "one 4 s budget bounds the whole teardown");
+ drv.GetHealth().State.ShouldBe(DriverState.Unknown);
+
+ await drv.InitializeAsync("{}", ct);
+ var read = await drv.ReadAsync([OpcPlcProfile.StepUpRef], ct);
+ read.Single().StatusCode.ShouldBe(0u, "the re-initialised instance must be fully usable");
+ drv.GetHealth().State.ShouldBe(DriverState.Healthy);
+ }
+
+ ///
+ /// The health contract against a real server: a write the server REFUSES (a string into the UInt32 StepUp —
+ /// BadTypeMismatch) is a reply — the refusal status passes through and the driver stays Healthy with its stamp
+ /// advanced.
+ ///
+ [Fact]
+ public async Task A_server_refused_write_is_a_reply_and_keeps_the_driver_healthy()
+ {
+ if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
+ var ct = TestContext.Current.CancellationToken;
+
+ var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
+ await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-refused-write");
+ await drv.InitializeAsync("{}", ct);
+ var before = drv.GetHealth().LastSuccessfulRead.ShouldNotBeNull();
+ var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(5);
+ while (DateTime.UtcNow <= before && DateTime.UtcNow < deadline) await Task.Yield();
+
+ var result = await drv.WriteAsync([new WriteRequest(OpcPlcProfile.StepUpRef, "not-a-number")], ct);
+
+ (result.Single().StatusCode & 0x80000000u).ShouldNotBe(0u, "opc-plc refuses a string written into a UInt32");
+ result.Single().StatusCode.ShouldNotBe(0x80050000u, "a server refusal is not a local comms error");
+ var h = drv.GetHealth();
+ h.State.ShouldBe(DriverState.Healthy);
+ h.LastSuccessfulRead.ShouldNotBeNull().ShouldBeGreaterThan(before);
+ }
}