diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs index d4e0e39f..c1dbd5b2 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs @@ -48,6 +48,10 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical /// public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _inner.RaiseNodesAddedModelChange(affectedNodeId, realm); + /// + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") + => _inner.AddReference(sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType); + /// // Forwards to the inner sink when it supports the surgical capability; returns false otherwise — // before the real SdkAddressSpaceSink is swapped in (inner is still the null sink), or any inner diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs index cfc6800c..64b97dab 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaAddressSpaceSink.cs @@ -111,6 +111,26 @@ public interface IOpcUaAddressSpaceSink /// The namespace realm lives in. // transitional default — B4-WP3/Wave B makes every call site explicit and removes this default. void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns); + + /// + /// Add an OPC UA reference from an already-materialised source node to an already-materialised + /// target node, both realm-qualified. Used by AddressSpaceApplier to link each UNS reference + /// Variable (source, ) to its backing Raw node (target, + /// ) with an Organizes edge, so the UNS variable + /// browse-resolves to its raw source (and the raw node back via the inverse). The edge is wired + /// bidirectionally (forward on the source, inverse on the target) so browse resolves both ways. + /// Idempotent (an existing edge is not duplicated); a missing endpoint is a no-op + /// (logged, never thrown) so a mid-rebuild race can't fault a deploy. + /// + /// The source node's s= id (the referencing node — e.g. the UNS variable). + /// The namespace realm lives in. + /// The target node's s= id (the referenced node — e.g. the backing Raw node). + /// The namespace realm lives in. + /// The hierarchical reference type name — Organizes (default), + /// HasComponent, or HasProperty; unknown names fall back to Organizes. + void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, + string targetNodeId, AddressSpaceRealm targetRealm, + string referenceType = "Organizes"); } /// OPC UA status code projection — Good / Uncertain / Bad. Real SDK has finer-grained @@ -144,4 +164,7 @@ public sealed class NullOpcUaAddressSpaceSink : IOpcUaAddressSpaceSink /// public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + + /// + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index b6375a5e..5f8b6169 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -1736,6 +1736,97 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 } } + /// + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, + string targetNodeId, AddressSpaceRealm targetRealm, + string referenceType = "Organizes") + { + ArgumentException.ThrowIfNullOrEmpty(sourceNodeId); + ArgumentException.ThrowIfNullOrEmpty(targetNodeId); + EnsureAddressSpaceCreated(); + + var refType = ResolveReferenceType(referenceType); + var sourceKey = MapKey(sourceRealm, sourceNodeId); + var targetKey = MapKey(targetRealm, targetNodeId); + lock (Lock) + { + var source = ResolveNodeState(sourceKey); + var target = ResolveNodeState(targetKey); + // A missing endpoint is a no-op (logged) — the applier may drive the cross-tree link while a + // concurrent rebuild has torn one side down; never throw out of a deploy-time reference wire. + if (source is null || target is null) + { +#pragma warning disable CS0618 // Utils.LogWarning is [Obsolete] in favour of an ITelemetryContext this manager doesn't carry. + Utils.LogWarning( + "OtOpcUaNodeManager: AddReference({0}) skipped — {1} node missing (source '{2}' present={3}, target '{4}' present={5}).", + refType, source is null ? "source" : "target", sourceKey, source is not null, targetKey, target is not null); +#pragma warning restore CS0618 + return; + } + + // Idempotent: a re-apply must not duplicate the edge. Organizes is hierarchical, so wire it + // bidirectionally — forward on the source, inverse (OrganizedBy) on the target — so browse + // resolves both ways. Only ClearChangeMasks a side we actually mutated. + var sourceChanged = false; + var targetChanged = false; + if (!source.ReferenceExists(refType, isInverse: false, target.NodeId)) + { + source.AddReference(refType, isInverse: false, target.NodeId); + sourceChanged = true; + } + if (!target.ReferenceExists(refType, isInverse: true, source.NodeId)) + { + target.AddReference(refType, isInverse: true, source.NodeId); + targetChanged = true; + } + if (sourceChanged) source.ClearChangeMasks(SystemContext, includeChildren: false); + if (targetChanged) target.ClearChangeMasks(SystemContext, includeChildren: false); + } + } + + /// Resolve a materialised node (variable, folder, or alarm condition) by its full + /// (namespace-qualified) map key, or null when no node is registered under that key. Used by + /// to link two already-materialised nodes regardless of their kind. + /// The namespace-qualified map key (see ). + /// The node state, or null when unknown. + private NodeState? ResolveNodeState(string key) + { + if (_variables.TryGetValue(key, out var variable)) return variable; + if (_folders.TryGetValue(key, out var folder)) return folder; + if (_alarmConditions.TryGetValue(key, out var condition)) return condition; + return null; + } + + /// Test/diagnostic accessor: snapshot the references on a materialised node (by realm-qualified + /// id) using the manager's own SystemContext, or an empty list when the node is unknown. Exposed + /// so SdkAddressSpaceSinkTests can assert the cross-tree Organizes edge + /// wires without needing the (protected) SDK system context. + /// The node's s= id. + /// The realm the node lives in. + /// The node's references, or an empty list when the node is not registered. + internal IReadOnlyList GetNodeReferences(string nodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) + { + lock (Lock) + { + var node = ResolveNodeState(MapKey(realm, nodeId)); + if (node is null) return Array.Empty(); + var list = new List(); + node.GetReferences(SystemContext, list); + return list; + } + } + + /// Map a hierarchical reference-type name to its SDK . Unknown names fall + /// back to (the UNS→Raw cross-tree link). + /// The reference-type name (e.g. "Organizes"). + /// The reference-type NodeId. + private static NodeId ResolveReferenceType(string referenceType) => referenceType switch + { + "HasComponent" => ReferenceTypeIds.HasComponent, + "HasProperty" => ReferenceTypeIds.HasProperty, + _ => ReferenceTypeIds.Organizes, + }; + /// Build (but do not report) the Part 3 GeneralModelChangeEvent announcing that nodes were /// added under . MIRRORS exactly — /// the only differences are Verb = NodeAdded (vs DataTypeChanged) and Affected = the diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs index 65305767..fd8c008d 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs @@ -65,4 +65,8 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddre /// public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _nodeManager.RaiseNodesAddedModelChange(affectedNodeId, realm); + + /// + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") + => _nodeManager.AddReference(sourceNodeId, sourceRealm, targetNodeId, targetRealm, referenceType); } diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredAddressSpaceSinkTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredAddressSpaceSinkTests.cs index 33082333..cdbb26b0 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredAddressSpaceSinkTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/DeferredAddressSpaceSinkTests.cs @@ -195,6 +195,7 @@ public class DeferredAddressSpaceSinkTests public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } public void RebuildAddressSpace() => RebuildCalled = true; public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } private sealed class SpySurgicalSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink @@ -209,6 +210,7 @@ public class DeferredAddressSpaceSinkTests public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } public void RebuildAddressSpace() { } public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierFailureSurfaceTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierFailureSurfaceTests.cs index 6e6d0729..6e5aec88 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierFailureSurfaceTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierFailureSurfaceTests.cs @@ -189,5 +189,6 @@ public sealed class AddressSpaceApplierFailureSurfaceTests } public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs index c5210663..214da200 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierHierarchyTests.cs @@ -338,5 +338,6 @@ public sealed class AddressSpaceApplierHierarchyTests : IDisposable public void RebuildAddressSpace() { } /// Announces a NodeAdded model-change (stub implementation for testing). public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierTests.cs index 5d6ff679..1040e634 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AddressSpaceApplierTests.cs @@ -2307,6 +2307,7 @@ public sealed class AddressSpaceApplierTests /// Records a NodeAdded model-change announcement. /// The node under which discovered nodes were added. public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => ModelChangeQueue.Enqueue(affectedNodeId); + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } /// A recording sink that does NOT implement — used to @@ -2331,6 +2332,7 @@ public sealed class AddressSpaceApplierTests public void RebuildAddressSpace() => Interlocked.Increment(ref RebuildCalls); /// No-op NodeAdded model-change announcement. public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } private sealed class ThrowingSink : IOpcUaAddressSpaceSink @@ -2379,5 +2381,6 @@ public sealed class AddressSpaceApplierTests public void RebuildAddressSpace() { } /// No-op NodeAdded model-change announcement. public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs index 2265421e..d73a0b28 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs @@ -249,6 +249,7 @@ public sealed class DeferredAddressSpaceSinkTests public void RebuildAddressSpace() => CallQueue.Enqueue("RB"); /// public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => CallQueue.Enqueue($"NA:{affectedNodeId}"); + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } private sealed class SurgicalRecordingSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink @@ -298,5 +299,6 @@ public sealed class DeferredAddressSpaceSinkTests public void RebuildAddressSpace() { } /// public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs index 495c8a82..88b11f2c 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/SdkAddressSpaceSinkTests.cs @@ -52,6 +52,67 @@ public sealed class SdkAddressSpaceSinkTests : IDisposable await host.DisposeAsync(); } + /// B4-WP2 M1: AddReference wires the cross-tree Organizes edge from a UNS reference variable + /// (source, realm=Uns) to its backing Raw node (target, realm=Raw), bidirectionally, so browse resolves + /// both ways — and is idempotent. + [Fact] + public async Task AddReference_wires_Organizes_edge_from_UNS_variable_to_Raw_node() + { + var (host, server) = await BootAsync(); + var nm = server.NodeManager!; + var sink = new SdkAddressSpaceSink(nm); + + // Backing Raw node (device tree) and the UNS reference variable (equipment tree). + sink.EnsureVariable("MAIN/modbus/dev/grp/temp", parentFolderNodeId: null, displayName: "Temp", + dataType: "Float", writable: false, realm: AddressSpaceRealm.Raw); + sink.EnsureVariable("Area/Line/Eq/Temperature", parentFolderNodeId: null, displayName: "Temperature", + dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns); + + sink.AddReference("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns, + "MAIN/modbus/dev/grp/temp", AddressSpaceRealm.Raw); + + var unsVar = nm.TryGetVariable("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns); + var rawVar = nm.TryGetVariable("MAIN/modbus/dev/grp/temp", AddressSpaceRealm.Raw); + unsVar.ShouldNotBeNull(); + rawVar.ShouldNotBeNull(); + + // Forward Organizes on the UNS variable → Raw node; inverse (OrganizedBy) on the Raw node. + unsVar.ReferenceExists(ReferenceTypeIds.Organizes, isInverse: false, rawVar.NodeId).ShouldBeTrue(); + rawVar.ReferenceExists(ReferenceTypeIds.Organizes, isInverse: true, unsVar.NodeId).ShouldBeTrue(); + + // Idempotent: a second identical call does not duplicate the edge. The UNS variable's only + // FORWARD Organizes ref is the one we added (its parent link manifests as an inverse ref). + sink.AddReference("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns, + "MAIN/modbus/dev/grp/temp", AddressSpaceRealm.Raw); + nm.GetNodeReferences("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns) + .Count(r => r.ReferenceTypeId == ReferenceTypeIds.Organizes && !r.IsInverse).ShouldBe(1); + + await host.DisposeAsync(); + } + + /// B4-WP2 M1: a missing endpoint makes AddReference a safe no-op (never throws, no edge added). + [Fact] + public async Task AddReference_with_missing_endpoint_is_a_safe_no_op() + { + var (host, server) = await BootAsync(); + var nm = server.NodeManager!; + var sink = new SdkAddressSpaceSink(nm); + + sink.EnsureVariable("Area/Line/Eq/Temperature", parentFolderNodeId: null, displayName: "Temperature", + dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns); + + // Target does not exist ⇒ no-op, no throw. + Should.NotThrow(() => sink.AddReference("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns, + "MAIN/does/not/exist", AddressSpaceRealm.Raw)); + + var unsVar = nm.TryGetVariable("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns); + unsVar.ShouldNotBeNull(); + nm.GetNodeReferences("Area/Line/Eq/Temperature", AddressSpaceRealm.Uns) + .Any(r => r.ReferenceTypeId == ReferenceTypeIds.Organizes && !r.IsInverse).ShouldBeFalse(); + + await host.DisposeAsync(); + } + /// Verifies that RebuildAddressSpace clears all registered variables. [Fact] public async Task RebuildAddressSpace_clears_all_registered_variables() diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs index aeee15cc..3a80d6ec 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DiscoveryInjectionEndToEndTests.cs @@ -346,5 +346,6 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase /// Records a NodeAdded model-change announcement. public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => _modelChanges.Enqueue(affectedNodeId); + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Observability/OtOpcUaTelemetryHookTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Observability/OtOpcUaTelemetryHookTests.cs index 751d2c11..6759ee96 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Observability/OtOpcUaTelemetryHookTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Observability/OtOpcUaTelemetryHookTests.cs @@ -224,5 +224,6 @@ public sealed class OtOpcUaTelemetryHookTests : RuntimeActorTestBase public void RebuildAddressSpace() { /* recorded via span */ } /// Announces a NodeAdded model-change (stub implementation). public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } } diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorApplyFailureTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorApplyFailureTests.cs index 1e4256ff..a97da8f0 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorApplyFailureTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorApplyFailureTests.cs @@ -125,6 +125,7 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } public void RebuildAddressSpace() => throw new InvalidOperationException("simulated rebuild fault"); public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } /// A no-op sink — a clean apply (no failures). @@ -137,6 +138,7 @@ public sealed class OpcUaPublishActorApplyFailureTests : RuntimeActorTestBase public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } public void RebuildAddressSpace() { } public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { } + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } /// Listens to a single instrument by name on the central meter and tallies value + tags. diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs index 1a1c43d1..1d2731db 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs @@ -473,6 +473,7 @@ public sealed class OpcUaPublishActorRebuildTests : RuntimeActorTestBase /// Records a NodeAdded model-change announcement. /// The node under which discovered nodes were added. public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => Calls.Enqueue($"NA:{affectedNodeId}"); + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } /// Records a surgical in-place tag-attribute update (always succeeds in this recording sink). public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns) { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorTests.cs index 7086d3c0..c9c25bc3 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorTests.cs @@ -647,6 +647,7 @@ public sealed class OpcUaPublishActorTests : RuntimeActorTestBase /// Records a NodeAdded model-change announcement. /// The node under which discovered nodes were added. public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => ModelChangeQueue.Enqueue(affectedNodeId); + public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { } } /// Test implementation of IServiceLevelPublisher that records publishes.