test+docs: retire the stale EquipmentTagsDarkBatch4 skips, guard the deploy-time

tag gate, and fix three silently-broken OpcUaClient integration tests

Continues working through deferment.md's remaining items.

SKIPPED TESTS (13 -> 0 for this reason). The skip said "dark until Batch 4";
Batch 4 shipped as v3.0 and RETIRED the equipment-tag path rather than lighting
it, so every one of them was waiting on something that had already happened and
gone the other way. Resolved individually rather than in bulk:
  - REVIVED onto the raw/UNS shape (6): four DriverHostActor primary-gate cases,
    the cluster-scoping rebuild, and the real-SDK dual-namespace materialisation
    E2E. The behaviour never went dark, only the v2 seeding did.
  - FOLDED into a live parity test (3): DeploymentArtifactRawUnsParityTests
    already compared RawTagPlan record-equal and RawTagPlan carries array /
    historize / alarm intent, so widening its corpus by two tags covers what
    three empty placeholder suites had been promising. Those suites are deleted.
  - DELETED (4): subjects genuinely retired -- equipment-namespace EquipmentTags,
    and both dot-joint {{equip}}.X token suites (that resolver was deleted in
    Batch 3; the slash-joint form is covered by DeploymentArtifactEquipRefParityTests).
Falsified, not assumed: removing the seeded UnsTagReference turns two revived
primary-gate tests red; re-homing the SITE-A node to MAIN turns the scoping test
red. Widening a parity corpus also needed direct value assertions -- parity alone
cannot distinguish "both seams right" from "both seams blind".
Runtime.Tests skips 13->3, OpcUaServer.Tests 4->1; all remaining are deliberate
EquipmentDeviceBindingRetired tombstones.

G-7 (deploy-time TagConfig gate). MQTT shipped an Inspect() nobody ever called;
now wired. Replaced the hand-maintained list with a reflection guard, because the
existing test enumerates driver types by hand and so guards renames but can never
notice a gap. NOTE: the first version of that guard was hollow -- it discovered
candidates via GetReferencedAssemblies(), which is circular, since the compiler
elides a reference nothing in the IL uses. Removing MQTT from the map also removed
its Contracts assembly from the manifest, and the guard passed green with the bug
reintroduced. Rewritten to scan the output directory; re-falsified and it now
fails naming MqttTagDefinitionFactory. Residual recorded at the code: Sql,
MTConnect, Calculation and OpcUaClient have no Contracts-side Inspect at all, so
the deploy gate stays weaker than the AdminUI's authoring gate for them.

OPCUACLIENT INTEGRATION TESTS (3 of 4 failing, on master too). Not fixture rot:
the simulator was healthy and Client.CLI read the very same node Good. v3 made the
driver resolve every reference through its authored RawTags table, so a bare
"ns=3;s=StepUp" fails LOCALLY at the resolver with BadNodeIdInvalid and never
reaches the wire -- the tests were exercising the unresolved-reference guard.
Fixed by seeding OpcPlcProfile.RawTags in the deploy artifact's TagConfig shape
and addressing by RawPath. 4/4 pass.

