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.