Adds deferment.md — a source-verified inventory of new-driver status, all 17
open issues, in-code deferrals, open live gates and plan bookkeeping. Every
claim was checked against src/ and git, not against documentation.
Headline finding: three subsystems are authored, persisted and shipped but
never executed —
* Node ACLs: IPermissionEvaluator/TriePermissionEvaluator have zero
production consumers and OtOpcUaNodeManager never references them, yet
ClusterAcls.razor authors NodeAcl rows and ConfigComposer.cs:51 ships them
in every artifact. An authored deny rule has no effect.
* IRediscoverable / IHostConnectivityProbe raise into the void (#518/#507);
nothing ever writes a DriverHostStatus row.
* DriverTypeRegistry is vestigial, so no factory passes a tier and every
driver runs Tier A with the Tier-C protections dormant.
Also records the Calculation driver as picker-visible but unauthorable
(DriverConfigModal has no case) — the "registered but unauthorable" class
recurring after the Sql picker defect — and notes that no parity test guards
DriverConfigModal/DeviceModal, which is why it survived review.
Documentation corrections (source-verified):
* ReadWriteOperations.md claimed "a denied read never hits the driver" via
four types that do not exist in src/. Bannered + struck through; a security
review reading that page would have concluded a per-node ACL gate exists.
* CLAUDE.md: the Change Detection sentence was false (DriverHost consumes
nothing); the mesh Phase 4 and Phase 5 live gates and the auto-down 1-vs-1
gate had all PASSED; the ScriptedAlarmState table was already dropped.
* driver-expansion tracking: Modbus RTU and SQL poll are merged, not
pending — its command table would have made someone rebuild two shipped
drivers in a fresh worktree.
* drivers/README.md: dead DriverTypeRegistry paragraph, retired
SystemPlatform namespace kind, missing Sql + Calculation rows, missing
Modbus RTU-over-TCP transport.
* TwinCAT.md/Galaxy.md promised an address-space rebuild that never happens.
* Historian.md gained the #491 unproven-value-capture pointer.
* IncrementalSync.md and AddressSpace.md bannered as wholesale v2-era (the
rewrite is recorded as still owed, not done here); four v2 status docs
bannered as historical; three wrong-name-for-a-live-type fixes.
Left deliberately untouched: the secrets NoOpSecretReplicator line, which
makes a security claim and needs the real behaviour identified rather than a
rename.
9.1 KiB
OPC UA Client test fixture
Coverage map + gap inventory for the OPC UA Client (gateway / aggregation) driver.
TL;DR: Wire-level coverage now exists via
opc-plc — Microsoft
Industrial IoT's OPC UA PLC simulator running in Docker (task #215). Real
Secure Channel, real Session, real MonitoredItem exchange against an
independent server implementation. Unit tests still carry the exhaustive
capability matrix (cert auth / security policies / reconnect / failover /
attribute mapping). Gaps remaining: upstream-server-specific quirks
(historian aggregates, typed ConditionType events, SDK-publish-queue edge
behavior under load) — opc-plc uses the same OPCFoundation stack internally
so fully-independent-stack coverage needs open62541/open62541 as a second
image (follow-up).
What the fixture is
Integration layer (task #215):
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/ stands up
mcr.microsoft.com/iotedge/opc-plc:2.14.10 via Docker/docker-compose.yml
on opc.tcp://10.100.0.35:50000 (the shared Docker host; override via
OPCUA_SIM_ENDPOINT). OpcPlcFixture probes the port at
collection init + skips tests with a clear message when the container's
not running (matches the Modbus/pymodbus + S7/python-snap7 skip pattern).
Docker is the launcher — no PowerShell wrapper needed because opc-plc
ships pre-containerized. Compose-file flags: --ut (unsecured transport
advertised), --aa (auto-accept client certs — opc-plc's cert trust store
resets on each spin-up), --alm (alarm simulation for IAlarmSource
follow-up coverage), --pn=50000 (port).
Unit layer:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ is still the primary
coverage. Tests inject fakes through the driver's construction path; the
OPCFoundation.NetStandard Session surface is wrapped behind an interface
the tests mock.
What it actually covers
Integration (opc-plc Docker, task #215)
OpcUaClientSmokeTests.Client_connects_and_reads_StepUp_node_through_real_OPC_UA_stack— full Secure Channel + Session +ns=3;s=StepUpRead round-tripOpcUaClientSmokeTests.Client_reads_batch_of_varied_types_from_live_simulator— batch Read of UInt32 / Int32 / Boolean; assertsbool-specific Variant decoding to catch a common attribute-mapping regressionOpcUaClientSmokeTests.Client_subscribe_receives_StepUp_data_changes_from_live_server— realMonitoredItemsubscription againstns=3;s=FastUInt1(ticks every 100 ms); assertsOnDataChangefires within 3 s of subscribe
Wire-level surfaces verified: IDriver + IReadable + ISubscribable +
IHostConnectivityProbe (via the Secure Channel exchange).
Unit
The surface is broad because OpcUaClientDriver is the richest-capability
driver in the fleet (it's a gateway for another OPC UA server, so it
mirrors the full capability matrix):
OpcUaClientDriverScaffoldTests—IDriverlifecycleOpcUaClientReadWriteTests— read + write lifecycleOpcUaClientSubscribeAndProbeTests— monitored-item subscription + probe state transitionsOpcUaClientDiscoveryTests—GetEndpoints+ endpoint selectionOpcUaClientAttributeMappingTests— OPC UA node attribute → driver value mappingOpcUaClientSecurityPolicyTests—SignAndEncrypt/Sign/Nonepolicy negotiation contractOpcUaClientCertAuthTests— cert store paths, revocation-list configOpcUaClientReconnectTests— SDK reconnect hook +TransferSubscriptionsacross the disconnect boundaryOpcUaClientFailoverTests— primary → secondary session fallback per driver configOpcUaClientAlarmTests— A&E severity bucket (1–1000 → Low / Medium / High / Critical), subscribe / unsubscribe / ack contractOpcUaClientHistoryTests— historical data read + interpolation contract
Capability surfaces whose contract is verified: IDriver, ITagDiscovery,
IReadable, IWritable, ISubscribable, IHostConnectivityProbe,
IAlarmSource, IHistoryProvider.
What it does NOT cover
1. Full real-stack exchange (unit tests only)
The unit suite mocks Session.ReadAsync, Session.CreateSubscription,
Session.AddItem, etc. — no UA Secure Channel is opened. The integration
suite (OpcUaClientSmokeTests, task #215) does open a real Secure Channel
against opc-plc and exercises Read + Subscribe end-to-end. What remains
untested even in the integration suite: certificate validation under
non-anonymous security policies, signing/encryption, nonce handling, chunk
assembly, keep-alive cadence — all SDK-internal.
2. Subscription transfer across reconnect
Contract test: "after a simulated reconnect, TransferSubscriptions is
called with the right handles." Real behavior: SDK re-publishes against the
new channel and some events can be lost depending on publish-queue state.
The lossy window is not characterized.
3. Large-scale subscription stress
100+ monitored items with heterogeneous publish intervals under a single session — the shape that breaks publish-queue-size tuning in the wild — is not exercised.
4. Real historian mappings
IHistoryProvider.ReadRawAsync + ReadProcessedAsync +
ReadAtTimeAsync + ReadEventsAsync are contract-mocked. Against a real
historian (AVEVA Historian, Prosys historian, Kepware LocalHistorian) each
has specific interpolation + bad-quality-handling quirks the contract test
doesn't see.
5. Real A&E events
Alarm subscription is mocked via filtered monitored items; the actual
EventFilter select-clause behavior against a server that exposes typed
ConditionType events (non-base BaseEventType) is not verified.
6. Authentication variants
- Anonymous, UserName/Password, X509 cert tokens — each is contract-tested but not exchanged against a server that actually enforces each.
- LDAP-backed
UserName(matching this repo's server-sideLdapOpcUaUserAuthenticator) requires a live LDAP round-trip; not tested.
When to trust OpcUaClient tests, when to reach for a server
| Question | Unit tests | Integration (opc-plc) | Real upstream server |
|---|---|---|---|
| "Does severity 750 bucket as High?" | yes | - | yes |
"Does the driver call TransferSubscriptions after reconnect?" |
yes | - | yes |
| "Does a real OPC UA read round-trip work?" | no | yes | yes |
| "Does a real OPC UA subscribe deliver changes?" | no | yes | yes |
| "Does write round-trip work against a live server?" | no | no (not yet exercised) | yes (required) |
| "Does event-filter-based alarm subscription return ConditionType events?" | no | no | yes (required) |
| "Does history read from AVEVA Historian return correct aggregates?" | no | no | yes (required) |
| "Does the SDK's publish queue lose notifications under load?" | no | no | yes (stress) |
Follow-up candidates
The easiest win here is to wire the client driver tests against this
repo's own server. The v2 integration test project
tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs
(the v2 replacement for the retired v1 OpcUaServerIntegrationTests) already
stands up a real OPC UA server on a non-default port with a seeded
FakeDriver. An OpcUaClientLiveLoopbackTests that connects the client
driver to that server would give:
- Real Secure Channel negotiation
- Real Session / Subscription / MonitoredItem exchange
- Real read/write round-trip
- Real certificate validation (the integration test already sets up PKI)
It wouldn't cover upstream-server-specific quirks (AVEVA Historian, Kepware, Prosys), but it would cover 80% of the SDK surface the driver sits on top of.
Beyond that:
- Prosys OPC UA Simulation Server — free, Windows-available, scriptable.
- UaExpert Server-Side Simulator — Unified Automation's sample server; good coverage of typed ConditionType events.
- Dedicated historian integration lab — only path for historian-specific coverage.
Key fixture / config files
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/— unit tests with mockedSessiontests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcFixture.cs— collection fixture; parsesOPCUA_SIM_ENDPOINT(defaultopc.tcp://10.100.0.35:50000), TCP-probes at collection init, recordsSkipReasonwhen unreachabletests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs— wire-level test suite (3[Fact]methods: read, batch read, subscribe)tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml—mcr.microsoft.com/iotedge/opc-plc:2.14.10with--ut --aa --alm --pn=50000src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs— ctor + session-factory seam tests mock through; implementsIAlarmSource+IHistoryProvider(unique among drivers); does NOT implementIRediscoverabletests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs— the v2 dual-endpoint integration harness a future loopback client test could piggyback on (v1OpcUaServerIntegrationTests.csretired with the v1 server project)