DOCS. Applied the Phase7* -> AddressSpace* rename across the four live docs
(historical bannered files deliberately untouched), resolving the three that are
not renames to their real members. Found en route that the earlier banner pass
missed three files: VirtualTags.md, OpcUaServer.md and ScriptedAlarms.md all
still presented the test-scaffolding GenericDriverNodeManager as the production
dispatch path. All three now carry the correction.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
This commit is contained in:
Joseph Doherty
2026-07-28 14:32:13 -04:00
parent 5fff597324
commit 4624141a5d
24 changed files with 556 additions and 495 deletions
@@ -3,6 +3,7 @@ using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
using ZB.MOM.WW.OtOpcUa.Driver.Mqtt;
using ZB.MOM.WW.OtOpcUa.Driver.S7;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;
@@ -13,8 +14,19 @@ namespace ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;
/// AdminUI <c>TagConfigValidator</c> shape: a case-insensitive map from the canonical driver-type string
/// to that driver's lightweight <c>Contracts</c> parser <c>Inspect()</c>, returning human-readable
/// warnings (present-but-invalid enum values that the lenient runtime silently defaults, plus
/// structurally-unparseable TagConfig). Unmapped drivers (Galaxy, OpcUaClient) are skipped exactly as in
/// the AdminUI validator. Never throws.
/// structurally-unparseable TagConfig). Never throws.
/// <para><b>Coverage is every driver that ships an <c>Inspect</c>, and that invariant is guarded</b> by
/// <c>EquipmentTagConfigInspectorCoverageTests</c>, which reflects over the referenced
/// <c>*.Contracts</c> assemblies — so a driver that gains an inspector cannot be silently left out. This
/// is the same guard shape the driver-form dispatch maps use, and for the same reason: the previous
/// hand-maintained list had drifted (MQTT shipped an <c>Inspect</c> and was never added).</para>
/// <para><b>Residual gap, deliberately recorded rather than papered over.</b> Galaxy, OpcUaClient, Sql,
/// MTConnect and Calculation have <b>no</b> <c>Inspect</c> in a Contracts assembly, so this deploy-time
/// gate cannot check them — while the AdminUI's <c>TagConfigValidator</c> DOES cover all but Galaxy via
/// its <c>*TagConfigModel.Validate()</c> models, which live in the AdminUI project and cannot be
/// referenced from here. The asymmetry runs the wrong way: config authored through the modal is checked,
/// but config arriving by CSV import or API is not. Closing it means promoting those models (or an
/// equivalent parser) into each driver's Contracts assembly. Tracked as deferment-register G-7.</para>
/// </summary>
public static class EquipmentTagConfigInspector
{
@@ -27,10 +39,12 @@ public static class EquipmentTagConfigInspector
[DriverTypeNames.AbLegacy] = AbLegacyTagDefinitionFactory.Inspect,
[DriverTypeNames.TwinCAT] = TwinCATTagDefinitionFactory.Inspect,
[DriverTypeNames.FOCAS] = FocasTagDefinitionFactory.Inspect,
[DriverTypeNames.Mqtt] = MqttTagDefinitionFactory.Inspect,
};
/// <summary>Inspects a tag's <c>TagConfig</c> for the given driver type. Returns the driver's warnings,
/// or an empty list when the driver type is unmapped (Galaxy/OpcUaClient) or the inputs are null.</summary>
/// or an empty list when the driver type has no inspector (see the class remarks) or the inputs are
/// null.</summary>
/// <param name="driverType">The bound driver's <c>DriverType</c> (canonical string, case-insensitive).</param>
/// <param name="tagConfig">The equipment tag's <c>TagConfig</c> JSON.</param>
/// <returns>The warnings; empty when the driver is unmapped or clean.</returns>
@@ -40,9 +54,12 @@ public static class EquipmentTagConfigInspector
return Inspectors.TryGetValue(driverType, out var inspect) ? inspect(tagConfig) : Array.Empty<string>();
}
/// <summary>Whether the given driver type has a mapped inspector (equipment-tag protocol drivers only).</summary>
/// <summary>Whether the given driver type has a mapped inspector.</summary>
/// <param name="driverType">The bound driver's <c>DriverType</c>.</param>
/// <returns><see langword="true"/> when an inspector is registered for the driver type.</returns>
public static bool IsMapped(string? driverType) =>
!string.IsNullOrEmpty(driverType) && Inspectors.ContainsKey(driverType);
/// <summary>The driver types this gate can check — the guard test's read of the map.</summary>
public static IReadOnlyCollection<string> MappedDriverTypes => (IReadOnlyCollection<string>)Inspectors.Keys;
}
@@ -45,6 +45,7 @@
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts\ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts.csproj"/>
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts\ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts.csproj"/>
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts\ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts.csproj"/>
<ProjectReference Include="..\..\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts\ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Contracts.csproj"/>
</ItemGroup>
</Project>