ce589569fe732ee46095e12701c35310806046b2
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4624141a5d |
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
|
||
|
|
db751d12a5 |
feat(alarms): condition Quality tracks source connectivity (#477)
Part 9 ConditionType.Quality was never assigned; default(StatusCode)==Good so every native + scripted condition reported Good unconditionally — a comms-lost device still showed a healthy, inactive, Good condition (a wrong-VALUE bug, distinct from the null-value #473/#475). Clients (and HMIs bucketing on IsGood) could not tell "genuinely inactive" from "lost contact". Layer 1 — make Quality a real, plumbed field: - AlarmConditionSnapshot gains OpcUaQuality Quality (default Good). - MaterialiseAlarmCondition sets it (native BadWaitingForInitialData, scripted Good). - WriteAlarmCondition projects snapshot.Quality; the delta-gate gains a Quality member so a quality-bucket change fires a Part 9 event. Layer 2 — drive native quality from driver connectivity (a comms-lost driver emits no alarm transitions, and an alarm-bearing raw tag has no value variable, so quality can't come from either existing channel): - DriverInstanceActor Tells parent ConnectivityChanged on Connected/Reconnecting. - DriverHostActor fans it to every native condition the driver owns as OpcUaPublishActor.AlarmQualityUpdate (Good on connect, Bad on disconnect). - New dedicated IOpcUaAddressSpaceSink.WriteAlarmQuality sets ONLY Quality and fires only on a bucket change — never touches Active/Acked/Retain (an active alarm that loses comms stays active). Not a full-snapshot re-projection, so it can't clobber severity/message and works for a never-fired condition. Forwarded through DeferredAddressSpaceSink (F10b trap; auto-verified by the reflection forwarding guard). Ungated by redundancy role; no /alerts row. Scripted conditions stay Good; worst-of-input quality deferred to #478 (Layer 3). Tests: node-level (materialise/project/no-clobber/unknown-node no-op), NativeAlarmProjector, DriverInstanceActor connectivity emission, DriverHostActor fan-out, OpcUaPublishActor routing, and the wire-level guard (Condition_event_Quality_tracks_source_connectivity_on_the_wire) — RED-verified against a simulated pre-fix always-Good server. Existing DriverInstanceActor parent probes ignore the new ConnectivityChanged. Docs: docs/AlarmTracking.md §"Condition source-data Quality (#477)"; design doc docs/plans/2026-07-17-alarm-condition-quality-477-design.md. |
||
|
|
8ebc712eff |
feat(v3-batch4-wp4): multi-notifier native alarms (single ReportEvent → raw + equipment notifiers) + teardown symmetry
Materialize each native alarm ONCE at the raw tag (ConditionId = RawPath, Raw realm); wire the single condition as an SDK event notifier of each referencing equipment's UNS folder so one ReportEvent fans to every root without re-reporting per root (which would break Server-object dedup + Part 9 ack correlation). - New sink method WireAlarmNotifiers(alarmNodeId, alarmRealm, notifierFolderNodeIds, notifierFolderRealm) on IOpcUaAddressSpaceSink, forwarded through DeferredAddressSpaceSink + SdkAddressSpaceSink + NullOpcUaAddressSpaceSink (the forwarding-trap guard); auto-covered by the DeferredSinkForwardingReflectionTests realm + forwarding guards + a hand-written forward test. - OtOpcUaNodeManager: the normative AddNotifier(isInverse) pair + idempotent EnsureFolderIsEventNotifier per equipment folder; tracked per condition in _alarmNotifierWiring. Teardown symmetry: RemoveNotifier(bidirectional:true) on RebuildAddressSpace, RemoveAlarmConditionNode, and RemoveEquipmentSubtree so no inverse-notifier entry leaks across redeploys. - AddressSpaceApplier.MaterialiseRawSubtree wires notifiers for each native alarm tag, resolving its ReferencingEquipmentPaths (Area/Line/Equipment) to the EquipmentId folder NodeIds via BuildEquipmentIdByFolderPath. - AlarmTransitionEvent gains ReferencingEquipmentPaths (empty default); /alerts renders the referencing-equipment list as display metadata. - Un-skipped + rewrote the native-alarm dark tests (DriverHostActorNativeAlarmTests x6, DriverHostActorNativeAlarmAckRoutingTests x1) for the v3 raw-condition model; new NodeManagerMultiNotifierAlarmTests proves multi-notifier wiring + teardown symmetry (no leaked duplicates after a re-trip) + applier wiring test. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox |
||
|
|
3efcf8014b |
feat(v3-batch4-wp3): raw-only binding + UNS fan-out + write routing
Wave B of Batch 4 — the runtime binding seam for the dual namespace. Applier (both realms, explicit realm at every sink call site): - MaterialiseRawSubtree: Raw containers as folders + Raw tags as variables keyed by RawPath (native-alarm tag → single Part 9 condition at the RawPath), all in AddressSpaceRealm.Raw; historian tagname = override else RawPath. - MaterialiseUnsReferences: each UNS reference Variable under its equipment folder (Uns realm) + an Organizes UNS->Raw edge; inherits writable/array/ historian tagname from the backing raw tag (both NodeIds -> one tagname). - FeedHistorizedRefs / ProvisionHistorizedTags now source RAW tags (mux ref stays single, keyed by RawPath); ApplyPureRemove tears down raw tags + UNS refs in place (raw-container removal falls back to rebuild). DriverHostActor (dual-NodeId, single-source fan-out): - _nodeIdByDriverRef value gains a realm (NodeRealmRef); rebuilt from RawTags UNION UnsReferenceVariables so one (DriverInstanceId, RawPath) fans to the raw NodeId AND every referencing UNS NodeId with identical value/quality/timestamp. Write inverse map keyed by the bare id; the ns-qualified NodeId the write hook passes is normalised (BareNodeId) so a write to either NodeId resolves the same driver ref (-> RawPath write). - Native raw alarm condition routing is realm-tagged (Raw); AttributeValueUpdate + AlarmStateUpdate carry the realm through to the sink. Retire EquipmentNodeIds -> V3NodeIds.Uns (applier/VirtualTagHostActor) and RawPaths.Combine (DiscoveredNodeMapper, discovered nodes are Raw now). DeploymentArtifact.ParseComposition emits the Raw + UNS subtrees byte-parity with the composer (reconstruct entities -> AddressSpaceComposer.Compose). Sink surface: removed the transitional `= AddressSpaceRealm.Uns` defaults from IOpcUaAddressSpaceSink / ISurgicalAddressSpaceSink / SdkAddressSpaceSink / DeferredAddressSpaceSink / NullOpcUaAddressSpaceSink — every call site is now explicit (realm reordered before the trailing optionals on EnsureVariable + MaterialiseAlarmCondition). Node-manager convenience methods keep their defaults (they are not the interface impl; Sdk delegates explicitly). Tests: rewrote DriverHostActorLiveValueTests (fan-out drift, 1:N) + DriverHostActorWriteRoutingTests (dual-NodeId raw/uns write routing) to the v3 raw+uns model; new AddressSpaceApplierRawUnsTests; migrated the EquipmentTags provisioning/feed tests to RawTags; swept EquipmentNodeIds test callers. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox |
||
|
|
e95615cef3 |
fix(v3-batch4-wp2): AddReference sink seam for Organizes UNS→Raw (Wave A review M1)
Close the Wave-A M1 gap: the sink surface had no way to wire the mandated cross-tree Organizes reference from each UNS reference Variable to its backing Raw node, forcing WP3 to reopen the frozen surface. Add a dedicated realm-qualified AddReference method instead. - IOpcUaAddressSpaceSink.AddReference(sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType="Organizes"): realm-qualified both ends; idempotent; a missing endpoint is a logged no-op (never throws) so a mid-rebuild race can't fault a deploy. Base-interface capability (no surgical sniff, no transitional default — WP3 wires it explicitly per UNS reference variable). - SdkAddressSpaceSink forwards to the node manager; DeferredAddressSpaceSink forwards unconditionally (like EnsureFolder); NullOpcUaAddressSpaceSink no-ops. - OtOpcUaNodeManager.AddReference: resolve both nodes by full ns-qualified key (variable/folder/condition via ResolveNodeState), guard both-exist, then wire the edge bidirectionally (forward Organizes on source, inverse on target) with a ReferenceExists idempotency guard + ClearChangeMasks on mutated sides. Reference type resolved by ResolveReferenceType (Organizes default). Added internal GetNodeReferences test/diagnostic accessor. - Reflection guard: the exhaustive-forwarding test + the realm-discriminator guard auto-cover AddReference (it has two AddressSpaceRealm params) — no edit needed. - New SdkAddressSpaceSinkTests: Organizes UNS→Raw edge created bidirectionally + idempotent; missing endpoint is a safe no-op. - All 15 IOpcUaAddressSpaceSink test doubles gain the AddReference no-op so the solution still builds. Build: dotnet build ZB.MOM.WW.OtOpcUa.slnx = 0 errors. Tests: Commons.Tests 310/310; OpcUaServer.Tests 337 passed / 4 pre-existing skips. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox |
||
|
|
5534698d70 |
feat(v3-batch4-wp2): node manager dual-namespace + realm-aware sink surface
Register both v3 namespaces (Raw first, UNS second) on OtOpcUaNodeManager and thread an AddressSpaceRealm discriminator through every node-naming sink method so a bare node id is resolved to the correct namespace by realm — never parsed out of the id string. - IOpcUaAddressSpaceSink / ISurgicalAddressSpaceSink: every node-naming method gains `AddressSpaceRealm realm = AddressSpaceRealm.Uns` (transitional default so un-migrated WP3 call sites still compile; WP3/Wave B makes them explicit and removes the default). Null + SdkAddressSpaceSink impls updated; DeferredAddress- SpaceSink forwards realm through every method (the forwarding trap). - DeferredSinkForwardingReflectionTests: existing exhaustive-forwarding guard auto-covers the new signatures; added an explicit guard that every node-naming sink method carries an AddressSpaceRealm parameter (RebuildAddressSpace exempt). - OtOpcUaNodeManager: register RawNamespaceUri + UnsNamespaceUri; realm->namespace index via NamespaceIndexForRealm; all node maps (_variables/_folders/ _alarmConditions/_nativeAlarmNodeIds/_historizedTagnames/_eventNotifierSources) re-keyed by the full ns-qualified NodeId string so Raw and UNS nodes sharing a bare id stay distinct; _notifierFolders already NodeId-keyed. A historized UNS reference node registers the SAME historian tagname as its backing raw node (both NodeIds -> one tagname). Inbound-write hook routes the full ns-qualified NodeId to the write gateway (realm-aware) and reverts by bare id + realm. HistoryRead seams resolve via NodeId directly. DefaultNamespaceUri kept as a transitional alias to UnsNamespaceUri for the SubscriptionSurvivalTests. - Test doubles across Commons.Tests / OpcUaServer.Tests / Runtime.Tests updated to the new interface signatures; SdkAddressSpaceSinkTests asserts the UNS namespace index for default-realm nodes. Build: dotnet build ZB.MOM.WW.OtOpcUa.slnx = 0 errors. Tests: Commons.Tests 310/310; OpcUaServer.Tests 335 passed / 4 pre-existing skips. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox |
||
|
|
2c7fc65bae |
test(v3): migrate OpcUaServer.Tests + Host.IntegrationTests to v3 dark address space
Migrate the server composer/applier + 2-node harness tests off the retired
Namespace/NamespaceKind + equipment→driver/device binding schema onto the v3
greenfield shape (raw-only Tag; DriverInstance-RawFolderId; equipment via UnsLine).
OpcUaServer.Tests (134 build errors -> green, 335 pass / 4 skip):
- LIVE (migrated, passing): VirtualTag-historize + ScriptedAlarm composition,
composer purity/hierarchy, applier MaterialiseHierarchy + MaterialiseEquipmentTags
idempotency (hand-fed applier, not dark).
- SKIP dark-until-Batch-4: {{equip}} token expansion, applier equipment-namespace
E2E (equipment-tag variable materialization).
- RETIRE-with-comment / skip: alias-tag (Tag.EquipmentId binding gone) + device-host
equipment binding (architecturally removed).
- Add local DarkAddressSpaceReasons Skip-reason constants (mirrors Runtime.Tests).
Host.IntegrationTests (green, in-memory): drop deleted-Namespace seeds from
MultiClusterScoping + DriverReconnect; reshape EquipmentNamespaceMaterialization to
assert the DARK contract (folder nodes decode, EquipmentTags empty despite a seeded
v3 raw tag) with the full raw-tag->EquipmentTag materialization kept as a
Batch-4-pending Skip.
Tests-only; no src/ changes.
|
||
|
|
598cdfad5a | feat(otopcua): applier pass to materialise discovered nodes idempotently | ||
|
|
23b42b424d |
fix(code-review): resolve OpcUaServer-001 — UNS Area/Line rename refreshes folder DisplayName
A rename-only deploy produced an IsEmpty plan that short-circuited before MaterialiseHierarchy, leaving the OPC UA folder DisplayName stale. AddressSpacePlanner now diffs UnsAreas/UnsLines by stable id into a RenamedFolders set (counted in IsEmpty); the applier refreshes the folder in place via a new UpdateFolderDisplayName on ISurgicalAddressSpaceSink (forwarded through DeferredAddressSpaceSink so it is NOT inert on driver hosts; falls back to rebuild when the sink is non-surgical). DeploymentArtifact byte-parity untouched (rename rides the existing Name round-trip). No EF migration, no serialized wire/proto contract change. +13 OpcUaServer tests, Runtime rebuild test. |
||
|
|
40e8a23e7c |
refactor(opcuaserver): rename Phase7* address-space pipeline to AddressSpace*
v2-ci / build (push) Failing after 37s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
The OPC UA address-space build pipeline was named after a v2-roadmap milestone number rather than its domain. Rename the family to describe what it does (build/diff/apply the OPC UA address space): Phase7Composer -> AddressSpaceComposer Phase7CompositionResult -> AddressSpaceComposition Phase7Planner -> AddressSpacePlanner Phase7Plan -> AddressSpacePlan Phase7Applier -> AddressSpaceApplier Phase7ApplyOutcome -> AddressSpaceApplyOutcome The 9 Phase7*Tests suites follow suit; Phase7ScriptingEntitiesTests -> ScriptingEntitiesTests (it tests the scripting migration, not the pipeline). Log-message prefixes move to the new class names. Pure mechanical rename, no behavioral change. EF migration classes/IDs (AddPhase7ScriptingTables, ExtendComputeGenerationDiffWithPhase7) are immutable and left untouched, as are historical design docs. Build clean; OpcUaServer 261/261, Runtime 272/272, ScriptingEntities 12/12 green. |