diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaNodeWriteGateway.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaNodeWriteGateway.cs
index 4469005f..17a10b80 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaNodeWriteGateway.cs
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IOpcUaNodeWriteGateway.cs
@@ -11,11 +11,17 @@ public interface IOpcUaNodeWriteGateway
{
/// Route a write of to the driver backing node
/// ; the returned task resolves with the device-write outcome.
- /// The folder-scoped equipment-variable node id being written.
+ /// The full ns-qualified node id being written (node.NodeId.ToString()).
/// The value the client wrote.
+ /// Which of the two v3 namespaces ( device tree or
+ /// equipment tree) lives in — the node
+ /// manager resolves it from the node's namespace index (RealmOf). It is REQUIRED for correct
+ /// routing: a raw s=<RawPath> and a UNS s=<Area/Line/Equip/Eff> can collide as bare
+ /// strings, so the routing map is keyed by (realm, bareId) — dropping the realm would let an
+ /// operator write route to the WRONG driver ref.
/// Cancellation token.
/// A task resolving to the device-write outcome.
- Task WriteAsync(string nodeId, object? value, CancellationToken ct);
+ Task WriteAsync(string nodeId, object? value, AddressSpaceRealm realm, CancellationToken ct);
}
/// Outcome of routing an inbound node write to the backing driver.
@@ -31,6 +37,6 @@ public sealed class NullOpcUaNodeWriteGateway : IOpcUaNodeWriteGateway
public static readonly NullOpcUaNodeWriteGateway Instance = new();
private NullOpcUaNodeWriteGateway() { }
///
- public Task WriteAsync(string nodeId, object? value, CancellationToken ct) =>
+ public Task WriteAsync(string nodeId, object? value, AddressSpaceRealm realm, CancellationToken ct) =>
Task.FromResult(new NodeWriteOutcome(false, "writes unavailable"));
}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
index 5f8b6169..e0f3d357 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
@@ -349,7 +349,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// The new value to write.
/// The OPC UA quality status code.
/// The timestamp of the value in UTC.
- public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc, AddressSpaceRealm realm)
{
ArgumentException.ThrowIfNullOrEmpty(nodeId);
EnsureAddressSpaceCreated();
@@ -386,7 +386,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// The node identifier of the alarm (== ScriptedAlarmId for materialised conditions).
/// The full condition state to project onto the node.
/// The timestamp of the alarm state change in UTC.
- public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc, AddressSpaceRealm realm)
{
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
ArgumentNullException.ThrowIfNull(state);
@@ -678,7 +678,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// . LimitAlarm deliberately falls back to base per the T13
/// notes — a script alarm carries no High/Low limits to populate.
///
- public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false)
{
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
ArgumentException.ThrowIfNullOrEmpty(displayName);
@@ -1000,7 +1000,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
// revert never runs inline on the SDK write thread (the gateway can return a synchronously-completed
// task — e.g. its boot-window "no DriverHostActor yet" branch), so RevertOptimisticWriteIfNeeded never
// re-enters lock (Lock) while CustomNodeManager2.Write still holds it.
- _ = gateway.WriteAsync(nodeKey, optimisticValue, CancellationToken.None)
+ _ = gateway.WriteAsync(nodeKey, optimisticValue, nodeRealm, CancellationToken.None)
.ContinueWith(
t =>
{
@@ -1206,7 +1206,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// from to populate the audit event's ClientUserId; null when unknown.
internal void RevertOptimisticWriteIfNeeded(
string nodeId, NodeWriteOutcome outcome, object? optimisticValue, object? priorValue, StatusCode priorStatus,
- string? clientUserId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ string? clientUserId, AddressSpaceRealm realm)
{
// Built under Lock if (and only if) a revert is performed, then reported AFTER Lock is released.
AuditWriteUpdateEventState? auditEvent = null;
@@ -1398,7 +1398,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// The node identifier of the folder.
/// The node identifier of the parent folder; null to use the namespace root.
/// The display name of the folder.
- public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm)
{
ArgumentException.ThrowIfNullOrEmpty(folderNodeId);
ArgumentException.ThrowIfNullOrEmpty(displayName);
@@ -1443,7 +1443,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// The node identifier of the folder to update in place.
/// The new display name to apply.
/// True when the in-place update was applied; false when the folder id is unknown.
- public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public bool UpdateFolderDisplayName(string folderNodeId, string displayName, AddressSpaceRealm realm)
{
ArgumentException.ThrowIfNullOrEmpty(folderNodeId);
ArgumentException.ThrowIfNullOrEmpty(displayName);
@@ -1485,7 +1485,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// — rank + dimensions carry the array-ness.
/// Phase 4c: the declared length of the 1-D array when
/// is true; ignored for scalars. Null ⇒ length 0.
- 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 EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
{
ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
ArgumentException.ThrowIfNullOrEmpty(displayName);
@@ -1583,7 +1583,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// When true the node becomes a 1-D array (ValueRank=OneDimension); when false scalar.
/// The declared length of the 1-D array when is true; ignored for scalars.
/// True when the in-place update was applied; false when the node id is unknown.
- public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm)
{
ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
ArgumentException.ThrowIfNullOrEmpty(dataType); // widened surface ⇒ explicit contract (unknown names still map to BaseDataType)
@@ -1712,7 +1712,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
///
///
/// The folder-scoped node id of the parent under which nodes were added.
- public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm)
{
ArgumentException.ThrowIfNullOrEmpty(affectedNodeId);
GeneralModelChangeEventState e;
@@ -2004,7 +2004,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
}
///
- public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public bool RemoveVariableNode(string variableNodeId, AddressSpaceRealm realm)
{
ArgumentException.ThrowIfNullOrEmpty(variableNodeId);
EnsureAddressSpaceCreated();
@@ -2028,7 +2028,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
}
///
- public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public bool RemoveAlarmConditionNode(string alarmNodeId, AddressSpaceRealm realm)
{
ArgumentException.ThrowIfNullOrEmpty(alarmNodeId);
EnsureAddressSpaceCreated();
@@ -2053,7 +2053,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
}
///
- public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
+ public bool RemoveEquipmentSubtree(string equipmentNodeId, AddressSpaceRealm realm)
{
ArgumentException.ThrowIfNullOrEmpty(equipmentNodeId);
EnsureAddressSpaceCreated();
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs
index 87927ae7..084ba026 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs
@@ -30,7 +30,7 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddre
///
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, AddressSpaceRealm realm, bool isNative = false)
- => _nodeManager.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative, realm);
+ => _nodeManager.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, realm, isNative);
///
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName, AddressSpaceRealm realm)
@@ -38,7 +38,7 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddre
///
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, AddressSpaceRealm realm, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
- => _nodeManager.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength, realm);
+ => _nodeManager.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, realm, historianTagname, isArray, arrayLength);
///
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength, AddressSpaceRealm realm)
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/ActorNodeWriteGateway.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/ActorNodeWriteGateway.cs
index ccb91771..7c6cb37e 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/ActorNodeWriteGateway.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/ActorNodeWriteGateway.cs
@@ -43,7 +43,7 @@ public sealed class ActorNodeWriteGateway : IOpcUaNodeWriteGateway
}
///
- public async Task WriteAsync(string nodeId, object? value, CancellationToken ct)
+ public async Task WriteAsync(string nodeId, object? value, AddressSpaceRealm realm, CancellationToken ct)
{
var driverHost = _resolveDriverHost();
if (driverHost is null)
@@ -55,7 +55,7 @@ public sealed class ActorNodeWriteGateway : IOpcUaNodeWriteGateway
try
{
var result = await driverHost.Ask(
- new DriverHostActor.RouteNodeWrite(nodeId, value), _askTimeout, ct).ConfigureAwait(false);
+ new DriverHostActor.RouteNodeWrite(nodeId, value, realm), _askTimeout, ct).ConfigureAwait(false);
if (!result.Success)
_logger.LogWarning("Operator write to {NodeId} rejected: {Reason}", nodeId, result.Reason);
return new NodeWriteOutcome(result.Success, result.Reason);
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
index 46e99fbf..7723c662 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs
@@ -128,19 +128,22 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
private readonly Dictionary<(string DriverInstanceId, string RawPath), HashSet> _nodeIdByDriverRef = new();
///
- /// Inverse of : materialised value NodeId (BARE s= id) →
- /// (DriverInstanceId, RawPath). v3 Batch 4: keyed by BOTH the raw NodeId AND every referencing UNS
- /// NodeId (all mapping to the same driver ref — the single value source). Rebuilt every apply by
- /// from the composition's RawTags ∪
- /// UnsReferenceVariables, and resolved by so an inbound
- /// operator write to EITHER NodeId is forwarded to the owning driver child as a write of its wire-ref
- /// RawPath. Keyed by the BARE id: the node manager's write hook passes the full ns-qualified id
- /// (node.NodeId.ToString()); normalises it to the bare id
- /// before lookup, so a raw write and a UNS write both resolve to the same driver ref (they share it —
- /// a UNS reference's backing RawPath IS the raw node's, so no cross-realm ambiguity is possible).
+ /// Inverse of : (Realm, BARE s= id) → (DriverInstanceId,
+ /// RawPath). v3 Batch 4: keyed by BOTH the raw NodeId () AND every
+ /// referencing UNS NodeId () — all mapping to the same driver ref
+ /// (the single value source). Rebuilt every apply by from the
+ /// composition's RawTags ∪ UnsReferenceVariables, and resolved by
+ /// so an inbound operator write to EITHER NodeId is forwarded to the
+ /// owning driver child as a write of its wire-ref RawPath.
+ /// The realm is part of the key (Wave B review H1): a raw s=<RawPath> and a UNS
+ /// s=<Area/Line/Equip/Eff> can collide as bare strings (folder/driver/device names and
+ /// Area/Line/Equip names are independent), so a bare-only key would let a colliding raw + UNS pair route
+ /// to the WRONG driver ref (last-writer-wins). The node manager's write hook passes the full
+ /// ns-qualified id PLUS the realm it resolved (RealmOf);
+ /// normalises the id to the bare form and looks up (realm, bareId), preserving exactly the
+ /// namespace disambiguation the ns-qualified id carries.
///
- private readonly Dictionary _driverRefByNodeId =
- new(StringComparer.Ordinal);
+ private readonly Dictionary<(AddressSpaceRealm Realm, string NodeId), (string DriverInstanceId, string RawPath)> _driverRefByNodeId = new();
/// (DriverInstanceId, RawPath = alarm ConditionId) → materialised condition NodeId(s) (realm-tagged).
/// v3 Batch 4: a native alarm is a single condition at the RawPath (Raw realm); WP4 adds the referencing
@@ -241,7 +244,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
///
/// The folder-scoped equipment-variable NodeId the operator wrote to.
/// The value to write (the driver coerces it to the attribute's data type).
- public sealed record RouteNodeWrite(string NodeId, object? Value);
+ public sealed record RouteNodeWrite(string NodeId, object? Value, AddressSpaceRealm Realm);
/// Reply to : the outcome of forwarding the write to the driver
/// (or a gate/lookup failure that never reached the driver).
@@ -654,6 +657,21 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
///
private void HandleDiscoveredNodes(DriverInstanceActor.DiscoveredNodesReady msg)
{
+ // v3 Batch 4 (review M1): discovered-node INJECTION is DORMANT in v3 and hard-guarded here. In v2 a
+ // driver-connected FixedTree was grafted under an equipment folder (equipment bound a driver); v3
+ // retired that binding — equipment references raw tags via UnsTagReference, and discovered raw tags are
+ // authored explicitly through the Batch-2 /raw browse-commit flow, NOT injected at runtime. The
+ // downstream mapper/materialiser were half-migrated to the Raw tree but are still rooted at an
+ // equipment id, so letting this fire would materialise incoherent nodes. Short-circuit BEFORE any
+ // caching/routing so _discoveredByDriver stays empty (the redeploy re-inject tail is therefore inert
+ // too) and there is exactly one enforcement point. Re-migrating injection onto the raw device subtree
+ // is a separate follow-up.
+ _log.Debug(
+ "DriverHost {Node}: discovered-node injection is dormant in v3 (DiscoveredNodesReady from {Driver} ignored; discovered raw tags are authored via /raw browse-commit)",
+ _localNode, msg.DriverInstanceId);
+ return;
+
+#pragma warning disable CS0162 // Unreachable code — retained for the raw-subtree re-migration follow-up.
if (_lastComposition is null)
{
_log.Debug("DriverHost {Node}: DiscoveredNodesReady from {Driver} before any composition applied — ignored",
@@ -728,6 +746,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
_discoveredByDriver[msg.DriverInstanceId] = newPlans;
ApplyDiscoveredPlansForDriver(msg.DriverInstanceId, newPlans);
+#pragma warning restore CS0162
}
///
@@ -933,7 +952,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
// v3 Batch 4: discovered (FixedTree) nodes graft onto the RAW device subtree, so they route
// through the Raw realm.
set.Add(new NodeRealmRef(nodeId, AddressSpaceRealm.Raw));
- _driverRefByNodeId[nodeId] = key;
+ _driverRefByNodeId[(AddressSpaceRealm.Raw, nodeId)] = key;
}
_opcUaPublishActor?.Tell(new ZB.MOM.WW.OtOpcUa.Runtime.OpcUa.OpcUaPublishActor.MaterialiseDiscoveredNodes(
equipmentId, plan.Folders, plan.Variables));
@@ -1101,15 +1120,15 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
return;
}
- // v3 Batch 4: the node manager's write hook passes the FULL ns-qualified NodeId string
- // (node.NodeId.ToString(), e.g. "ns=3;s="). The routing map is keyed by the BARE s= identifier
- // (the raw RawPath or the UNS path). Normalise before lookup so a write to EITHER a raw NodeId or a
- // referencing UNS NodeId resolves to the SAME driver ref (they share it — a UNS reference's backing
- // RawPath IS the raw node's, so there is no cross-realm ambiguity).
+ // v3 Batch 4 (review H1): the node manager's write hook passes the FULL ns-qualified NodeId string
+ // (node.NodeId.ToString(), e.g. "ns=3;s=") PLUS the realm it resolved from the namespace index.
+ // The routing map is keyed by (realm, BARE s= identifier). Normalise the id to its bare form and look
+ // up (realm, bareId) — the realm disambiguates a raw RawPath from a UNS path that happen to share a
+ // bare string, so a write always routes to its OWN driver ref (never a colliding sibling's).
var bareNodeId = BareNodeId(msg.NodeId);
- if (!_driverRefByNodeId.TryGetValue(bareNodeId, out var target))
+ if (!_driverRefByNodeId.TryGetValue((msg.Realm, bareNodeId), out var target))
{
- Sender.Tell(new NodeWriteResult(false, $"no driver mapping for node {msg.NodeId}"));
+ Sender.Tell(new NodeWriteResult(false, $"no driver mapping for node {msg.NodeId} ({msg.Realm})"));
return;
}
@@ -1564,14 +1583,16 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers
if (!_nodeIdByDriverRef.TryGetValue(key, out var set))
_nodeIdByDriverRef[key] = set = new HashSet();
set.Add(new NodeRealmRef(t.NodeId, AddressSpaceRealm.Raw));
- _driverRefByNodeId[t.NodeId] = key;
+ _driverRefByNodeId[(AddressSpaceRealm.Raw, t.NodeId)] = key;
if (unsRefsByRawPath.TryGetValue(t.NodeId, out var refs))
{
foreach (var v in refs)
{
set.Add(new NodeRealmRef(v.NodeId, AddressSpaceRealm.Uns));
- _driverRefByNodeId[v.NodeId] = key; // a UNS write resolves to the same driver ref
+ // A UNS write resolves to the same driver ref — keyed under the Uns realm so it can never
+ // collide with a raw NodeId that shares its bare string.
+ _driverRefByNodeId[(AddressSpaceRealm.Uns, v.NodeId)] = key;
}
}
}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
index b49b623e..15e0fe51 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs
@@ -45,7 +45,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
/// referencing UNS node in ) with identical value/quality/timestamp.
/// defaults to so pre-v3 callers (VirtualTag
/// equipment nodes) keep compiling; the driver fan-out passes it EXPLICITLY per node.
- public sealed record AttributeValueUpdate(string NodeId, object? Value, OpcUaQuality Quality, DateTime TimestampUtc, AddressSpaceRealm Realm = AddressSpaceRealm.Uns);
+ public sealed record AttributeValueUpdate(string NodeId, object? Value, OpcUaQuality Quality, DateTime TimestampUtc, AddressSpaceRealm Realm);
/// Carries the full Part 9 condition state for a scripted alarm to the sink. The
/// snapshot is the Commons projection the Runtime host maps from the engine's
@@ -57,7 +57,7 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers
/// The source timestamp of the transition in UTC.
/// The namespace realm the condition lives in — for
/// scripted alarms (default), for v3 native raw conditions.
- public sealed record AlarmStateUpdate(string AlarmNodeId, AlarmConditionSnapshot State, DateTime TimestampUtc, AddressSpaceRealm Realm = AddressSpaceRealm.Uns);
+ public sealed record AlarmStateUpdate(string AlarmNodeId, AlarmConditionSnapshot State, DateTime TimestampUtc, AddressSpaceRealm Realm);
///
/// Triggers an address-space rebuild. is the deployment
/// just applied by the host; the rebuild loads THAT artifact so materialisation matches the
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs
index a8d790b8..0edea434 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs
@@ -297,10 +297,12 @@ public sealed class ScriptedAlarmHostActor : ReceiveActor
// Bridge to OPC UA: project the FULL Part 9 condition state (enabled/active/acked/confirmed/
// shelving/severity/message) onto the materialised condition node via the Commons snapshot.
// e.AlarmId is the materialised condition's NodeId (T14 aligned it to the ScriptedAlarmId).
+ // Scripted alarms are per-equipment condition nodes in the UNS realm.
_publishActor.Tell(new OpcUaPublishActor.AlarmStateUpdate(
AlarmNodeId: e.AlarmId,
State: ToSnapshot(e),
- TimestampUtc: e.TimestampUtc));
+ TimestampUtc: e.TimestampUtc,
+ Realm: AddressSpaceRealm.Uns));
// Publish the transition to the cluster `alerts` topic — the single historization + live
// fan-out path. The mediator was cached on the ACTOR thread in PreStart; we only Tell it here.
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs
index 961f2fad..4229d68d 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs
@@ -19,9 +19,10 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;
/// already-materialised Variable node (currently BadWaitingForInitialData) reflects the value.
///
///
-/// The published NodeId is computed by the shared —
-/// the single source of truth AddressSpaceApplier.MaterialiseEquipmentVirtualTags also
-/// materialises against — so the value always lands on a NodeId that exists.
+/// The published NodeId is computed via (the equipment-anchored
+/// slash path {EquipmentId}[/{FolderPath}]/{Name}) — byte-identical to the NodeId
+/// AddressSpaceApplier.MaterialiseEquipmentVirtualTags materialises against — so the value
+/// always lands on a NodeId that exists.
///
///
public sealed class VirtualTagHostActor : ReceiveActor
@@ -192,8 +193,9 @@ public sealed class VirtualTagHostActor : ReceiveActor
// 02/S13: the child now expresses quality — a Good fresh value or a Bad degradation (script
// failure/timeout) carrying the last-known value. Bridge result.Quality through verbatim; the
// sink maps it to StatusCodes.Good/Bad on the node so a broken script no longer freezes Good.
+ // VirtualTags materialise as equipment nodes in the UNS realm — publish there.
_publishActor.Tell(new OpcUaPublishActor.AttributeValueUpdate(
- nodeId, result.Value, result.Quality, result.TimestampUtc));
+ nodeId, result.Value, result.Quality, result.TimestampUtc, AddressSpaceRealm.Uns));
// Historize iff the plan opted in. Reuses _planByVtag (kept in lock-step with _children), so
// no parallel map. The historian path key is the SAME folder-scoped NodeId we just published
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/NullOpcUaNodeWriteGatewayTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/NullOpcUaNodeWriteGatewayTests.cs
index dc7a2899..60cc02d7 100644
--- a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/NullOpcUaNodeWriteGatewayTests.cs
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/OpcUa/NullOpcUaNodeWriteGatewayTests.cs
@@ -9,7 +9,7 @@ public class NullOpcUaNodeWriteGatewayTests
[Fact]
public async Task NullGateway_returns_writes_unavailable()
{
- var outcome = await NullOpcUaNodeWriteGateway.Instance.WriteAsync("ns=2;s=x", 1, TestContext.Current.CancellationToken);
+ var outcome = await NullOpcUaNodeWriteGateway.Instance.WriteAsync("ns=2;s=x", 1, AddressSpaceRealm.Uns, TestContext.Current.CancellationToken);
outcome.Success.ShouldBeFalse();
outcome.Reason.ShouldBe("writes unavailable");
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/PrimaryGateFailoverTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/PrimaryGateFailoverTests.cs
index c9ca2b92..d32e42b9 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/PrimaryGateFailoverTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests/PrimaryGateFailoverTests.cs
@@ -2,6 +2,7 @@ using Akka.Actor;
using Akka.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Runtime;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
@@ -68,7 +69,7 @@ public sealed class PrimaryGateFailoverTests
private static async Task RouteProbeWriteAsync(IActorRef driverHost)
=> await driverHost.Ask(
- new DriverHostActor.RouteNodeWrite(UnmappedProbeNode, 0.0), TimeSpan.FromSeconds(10));
+ new DriverHostActor.RouteNodeWrite(UnmappedProbeNode, 0.0, AddressSpaceRealm.Uns), TimeSpan.FromSeconds(10));
private static bool IsGateReject(DriverHostActor.NodeWriteResult r)
=> !r.Success && r.Reason is not null && r.Reason.StartsWith("not primary", StringComparison.Ordinal);
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs
index e5f9f928..210c15c2 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs
@@ -34,8 +34,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
- nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-1");
condition.ShouldNotBeNull();
condition!.OnAcknowledge.ShouldNotBeNull();
@@ -66,8 +66,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-2", parentNodeId: null, displayName: "Equipment 2");
- nm.MaterialiseAlarmCondition("alm-2", "eq-2", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-2", parentNodeId: null, displayName: "Equipment 2", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-2", "eq-2", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-2");
condition.ShouldNotBeNull();
@@ -91,8 +91,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-3", parentNodeId: null, displayName: "Equipment 3");
- nm.MaterialiseAlarmCondition("alm-3", "eq-3", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-3", parentNodeId: null, displayName: "Equipment 3", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-3", "eq-3", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-3");
condition.ShouldNotBeNull();
@@ -115,8 +115,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-c", parentNodeId: null, displayName: "Equipment C");
- nm.MaterialiseAlarmCondition("alm-c", "eq-c", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-c", parentNodeId: null, displayName: "Equipment C", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-c", "eq-c", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-c");
condition.ShouldNotBeNull();
condition!.OnConfirm.ShouldNotBeNull();
@@ -142,8 +142,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-ac", parentNodeId: null, displayName: "Equipment AC");
- nm.MaterialiseAlarmCondition("alm-ac", "eq-ac", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-ac", parentNodeId: null, displayName: "Equipment AC", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-ac", "eq-ac", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-ac");
condition.ShouldNotBeNull();
condition!.OnAddComment.ShouldNotBeNull();
@@ -168,8 +168,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-s1", parentNodeId: null, displayName: "Equipment S1");
- nm.MaterialiseAlarmCondition("alm-s1", "eq-s1", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-s1", parentNodeId: null, displayName: "Equipment S1", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-s1", "eq-s1", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-s1");
condition.ShouldNotBeNull();
condition!.OnShelve.ShouldNotBeNull();
@@ -196,8 +196,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-s2", parentNodeId: null, displayName: "Equipment S2");
- nm.MaterialiseAlarmCondition("alm-s2", "eq-s2", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-s2", parentNodeId: null, displayName: "Equipment S2", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-s2", "eq-s2", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-s2");
condition.ShouldNotBeNull();
@@ -228,8 +228,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-s3", parentNodeId: null, displayName: "Equipment S3");
- nm.MaterialiseAlarmCondition("alm-s3", "eq-s3", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-s3", parentNodeId: null, displayName: "Equipment S3", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-s3", "eq-s3", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-s3");
condition.ShouldNotBeNull();
@@ -254,8 +254,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-s4", parentNodeId: null, displayName: "Equipment S4");
- nm.MaterialiseAlarmCondition("alm-s4", "eq-s4", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-s4", parentNodeId: null, displayName: "Equipment S4", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-s4", "eq-s4", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-s4");
condition.ShouldNotBeNull();
@@ -287,8 +287,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-tu", parentNodeId: null, displayName: "Equipment TU");
- nm.MaterialiseAlarmCondition("alm-tu", "eq-tu", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-tu", parentNodeId: null, displayName: "Equipment TU", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-tu", "eq-tu", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-tu");
condition.ShouldNotBeNull();
condition!.OnTimedUnshelve.ShouldNotBeNull();
@@ -322,8 +322,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-ed1", parentNodeId: null, displayName: "Equipment ED1");
- nm.MaterialiseAlarmCondition("alm-ed1", "eq-ed1", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false);
+ nm.EnsureFolder("eq-ed1", parentNodeId: null, displayName: "Equipment ED1", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-ed1", "eq-ed1", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-ed1");
condition.ShouldNotBeNull();
condition!.OnEnableDisable.ShouldNotBeNull();
@@ -351,8 +351,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-ed2", parentNodeId: null, displayName: "Equipment ED2");
- nm.MaterialiseAlarmCondition("alm-ed2", "eq-ed2", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false);
+ nm.EnsureFolder("eq-ed2", parentNodeId: null, displayName: "Equipment ED2", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-ed2", "eq-ed2", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-ed2");
condition.ShouldNotBeNull();
condition!.OnEnableDisable.ShouldNotBeNull();
@@ -379,8 +379,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-ed3", parentNodeId: null, displayName: "Equipment ED3");
- nm.MaterialiseAlarmCondition("alm-ed3", "eq-ed3", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false);
+ nm.EnsureFolder("eq-ed3", parentNodeId: null, displayName: "Equipment ED3", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-ed3", "eq-ed3", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-ed3");
condition.ShouldNotBeNull();
@@ -405,8 +405,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var captured = new List();
nm.AlarmCommandRouter = captured.Add;
- nm.EnsureFolder("eq-ed4", parentNodeId: null, displayName: "Equipment ED4");
- nm.MaterialiseAlarmCondition("alm-ed4", "eq-ed4", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
+ nm.EnsureFolder("eq-ed4", parentNodeId: null, displayName: "Equipment ED4", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-ed4", "eq-ed4", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-ed4");
condition.ShouldNotBeNull();
condition!.OnEnableDisable.ShouldNotBeNull();
@@ -436,8 +436,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
nm.AlarmCommandRouter = scripted.Add;
nm.NativeAlarmAckRouter = native.Add;
- nm.EnsureFolder("eq-nak1", parentNodeId: null, displayName: "Equipment NAK1");
- nm.MaterialiseAlarmCondition("alm-nak1", "eq-nak1", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
+ nm.EnsureFolder("eq-nak1", parentNodeId: null, displayName: "Equipment NAK1", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-nak1", "eq-nak1", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-nak1");
condition.ShouldNotBeNull();
condition!.OnAcknowledge.ShouldNotBeNull();
@@ -469,8 +469,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
nm.AlarmCommandRouter = scripted.Add;
nm.NativeAlarmAckRouter = native.Add;
- nm.EnsureFolder("eq-nak2", parentNodeId: null, displayName: "Equipment NAK2");
- nm.MaterialiseAlarmCondition("alm-nak2", "eq-nak2", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false);
+ nm.EnsureFolder("eq-nak2", parentNodeId: null, displayName: "Equipment NAK2", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-nak2", "eq-nak2", "HighTemp", "OffNormalAlarm", severity: 700, isNative: false, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-nak2");
condition.ShouldNotBeNull();
condition!.OnAcknowledge.ShouldNotBeNull();
@@ -503,8 +503,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
nm.AlarmCommandRouter = scripted.Add;
nm.NativeAlarmAckRouter = native.Add;
- nm.EnsureFolder("eq-nak4", parentNodeId: null, displayName: "Equipment NAK4");
- nm.MaterialiseAlarmCondition("alm-nak4", "eq-nak4", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
+ nm.EnsureFolder("eq-nak4", parentNodeId: null, displayName: "Equipment NAK4", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-nak4", "eq-nak4", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-nak4");
condition.ShouldNotBeNull();
@@ -531,8 +531,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
nm.AlarmCommandRouter = scripted.Add;
nm.NativeAlarmAckRouter = native.Add;
- nm.EnsureFolder("eq-nak3", parentNodeId: null, displayName: "Equipment NAK3");
- nm.MaterialiseAlarmCondition("alm-nak3", "eq-nak3", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true);
+ nm.EnsureFolder("eq-nak3", parentNodeId: null, displayName: "Equipment NAK3", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-nak3", "eq-nak3", "HighTemp", "OffNormalAlarm", severity: 700, isNative: true, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-nak3");
condition.ShouldNotBeNull();
@@ -555,8 +555,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var nm = server.NodeManager!;
// No router set (default null).
- nm.EnsureFolder("eq-nr", parentNodeId: null, displayName: "Equipment NR");
- nm.MaterialiseAlarmCondition("alm-nr", "eq-nr", "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder("eq-nr", parentNodeId: null, displayName: "Equipment NR", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-nr", "eq-nr", "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var condition = nm.TryGetAlarmCondition("alm-nr");
condition.ShouldNotBeNull();
@@ -576,8 +576,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true);
+ nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
nm.IsNativeAlarmNode("a1").ShouldBeTrue();
@@ -591,8 +591,8 @@ public sealed class AlarmCommandRouterTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("a2", "eq", "d", "OffNormalAlarm", 700, isNative: false);
+ nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("a2", "eq", "d", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
nm.IsNativeAlarmNode("a2").ShouldBeFalse();
@@ -611,15 +611,15 @@ public sealed class AlarmCommandRouterTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true);
+ nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
nm.IsNativeAlarmNode("a1").ShouldBeTrue();
// RebuildAddressSpace clears the folder set too, so the equipment folder must be re-ensured
// before the same id can be re-materialised (ResolveParentFolder needs the parent back).
nm.RebuildAddressSpace();
- nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false);
+ nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
nm.IsNativeAlarmNode("a1").ShouldBeFalse();
@@ -636,13 +636,13 @@ public sealed class AlarmCommandRouterTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false);
+ nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
nm.IsNativeAlarmNode("a1").ShouldBeFalse();
nm.RebuildAddressSpace();
- nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true);
+ nm.EnsureFolder("eq", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("a1", "eq", "d", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
nm.IsNativeAlarmNode("a1").ShouldBeTrue();
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmIdempotentMaterialiseTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmIdempotentMaterialiseTests.cs
index c34a3151..6b62c032 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmIdempotentMaterialiseTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerAlarmIdempotentMaterialiseTests.cs
@@ -1,4 +1,5 @@
using Shouldly;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
@@ -27,14 +28,14 @@ public sealed class NodeManagerAlarmIdempotentMaterialiseTests : IDisposable
{
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
- nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
+ nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
var first = nm.TryGetAlarmCondition("alm-1");
first.ShouldNotBeNull();
// Same id + same kind ⇒ skip-if-present: the existing instance is kept.
- nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
+ nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
var second = nm.TryGetAlarmCondition("alm-1");
second.ShouldBeSameAs(first);
@@ -50,14 +51,14 @@ public sealed class NodeManagerAlarmIdempotentMaterialiseTests : IDisposable
{
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
- nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
+ nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
var scripted = nm.TryGetAlarmCondition("alm-1");
nm.IsNativeAlarmNode("alm-1").ShouldBeFalse();
// Same id but the OTHER kind ⇒ recreate (a different instance) and the native flag is now set.
- nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: true);
+ nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
var native = nm.TryGetAlarmCondition("alm-1");
native.ShouldNotBeSameAs(scripted);
@@ -75,16 +76,16 @@ public sealed class NodeManagerAlarmIdempotentMaterialiseTests : IDisposable
{
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
- nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
+ nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
var before = nm.TryGetAlarmCondition("alm-1");
nm.RebuildAddressSpace();
nm.TryGetAlarmCondition("alm-1").ShouldBeNull();
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
- nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false);
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 700, isNative: false, realm: AddressSpaceRealm.Uns);
var after = nm.TryGetAlarmCondition("alm-1");
after.ShouldNotBeNull();
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerArrayTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerArrayTests.cs
index 2dfce4d1..642fa81f 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerArrayTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerArrayTests.cs
@@ -32,7 +32,7 @@ public sealed class NodeManagerArrayTests : IDisposable
var nm = server.NodeManager!;
nm.EnsureVariable("eq-1/arr", parentFolderNodeId: null, displayName: "arr", dataType: "Int32",
- writable: false, historianTagname: null, isArray: true, arrayLength: 8);
+ writable: false, historianTagname: null, isArray: true, arrayLength: 8, realm: AddressSpaceRealm.Uns);
var variable = nm.TryGetVariable("eq-1/arr");
variable.ShouldNotBeNull();
@@ -52,7 +52,7 @@ public sealed class NodeManagerArrayTests : IDisposable
var nm = server.NodeManager!;
nm.EnsureVariable("eq-1/arr-unfixed", parentFolderNodeId: null, displayName: "arr-unfixed", dataType: "Int32",
- writable: false, historianTagname: null, isArray: true, arrayLength: null);
+ writable: false, historianTagname: null, isArray: true, arrayLength: null, realm: AddressSpaceRealm.Uns);
var variable = nm.TryGetVariable("eq-1/arr-unfixed");
variable.ShouldNotBeNull();
@@ -72,7 +72,7 @@ public sealed class NodeManagerArrayTests : IDisposable
var nm = server.NodeManager!;
nm.EnsureVariable("eq-1/scalar", parentFolderNodeId: null, displayName: "scalar", dataType: "Int32",
- writable: false);
+ writable: false, realm: AddressSpaceRealm.Uns);
var variable = nm.TryGetVariable("eq-1/scalar");
variable.ShouldNotBeNull();
@@ -91,10 +91,10 @@ public sealed class NodeManagerArrayTests : IDisposable
var nm = server.NodeManager!;
nm.EnsureVariable("eq-1/arrwrite", parentFolderNodeId: null, displayName: "arrwrite", dataType: "Int32",
- writable: false, historianTagname: null, isArray: true, arrayLength: 3);
+ writable: false, historianTagname: null, isArray: true, arrayLength: 3, realm: AddressSpaceRealm.Uns);
var payload = new[] { 1, 2, 3 };
- nm.WriteValue("eq-1/arrwrite", payload, OpcUaQuality.Good, DateTime.UtcNow);
+ nm.WriteValue("eq-1/arrwrite", payload, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var variable = nm.TryGetVariable("eq-1/arrwrite");
variable.ShouldNotBeNull();
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistorizeTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistorizeTests.cs
index d4e4f8a1..8e236777 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistorizeTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistorizeTests.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Opc.Ua;
using Shouldly;
using Xunit;
@@ -29,7 +30,7 @@ public sealed class NodeManagerHistorizeTests : IDisposable
var nm = server.NodeManager!;
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp", dataType: "Float",
- writable: false, historianTagname: "WW.Tag");
+ writable: false, historianTagname: "WW.Tag", realm: AddressSpaceRealm.Uns);
var variable = nm.TryGetVariable("eq-1/temp");
variable.ShouldNotBeNull();
@@ -56,9 +57,9 @@ public sealed class NodeManagerHistorizeTests : IDisposable
// Explicit null and the defaulted-param form both mean "not historized".
nm.EnsureVariable("eq-1/plain", parentFolderNodeId: null, displayName: "Plain", dataType: "Int32",
- writable: false, historianTagname: null);
+ writable: false, historianTagname: null, realm: AddressSpaceRealm.Uns);
nm.EnsureVariable("eq-1/plain2", parentFolderNodeId: null, displayName: "Plain2", dataType: "Int32",
- writable: false);
+ writable: false, realm: AddressSpaceRealm.Uns);
foreach (var nodeId in new[] { "eq-1/plain", "eq-1/plain2" })
{
@@ -83,7 +84,7 @@ public sealed class NodeManagerHistorizeTests : IDisposable
var nm = server.NodeManager!;
nm.EnsureVariable("eq-1/setpoint", parentFolderNodeId: null, displayName: "Setpoint", dataType: "Float",
- writable: true, historianTagname: "WW.Setpoint");
+ writable: true, historianTagname: "WW.Setpoint", realm: AddressSpaceRealm.Uns);
var variable = nm.TryGetVariable("eq-1/setpoint");
variable.ShouldNotBeNull();
@@ -109,7 +110,7 @@ public sealed class NodeManagerHistorizeTests : IDisposable
var nm = server.NodeManager!;
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp", dataType: "Float",
- writable: false, historianTagname: "WW.Tag");
+ writable: false, historianTagname: "WW.Tag", realm: AddressSpaceRealm.Uns);
nm.TryGetHistorizedTagname("eq-1/temp", out _).ShouldBeTrue();
nm.RebuildAddressSpace();
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadConcurrencyTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadConcurrencyTests.cs
index f8e97331..a8658918 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadConcurrencyTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadConcurrencyTests.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Server;
@@ -228,7 +229,7 @@ public sealed class NodeManagerHistoryReadConcurrencyTests : IDisposable
{
var key = $"eq/tag{i}";
nm.EnsureVariable(key, parentFolderNodeId: null, displayName: $"Tag{i}", dataType: "Float",
- writable: false, historianTagname: $"WW.Tag{i}");
+ writable: false, historianTagname: $"WW.Tag{i}", realm: AddressSpaceRealm.Uns);
ids[i] = nm.TryGetVariable(key)!.NodeId;
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadEventsTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadEventsTests.cs
index 20a14ccf..28bba442 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadEventsTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadEventsTests.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Opc.Ua;
using Opc.Ua.Server;
using Shouldly;
@@ -42,8 +43,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
nm.HistorianDataSource = fake;
const string equipmentId = "eq-evt";
- nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("alarm-1", equipmentId, "HighTemp", "OffNormalAlarm", severity: 700);
+ nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alarm-1", equipmentId, "HighTemp", "OffNormalAlarm", severity: 700, realm: AddressSpaceRealm.Uns);
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
var evtTime = new DateTime(2026, 6, 14, 10, 0, 0, DateTimeKind.Utc);
@@ -107,8 +108,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
nm.HistorianDataSource = fake;
const string equipmentId = "eq-unbounded";
- nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("alarm-0", equipmentId, "Cond", "OffNormalAlarm", severity: 600);
+ nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alarm-0", equipmentId, "Cond", "OffNormalAlarm", severity: 600, realm: AddressSpaceRealm.Uns);
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
fake.EventsResult = new HistoricalEventsResult(
@@ -143,8 +144,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
nm.HistorianDataSource = fake;
const string equipmentId = "eq-unsupported";
- nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("alarm-2", equipmentId, "Cond", "OffNormalAlarm", severity: 500);
+ nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alarm-2", equipmentId, "Cond", "OffNormalAlarm", severity: 500, realm: AddressSpaceRealm.Uns);
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
fake.EventsResult = new HistoricalEventsResult(
@@ -187,8 +188,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
nm.HistorianDataSource = fake;
const string equipmentId = "eq-empty";
- nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("alarm-3", equipmentId, "Cond", "OffNormalAlarm", severity: 300);
+ nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alarm-3", equipmentId, "Cond", "OffNormalAlarm", severity: 300, realm: AddressSpaceRealm.Uns);
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
var details = new ReadEventDetails
@@ -221,8 +222,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
// Materialise the alarm while the source is still the Null default — the folder is promoted to
// SubscribeToEvents but DOES NOT get the HistoryRead bit / source registration.
const string equipmentId = "eq-nosrc";
- nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("alarm-4", equipmentId, "Cond", "OffNormalAlarm", severity: 200);
+ nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alarm-4", equipmentId, "Cond", "OffNormalAlarm", severity: 200, realm: AddressSpaceRealm.Uns);
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
// Wire a real source AFTER promotion — it must NOT retroactively make the folder a source.
@@ -271,7 +272,7 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
// A historized variable node — has AccessLevel.HistoryRead (variable-history reads) but
// EventNotifier=None (no event-notifier bit). The SDK base rejects it before our override runs.
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp", dataType: "Float",
- writable: false, historianTagname: "WW.Temp");
+ writable: false, historianTagname: "WW.Temp", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/temp")!.NodeId;
var details = new ReadEventDetails
@@ -303,8 +304,8 @@ public sealed class NodeManagerHistoryReadEventsTests : IDisposable
nm.HistorianDataSource = fake;
const string equipmentId = "eq-boom";
- nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment");
- nm.MaterialiseAlarmCondition("alarm-5", equipmentId, "Cond", "OffNormalAlarm", severity: 900);
+ nm.EnsureFolder(equipmentId, parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alarm-5", equipmentId, "Cond", "OffNormalAlarm", severity: 900, realm: AddressSpaceRealm.Uns);
var notifierNodeId = nm.TryGetFolder(equipmentId)!.NodeId;
var details = new ReadEventDetails
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadPagingTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadPagingTests.cs
index 16ddb42e..16d8bf29 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadPagingTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadPagingTests.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Opc.Ua;
using Opc.Ua.Server;
using Shouldly;
@@ -42,7 +43,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
nm.HistorianDataSource = new SeriesHistorianDataSource(series);
nm.EnsureVariable("eq-1/pv", parentFolderNodeId: null, displayName: "PV", dataType: "Double",
- writable: false, historianTagname: "WW.PV");
+ writable: false, historianTagname: "WW.PV", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/pv")!.NodeId;
var collected = new List();
@@ -84,7 +85,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
nm.HistorianDataSource = new SeriesHistorianDataSource(MakeSeries(count: 100, stepSeconds: 1));
nm.EnsureVariable("eq-1/exact", parentFolderNodeId: null, displayName: "Exact", dataType: "Double",
- writable: false, historianTagname: "WW.Exact");
+ writable: false, historianTagname: "WW.Exact", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/exact")!.NodeId;
var (r1, _, cp1) = ReadRaw(nm, nodeId, Epoch, Epoch.AddHours(1), max: 100, inboundCp: null);
@@ -123,7 +124,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
nm.HistorianDataSource = new SeriesHistorianDataSource(series);
nm.EnsureVariable("eq-1/tie", parentFolderNodeId: null, displayName: "Tie", dataType: "Double",
- writable: false, historianTagname: "WW.Tie");
+ writable: false, historianTagname: "WW.Tie", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/tie")!.NodeId;
var collected = new List();
@@ -167,7 +168,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
nm.HistorianDataSource = new SeriesHistorianDataSource(series);
nm.EnsureVariable("eq-1/burst", parentFolderNodeId: null, displayName: "Burst", dataType: "Double",
- writable: false, historianTagname: "WW.Burst");
+ writable: false, historianTagname: "WW.Burst", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/burst")!.NodeId;
var collected = new List();
@@ -211,7 +212,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
nm.HistorianDataSource = new SeriesHistorianDataSource(series);
nm.EnsureVariable("eq-1/absurd", parentFolderNodeId: null, displayName: "Absurd", dataType: "Double",
- writable: false, historianTagname: "WW.Absurd");
+ writable: false, historianTagname: "WW.Absurd", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/absurd")!.NodeId;
// Page 1: a full page of the first 2 ties, with a continuation point.
@@ -241,7 +242,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
nm.HistorianDataSource = new SeriesHistorianDataSource(MakeSeries(count: 250, stepSeconds: 1));
nm.EnsureVariable("eq-1/all", parentFolderNodeId: null, displayName: "All", dataType: "Double",
- writable: false, historianTagname: "WW.All");
+ writable: false, historianTagname: "WW.All", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/all")!.NodeId;
var (r, e, cp) = ReadRaw(nm, nodeId, Epoch, Epoch.AddHours(1), max: 0, inboundCp: null);
@@ -265,7 +266,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/bad-cp", parentFolderNodeId: null, displayName: "BadCp", dataType: "Double",
- writable: false, historianTagname: "WW.BadCp");
+ writable: false, historianTagname: "WW.BadCp", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/bad-cp")!.NodeId;
fake.ResetReadCount();
@@ -291,7 +292,7 @@ public sealed class NodeManagerHistoryReadPagingTests : IDisposable
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/rel", parentFolderNodeId: null, displayName: "Rel", dataType: "Double",
- writable: false, historianTagname: "WW.Rel");
+ writable: false, historianTagname: "WW.Rel", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/rel")!.NodeId;
// Page 1 — get a CP.
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadTests.cs
index 48ee37f5..12b4b9d8 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerHistoryReadTests.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Opc.Ua;
using Opc.Ua.Server;
using Shouldly;
@@ -39,7 +40,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp", dataType: "Float",
- writable: false, historianTagname: "WW.Temp");
+ writable: false, historianTagname: "WW.Temp", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/temp")!.NodeId;
var src = DateTime.UtcNow.AddSeconds(-5);
@@ -91,7 +92,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
// Node id "eq-9/flow" but a DISTINCT historian tagname "Plant.Flow.PV".
nm.EnsureVariable("eq-9/flow", parentFolderNodeId: null, displayName: "Flow", dataType: "Double",
- writable: false, historianTagname: "Plant.Flow.PV");
+ writable: false, historianTagname: "Plant.Flow.PV", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-9/flow")!.NodeId;
var details = new ReadRawModifiedDetails
@@ -123,7 +124,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/empty", parentFolderNodeId: null, displayName: "Empty", dataType: "Float",
- writable: false, historianTagname: "WW.Empty");
+ writable: false, historianTagname: "WW.Empty", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/empty")!.NodeId;
var details = new ReadRawModifiedDetails
@@ -156,7 +157,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
// Plain (non-historized) variable — no HistoryRead access bit.
nm.EnsureVariable("eq-1/plain", parentFolderNodeId: null, displayName: "Plain", dataType: "Int32",
- writable: false, historianTagname: null);
+ writable: false, historianTagname: null, realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/plain")!.NodeId;
var details = new ReadRawModifiedDetails
@@ -185,7 +186,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/mod", parentFolderNodeId: null, displayName: "Mod", dataType: "Float",
- writable: false, historianTagname: "WW.Mod");
+ writable: false, historianTagname: "WW.Mod", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/mod")!.NodeId;
var details = new ReadRawModifiedDetails
@@ -215,7 +216,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/avg", parentFolderNodeId: null, displayName: "Avg", dataType: "Float",
- writable: false, historianTagname: "WW.Avg");
+ writable: false, historianTagname: "WW.Avg", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/avg")!.NodeId;
var details = new ReadProcessedDetails
@@ -247,7 +248,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/sd", parentFolderNodeId: null, displayName: "Sd", dataType: "Float",
- writable: false, historianTagname: "WW.Sd");
+ writable: false, historianTagname: "WW.Sd", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/sd")!.NodeId;
var details = new ReadProcessedDetails
@@ -277,7 +278,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/at", parentFolderNodeId: null, displayName: "At", dataType: "Float",
- writable: false, historianTagname: "WW.At");
+ writable: false, historianTagname: "WW.At", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/at")!.NodeId;
var t1 = DateTime.UtcNow.AddMinutes(-2);
@@ -309,9 +310,9 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
// Materialise two distinct historized variables under separate equipment folders.
nm.EnsureVariable("eqA/good", parentFolderNodeId: null, displayName: "Good", dataType: "Float",
- writable: false, historianTagname: "A.PV");
+ writable: false, historianTagname: "A.PV", realm: AddressSpaceRealm.Uns);
nm.EnsureVariable("eqB/bad", parentFolderNodeId: null, displayName: "Bad", dataType: "Float",
- writable: false, historianTagname: "B.PV");
+ writable: false, historianTagname: "B.PV", realm: AddressSpaceRealm.Uns);
var goodNodeId = nm.TryGetVariable("eqA/good")!.NodeId;
var badNodeId = nm.TryGetVariable("eqB/bad")!.NodeId;
@@ -368,7 +369,7 @@ public sealed class NodeManagerHistoryReadTests : IDisposable
nm.HistorianDataSource = fake;
nm.EnsureVariable("eq-1/boom", parentFolderNodeId: null, displayName: "Boom", dataType: "Float",
- writable: false, historianTagname: "WW.Boom");
+ writable: false, historianTagname: "WW.Boom", realm: AddressSpaceRealm.Uns);
var nodeId = nm.TryGetVariable("eq-1/boom")!.NodeId;
var details = new ReadRawModifiedDetails
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeOnAddTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeOnAddTests.cs
index 7b942b55..085112e4 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeOnAddTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerModelChangeOnAddTests.cs
@@ -43,8 +43,8 @@ public sealed class NodeManagerModelChangeOnAddTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq-7", parentNodeId: null, displayName: "Equipment 7");
- nm.EnsureVariable("eq-7/speed", parentFolderNodeId: "eq-7", displayName: "Speed", dataType: "Float", writable: false);
+ nm.EnsureFolder("eq-7", parentNodeId: null, displayName: "Equipment 7", realm: AddressSpaceRealm.Uns);
+ nm.EnsureVariable("eq-7/speed", parentFolderNodeId: "eq-7", displayName: "Speed", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
var parent = nm.TryGetFolder("eq-7")!;
var e = nm.BuildNodesAddedModelChange("eq-7");
@@ -95,13 +95,13 @@ public sealed class NodeManagerModelChangeOnAddTests : IDisposable
var nm = server.NodeManager!;
// Before any nodes exist under the parent — must not throw.
- Should.NotThrow(() => nm.RaiseNodesAddedModelChange("eq-9"));
+ Should.NotThrow(() => nm.RaiseNodesAddedModelChange("eq-9", realm: AddressSpaceRealm.Uns));
- nm.EnsureFolder("eq-9", parentNodeId: null, displayName: "Equipment 9");
- nm.EnsureVariable("eq-9/temp", parentFolderNodeId: "eq-9", displayName: "Temp", dataType: "Float", writable: false);
+ nm.EnsureFolder("eq-9", parentNodeId: null, displayName: "Equipment 9", realm: AddressSpaceRealm.Uns);
+ nm.EnsureVariable("eq-9/temp", parentFolderNodeId: "eq-9", displayName: "Temp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
// After the nodes are materialised — still must not throw.
- Should.NotThrow(() => nm.RaiseNodesAddedModelChange("eq-9"));
+ Should.NotThrow(() => nm.RaiseNodesAddedModelChange("eq-9", realm: AddressSpaceRealm.Uns));
await host.DisposeAsync();
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerPreStartGuardTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerPreStartGuardTests.cs
index 1f2490fd..2c8e5bcf 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerPreStartGuardTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerPreStartGuardTests.cs
@@ -38,7 +38,7 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
try
{
var ex = Should.Throw(() =>
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment"));
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment", realm: AddressSpaceRealm.Uns));
ex.Message.ShouldContain("address space has not been created");
}
finally
@@ -55,7 +55,7 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
{
Should.Throw(() =>
nm.EnsureVariable("eq-1/temp", parentFolderNodeId: null, displayName: "Temp",
- dataType: "Float", writable: false));
+ dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns));
}
finally
{
@@ -70,7 +70,7 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
try
{
Should.Throw(() =>
- nm.WriteValue("eq-1/temp", 1.0, OpcUaQuality.Good, DateTime.UtcNow));
+ nm.WriteValue("eq-1/temp", 1.0, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns));
}
finally
{
@@ -85,7 +85,7 @@ public sealed class NodeManagerPreStartGuardTests : IDisposable
try
{
Should.Throw(() =>
- nm.MaterialiseAlarmCondition("alarm-1", "eq-1", "Cond", "OffNormalAlarm", severity: 500));
+ nm.MaterialiseAlarmCondition("alarm-1", "eq-1", "Cond", "OffNormalAlarm", severity: 500, realm: AddressSpaceRealm.Uns));
}
finally
{
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalRemoveTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalRemoveTests.cs
index fd225779..b064ddf0 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalRemoveTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalRemoveTests.cs
@@ -1,4 +1,5 @@
using Opc.Ua;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Shouldly;
using Xunit;
@@ -31,13 +32,13 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
{
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
- nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A");
- nm.EnsureVariable("eq-1/B", "eq-1", "B", "Float", writable: false);
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
+ nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A", realm: AddressSpaceRealm.Uns);
+ nm.EnsureVariable("eq-1/B", "eq-1", "B", "Float", writable: false, realm: AddressSpaceRealm.Uns);
var countBefore = nm.VariableCount;
nm.TryGetHistorizedTagname("eq-1/A", out _).ShouldBeTrue();
- nm.RemoveVariableNode("eq-1/A").ShouldBeTrue();
+ nm.RemoveVariableNode("eq-1/A", realm: AddressSpaceRealm.Uns).ShouldBeTrue();
nm.TryGetVariable("eq-1/A").ShouldBeNull();
nm.VariableCount.ShouldBe(countBefore - 1);
@@ -56,7 +57,7 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.RemoveVariableNode("eq-1/nope").ShouldBeFalse();
+ nm.RemoveVariableNode("eq-1/nope", realm: AddressSpaceRealm.Uns).ShouldBeFalse();
await host.DisposeAsync();
}
@@ -68,8 +69,8 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
{
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
- nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false);
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
+ nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, realm: AddressSpaceRealm.Uns);
var e = nm.BuildNodesRemovedModelChange("eq-1/A");
@@ -92,17 +93,17 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
{
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
- nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true);
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
nm.TryGetAlarmCondition("eq-1/OverTemp").ShouldNotBeNull();
nm.IsNativeAlarmNode("eq-1/OverTemp").ShouldBeTrue();
- nm.RemoveAlarmConditionNode("eq-1/OverTemp").ShouldBeTrue();
+ nm.RemoveAlarmConditionNode("eq-1/OverTemp", realm: AddressSpaceRealm.Uns).ShouldBeTrue();
nm.TryGetAlarmCondition("eq-1/OverTemp").ShouldBeNull();
nm.IsNativeAlarmNode("eq-1/OverTemp").ShouldBeFalse();
- nm.RemoveAlarmConditionNode("eq-1/OverTemp").ShouldBeFalse(); // already gone ⇒ false
+ nm.RemoveAlarmConditionNode("eq-1/OverTemp", realm: AddressSpaceRealm.Uns).ShouldBeFalse(); // already gone ⇒ false
await host.DisposeAsync();
}
@@ -114,10 +115,10 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
{
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
- nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 500, isNative: false);
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("alm-1", "eq-1", "HighTemp", "OffNormalAlarm", 500, isNative: false, realm: AddressSpaceRealm.Uns);
- nm.RemoveAlarmConditionNode("alm-1").ShouldBeTrue();
+ nm.RemoveAlarmConditionNode("alm-1", realm: AddressSpaceRealm.Uns).ShouldBeTrue();
nm.TryGetAlarmCondition("alm-1").ShouldBeNull();
await host.DisposeAsync();
@@ -136,17 +137,17 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
var nm = server.NodeManager!;
// Target equipment eq-1 with a sub-folder, two variables (one historized), and a native condition.
- nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1");
- nm.EnsureFolder("eq-1/Diag", parentNodeId: "eq-1", displayName: "Diag");
- nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A");
- nm.EnsureVariable("eq-1/Diag/T", "eq-1/Diag", "T", "Float", writable: false);
- nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true);
+ nm.EnsureFolder("eq-1", parentNodeId: null, displayName: "Equipment 1", realm: AddressSpaceRealm.Uns);
+ nm.EnsureFolder("eq-1/Diag", parentNodeId: "eq-1", displayName: "Diag", realm: AddressSpaceRealm.Uns);
+ nm.EnsureVariable("eq-1/A", "eq-1", "A", "Float", writable: false, historianTagname: "Hist.A", realm: AddressSpaceRealm.Uns);
+ nm.EnsureVariable("eq-1/Diag/T", "eq-1/Diag", "T", "Float", writable: false, realm: AddressSpaceRealm.Uns);
+ nm.MaterialiseAlarmCondition("eq-1/OverTemp", "eq-1", "OverTemp", "OffNormalAlarm", 700, isNative: true, realm: AddressSpaceRealm.Uns);
// Sibling equipment eq-2 that must survive untouched.
- nm.EnsureFolder("eq-2", parentNodeId: null, displayName: "Equipment 2");
- nm.EnsureVariable("eq-2/S", "eq-2", "S", "Float", writable: false);
+ nm.EnsureFolder("eq-2", parentNodeId: null, displayName: "Equipment 2", realm: AddressSpaceRealm.Uns);
+ nm.EnsureVariable("eq-2/S", "eq-2", "S", "Float", writable: false, realm: AddressSpaceRealm.Uns);
- nm.RemoveEquipmentSubtree("eq-1").ShouldBeTrue();
+ nm.RemoveEquipmentSubtree("eq-1", realm: AddressSpaceRealm.Uns).ShouldBeTrue();
// Every eq-1 descendant is gone from every map.
nm.TryGetFolder("eq-1").ShouldBeNull();
@@ -163,7 +164,7 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
// Re-materialising an alarm under eq-2 still works (the notifier machinery was not corrupted by the
// eq-1 demotion) — proves no orphaned root-notifier ref broke the event path.
- Should.NotThrow(() => nm.MaterialiseAlarmCondition("eq-2/Alm", "eq-2", "Alm", "OffNormalAlarm", 300, isNative: false));
+ Should.NotThrow(() => nm.MaterialiseAlarmCondition("eq-2/Alm", "eq-2", "Alm", "OffNormalAlarm", 300, isNative: false, realm: AddressSpaceRealm.Uns));
await host.DisposeAsync();
}
@@ -176,7 +177,7 @@ public sealed class NodeManagerSurgicalRemoveTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.RemoveEquipmentSubtree("eq-nope").ShouldBeFalse();
+ nm.RemoveEquipmentSubtree("eq-nope", realm: AddressSpaceRealm.Uns).ShouldBeFalse();
await host.DisposeAsync();
}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalShapeUpdateTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalShapeUpdateTests.cs
index d064246c..9205bf10 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalShapeUpdateTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerSurgicalShapeUpdateTests.cs
@@ -44,13 +44,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false);
- nm.WriteValue("eq-1/sp", 3.5f, OpcUaQuality.Good, DateTime.UtcNow);
+ nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
+ nm.WriteValue("eq-1/sp", 3.5f, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/sp")!;
node.DataType.ShouldBe(DataTypeIds.Float); // arrange guard
var applied = nm.UpdateTagAttributes("eq-1/sp", writable: false, historianTagname: null,
- dataType: "Int32", isArray: false, arrayLength: null);
+ dataType: "Int32", isArray: false, arrayLength: null, realm: AddressSpaceRealm.Uns);
applied.ShouldBeTrue();
node.DataType.ShouldBe(DataTypeIds.Int32); // swapped in place
@@ -69,13 +69,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16", writable: false);
- nm.WriteValue("eq-1/buf", (short)42, OpcUaQuality.Good, DateTime.UtcNow);
+ nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16", writable: false, realm: AddressSpaceRealm.Uns);
+ nm.WriteValue("eq-1/buf", (short)42, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/buf")!;
node.ValueRank.ShouldBe(ValueRanks.Scalar); // arrange guard
var applied = nm.UpdateTagAttributes("eq-1/buf", writable: false, historianTagname: null,
- dataType: "Int16", isArray: true, arrayLength: 8u);
+ dataType: "Int16", isArray: true, arrayLength: 8u, realm: AddressSpaceRealm.Uns);
applied.ShouldBeTrue();
node.ValueRank.ShouldBe(ValueRanks.OneDimension);
@@ -96,13 +96,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
var nm = server.NodeManager!;
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16",
- writable: false, historianTagname: null, isArray: true, arrayLength: 4u);
- nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow);
+ writable: false, historianTagname: null, isArray: true, arrayLength: 4u, realm: AddressSpaceRealm.Uns);
+ nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/buf")!;
node.ArrayDimensions![0].ShouldBe(4u); // arrange guard
var applied = nm.UpdateTagAttributes("eq-1/buf", writable: false, historianTagname: null,
- dataType: "Int16", isArray: true, arrayLength: 8u);
+ dataType: "Int16", isArray: true, arrayLength: 8u, realm: AddressSpaceRealm.Uns);
applied.ShouldBeTrue();
node.ArrayDimensions[0].ShouldBe(8u);
@@ -121,13 +121,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
var nm = server.NodeManager!;
nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16",
- writable: false, historianTagname: null, isArray: true, arrayLength: 4u);
- nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow);
+ writable: false, historianTagname: null, isArray: true, arrayLength: 4u, realm: AddressSpaceRealm.Uns);
+ nm.WriteValue("eq-1/buf", new short[] { 1, 2, 3, 4 }, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/buf")!;
node.ValueRank.ShouldBe(ValueRanks.OneDimension); // arrange guard
var applied = nm.UpdateTagAttributes("eq-1/buf", writable: false, historianTagname: null,
- dataType: "Int16", isArray: false, arrayLength: null);
+ dataType: "Int16", isArray: false, arrayLength: null, realm: AddressSpaceRealm.Uns);
applied.ShouldBeTrue();
node.ValueRank.ShouldBe(ValueRanks.Scalar);
@@ -146,11 +146,11 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16", writable: false);
+ nm.EnsureVariable("eq-1/buf", parentFolderNodeId: null, displayName: "Buf", dataType: "Int16", writable: false, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/buf")!;
var applied = nm.UpdateTagAttributes("eq-1/buf", writable: false, historianTagname: null,
- dataType: "Int16", isArray: true, arrayLength: null);
+ dataType: "Int16", isArray: true, arrayLength: null, realm: AddressSpaceRealm.Uns);
applied.ShouldBeTrue();
node.ValueRank.ShouldBe(ValueRanks.OneDimension);
@@ -171,13 +171,13 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false);
- nm.WriteValue("eq-1/sp", 7.0f, OpcUaQuality.Good, DateTime.UtcNow);
+ nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
+ nm.WriteValue("eq-1/sp", 7.0f, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/sp")!;
// Same DataType ("Float") + same scalar shape — only Writable flips false → true.
var applied = nm.UpdateTagAttributes("eq-1/sp", writable: true, historianTagname: null,
- dataType: "Float", isArray: false, arrayLength: null);
+ dataType: "Float", isArray: false, arrayLength: null, realm: AddressSpaceRealm.Uns);
applied.ShouldBeTrue();
node.Value.ShouldBe(7.0f); // value preserved (NOT reset)
@@ -199,7 +199,7 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
bool result = true;
Should.NotThrow(() => result = nm.UpdateTagAttributes("eq-1/gone", writable: false, historianTagname: null,
- dataType: "Int32", isArray: false, arrayLength: null));
+ dataType: "Int32", isArray: false, arrayLength: null, realm: AddressSpaceRealm.Uns));
result.ShouldBeFalse();
await host.DisposeAsync();
@@ -215,7 +215,7 @@ public sealed class NodeManagerSurgicalShapeUpdateTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false);
+ nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Float", writable: false, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/sp")!;
var e = nm.BuildNodeShapeChangedEvent(node);
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerWriteRevertTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerWriteRevertTests.cs
index 264a67dd..303b8827 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerWriteRevertTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/NodeManagerWriteRevertTests.cs
@@ -50,9 +50,9 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
+ nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
// Prior state = Good 7; the SDK then optimistically applied 42 (the write the device will reject).
- nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
+ nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/sp")!;
node.Value = 42; // simulate the SDK's optimistic apply of the rejected write
@@ -62,7 +62,7 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
optimisticValue: 42,
priorValue: 7,
priorStatus: StatusCodes.Good,
- clientUserId: "op");
+ clientUserId: "op", realm: AddressSpaceRealm.Uns);
node.Value.ShouldBe(7); // settled back to prior value
node.StatusCode.ShouldBe((StatusCode)StatusCodes.Good); // settled back to prior status (NOT stuck Bad)
@@ -78,15 +78,15 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
- nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
+ nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
+ nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/sp")!;
node.Value = 42;
nm.RevertOptimisticWriteIfNeeded(
"eq-1/sp",
outcome: new NodeWriteOutcome(true, null),
- optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op");
+ optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op", realm: AddressSpaceRealm.Uns);
node.Value.ShouldBe(42); // success ⇒ optimistic value stands
@@ -101,15 +101,15 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
- nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
+ nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
+ nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/sp")!;
node.Value = 99; // a fresh poll already republished the confirmed register value (NOT the optimistic 42)
nm.RevertOptimisticWriteIfNeeded(
"eq-1/sp",
outcome: new NodeWriteOutcome(false, "device rejected"),
- optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op");
+ optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op", realm: AddressSpaceRealm.Uns);
node.Value.ShouldBe(99); // poll value preserved — not reverted to 7
@@ -127,7 +127,71 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
Should.NotThrow(() => nm.RevertOptimisticWriteIfNeeded(
"eq-1/gone",
outcome: new NodeWriteOutcome(false, "device rejected"),
- optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op"));
+ optimisticValue: 42, priorValue: 7, priorStatus: StatusCodes.Good, clientUserId: "op", realm: AddressSpaceRealm.Uns));
+
+ await host.DisposeAsync();
+ }
+
+ // ─────────────────── v3 Batch 4 (review M2) — realm-qualified dual-node self-correction ───────────────────
+
+ /// (M2a) A failed write driven through a UNS NodeId reverts ONLY the UNS node back to its prior
+ /// value; the raw node that shares the SAME bare id (but lives in the Raw realm) is NEVER touched and keeps
+ /// its driver value. The revert routes by (realm, bareId), so it can't clobber the sibling node.
+ [Fact]
+ public async Task Failed_uns_write_reverts_uns_node_and_leaves_raw_node_untouched()
+ {
+ var (host, server) = await BootAsync();
+ var nm = server.NodeManager!;
+
+ // Same bare id "A/B/C/D" in BOTH realms — the raw device node and the UNS reference node.
+ nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Raw", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Raw);
+ nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Uns", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
+ var now = DateTime.UtcNow;
+ nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Raw); // raw driver value
+ nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Uns); // fanned UNS value
+ var raw = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Raw)!;
+ var uns = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Uns)!;
+
+ uns.Value = 42; // the SDK optimistically applied the client's UNS write
+
+ nm.RevertOptimisticWriteIfNeeded(
+ "A/B/C/D", outcome: new NodeWriteOutcome(false, "device rejected"),
+ optimisticValue: 42, priorValue: 10, priorStatus: StatusCodes.Good, clientUserId: "op",
+ realm: AddressSpaceRealm.Uns);
+
+ uns.Value.ShouldBe(10); // UNS node reverted to prior
+ raw.Value.ShouldBe(10); // raw node NEVER changed (realm-qualified revert)
+
+ await host.DisposeAsync();
+ }
+
+ /// (M2b) A raw-realm revert reverts the RAW node and leaves the same-bare-id UNS node untouched.
+ /// This locks the realm arg: were it omitted (defaulting to Uns) the revert would target the UNS node — whose
+ /// value is not the optimistic one, so the still-holds-optimistic guard skips it — leaving the raw node stuck
+ /// at the rejected value and FAILING the raw-reverts assertion below.
+ [Fact]
+ public async Task Raw_realm_revert_reverts_raw_node_only()
+ {
+ var (host, server) = await BootAsync();
+ var nm = server.NodeManager!;
+
+ nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Raw", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Raw);
+ nm.EnsureVariable("A/B/C/D", parentFolderNodeId: null, displayName: "Uns", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
+ var now = DateTime.UtcNow;
+ nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Raw);
+ nm.WriteValue("A/B/C/D", 10, OpcUaQuality.Good, now, AddressSpaceRealm.Uns);
+ var raw = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Raw)!;
+ var uns = nm.TryGetVariable("A/B/C/D", AddressSpaceRealm.Uns)!;
+
+ raw.Value = 42; // optimistic write to the RAW node
+
+ nm.RevertOptimisticWriteIfNeeded(
+ "A/B/C/D", outcome: new NodeWriteOutcome(false, "device rejected"),
+ optimisticValue: 42, priorValue: 10, priorStatus: StatusCodes.Good, clientUserId: "op",
+ realm: AddressSpaceRealm.Raw);
+
+ raw.Value.ShouldBe(10); // raw reverted (would stay 42 if the realm were dropped to Uns)
+ uns.Value.ShouldBe(10); // UNS node untouched
await host.DisposeAsync();
}
@@ -143,8 +207,8 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
- nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
+ nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
+ nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/sp")!;
var audit = nm.BuildWriteFailureAuditEvent(
@@ -174,8 +238,8 @@ public sealed class NodeManagerWriteRevertTests : IDisposable
var (host, server) = await BootAsync();
var nm = server.NodeManager!;
- nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true);
- nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow);
+ nm.EnsureVariable("eq-1/sp", parentFolderNodeId: null, displayName: "Sp", dataType: "Int32", writable: true, realm: AddressSpaceRealm.Uns);
+ nm.WriteValue("eq-1/sp", 7, OpcUaQuality.Good, DateTime.UtcNow, realm: AddressSpaceRealm.Uns);
var node = nm.TryGetVariable("eq-1/sp")!;
var audit = nm.BuildWriteFailureAuditEvent(
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/ActorNodeWriteGatewayTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/ActorNodeWriteGatewayTests.cs
index d70484bc..c168cf88 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/ActorNodeWriteGatewayTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/ActorNodeWriteGatewayTests.cs
@@ -1,6 +1,7 @@
using Akka.Actor;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
@@ -24,7 +25,7 @@ public sealed class ActorNodeWriteGatewayTests : RuntimeActorTestBase
var probe = CreateTestProbe();
var gateway = new ActorNodeWriteGateway(resolveDriverHost: () => probe.Ref, NullLogger.Instance);
- var writeTask = gateway.WriteAsync("eq-1/speed", 123.0, CancellationToken.None);
+ var writeTask = gateway.WriteAsync("eq-1/speed", 123.0, AddressSpaceRealm.Uns, CancellationToken.None);
var routed = probe.ExpectMsg(TimeSpan.FromSeconds(5));
routed.NodeId.ShouldBe("eq-1/speed");
@@ -43,7 +44,7 @@ public sealed class ActorNodeWriteGatewayTests : RuntimeActorTestBase
var probe = CreateTestProbe();
var gateway = new ActorNodeWriteGateway(resolveDriverHost: () => probe.Ref, NullLogger.Instance);
- var writeTask = gateway.WriteAsync("eq-1/speed", 123.0, CancellationToken.None);
+ var writeTask = gateway.WriteAsync("eq-1/speed", 123.0, AddressSpaceRealm.Uns, CancellationToken.None);
probe.ExpectMsg(TimeSpan.FromSeconds(5));
probe.Reply(new DriverHostActor.NodeWriteResult(false, "not primary"));
@@ -63,7 +64,7 @@ public sealed class ActorNodeWriteGatewayTests : RuntimeActorTestBase
resolveDriverHost: () => probe.Ref, NullLogger.Instance,
askTimeout: TimeSpan.FromMilliseconds(200));
- var outcome = await gateway.WriteAsync("eq-1/speed", 123.0, CancellationToken.None);
+ var outcome = await gateway.WriteAsync("eq-1/speed", 123.0, AddressSpaceRealm.Uns, CancellationToken.None);
outcome.Success.ShouldBeFalse();
outcome.Reason.ShouldNotBeNull();
@@ -77,7 +78,7 @@ public sealed class ActorNodeWriteGatewayTests : RuntimeActorTestBase
{
var gateway = new ActorNodeWriteGateway(resolveDriverHost: () => null, NullLogger.Instance);
- var outcome = await gateway.WriteAsync("eq-1/speed", 123.0, CancellationToken.None);
+ var outcome = await gateway.WriteAsync("eq-1/speed", 123.0, AddressSpaceRealm.Uns, CancellationToken.None);
outcome.Success.ShouldBeFalse();
outcome.Reason.ShouldBe("writes unavailable");
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DarkAddressSpaceReasons.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DarkAddressSpaceReasons.cs
index cd44ab42..c8d5ca33 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DarkAddressSpaceReasons.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DarkAddressSpaceReasons.cs
@@ -18,6 +18,18 @@ internal static class DarkAddressSpaceReasons
"empty equipment-tag plan set this batch, so this parity has nothing to compare. Raw-tag parse " +
"intent is covered by TagConfigCorpusParityTests (RawTagEntry round-trip) + TagConfigIntentTests.";
+ /// Discovered-node INJECTION is dormant in v3 (Wave-B review M1). The v2 path grafted a driver's
+ /// FixedTree under an equipment folder (equipment bound a driver); v3 retired that binding and discovered
+ /// raw tags are authored explicitly via the Batch-2 /raw browse-commit flow. HandleDiscoveredNodes
+ /// hard-short-circuits, so these v2 injection scenarios have no live path to assert; re-migrating injection
+ /// onto the raw device subtree is a separate follow-up. The dormant guard itself is pinned by
+ /// DriverHostActorDiscoveryTests.Discovered_nodes_are_ignored_dormant_in_v3.
+ public const string DiscoveryInjectionDormantV3 =
+ "v3: discovered-node injection is DORMANT (Wave-B review M1) — equipment no longer binds a driver, so the " +
+ "v2 FixedTree-under-equipment graft has no live path. Discovered raw tags are authored via the /raw " +
+ "browse-commit flow. HandleDiscoveredNodes short-circuits (pinned by Discovered_nodes_are_ignored_dormant_in_v3); " +
+ "re-migrating injection onto the raw device subtree is a separate follow-up.";
+
/// Equipment↔device host binding is architecturally retired in v3.
public const string EquipmentDeviceBindingRetired =
"v3: equipment no longer binds a driver/device (EquipmentNode has no DriverInstanceId/DeviceId/" +
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactRawUnsParityTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactRawUnsParityTests.cs
new file mode 100644
index 00000000..fe2cc5ce
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DeploymentArtifactRawUnsParityTests.cs
@@ -0,0 +1,84 @@
+using System.Text.Json;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+using ZB.MOM.WW.OtOpcUa.OpcUaServer;
+using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
+
+///
+/// v3 Batch 4 (Wave-B review recommendation) — the artifact-decode seam
+/// () and the live compose seam
+/// () must produce BYTE-IDENTICAL Raw and UNS node
+/// sets for the same seeded config. This is the guard that a future artifact-decode drift can't silently
+/// diverge the deployed dual-namespace address space from what was authored/validated. The existing
+/// DeploymentArtifactEquipRefParityTests covers {{equip}} script-path parity; this covers the
+/// RawContainers / RawTags / UnsReferenceVariables sets (RawPaths, UNS NodeIds, Organizes backing paths,
+/// writable + historian + array shape).
+///
+public sealed class DeploymentArtifactRawUnsParityTests
+{
+ [Fact]
+ public void Raw_and_uns_node_sets_are_byte_parity_between_compose_and_artifact_decode()
+ {
+ // Entity-side config: folder → driver → device → group → 2 tags (one historized+writable+override,
+ // one plain read-only), an area/line/equipment, and a UNS reference with a display-name override.
+ var folder = new RawFolder { RawFolderId = "fld-1", ClusterId = "c1", Name = "Cell1", ParentRawFolderId = null };
+ var driver = new DriverInstance { DriverInstanceId = "drv-1", ClusterId = "c1", Name = "Modbus", DriverType = "Modbus", DriverConfig = "{}", RawFolderId = "fld-1" };
+ var device = new Device { DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}" };
+ var group = new TagGroup { TagGroupId = "grp-1", DeviceId = "dev-1", Name = "Fast", ParentTagGroupId = null };
+ var speed = new Tag
+ {
+ TagId = "tag-speed", DeviceId = "dev-1", TagGroupId = "grp-1", Name = "Speed", DataType = "Float",
+ AccessLevel = TagAccessLevel.ReadWrite, TagConfig = "{\"isHistorized\":true,\"historianTagname\":\"WW.Speed\"}",
+ };
+ var run = new Tag
+ {
+ TagId = "tag-run", DeviceId = "dev-1", TagGroupId = null, Name = "Run", DataType = "Boolean",
+ AccessLevel = TagAccessLevel.Read, TagConfig = "{}",
+ };
+ var area = new UnsArea { UnsAreaId = "a1", ClusterId = "c1", Name = "filling" };
+ var line = new UnsLine { UnsLineId = "l1", UnsAreaId = "a1", Name = "line1" };
+ var equip = new Equipment { EquipmentId = "EQ-1", UnsLineId = "l1", Name = "station1", MachineCode = "M1" };
+ var reference = new UnsTagReference { UnsTagReferenceId = "ref-1", EquipmentId = "EQ-1", TagId = "tag-speed", DisplayNameOverride = "MotorSpeed" };
+
+ var composed = AddressSpaceComposer.Compose(
+ new[] { area }, new[] { line }, new[] { equip },
+ new[] { driver }, Array.Empty(),
+ unsTagReferences: new[] { reference }, tags: new[] { speed, run },
+ rawFolders: new[] { folder }, devices: new[] { device }, tagGroups: new[] { group });
+
+ // Artifact JSON — the same shape ConfigComposer serializes (enums numeric: ReadWrite = 1, Read = 0).
+ var blob = JsonSerializer.SerializeToUtf8Bytes(new
+ {
+ RawFolders = new[] { new { RawFolderId = "fld-1", ParentRawFolderId = (string?)null, Name = "Cell1", ClusterId = "c1" } },
+ DriverInstances = new[] { new { DriverInstanceId = "drv-1", DriverType = "Modbus", DriverConfig = "{}", Name = "Modbus", RawFolderId = "fld-1", ClusterId = "c1" } },
+ Devices = new[] { new { DeviceId = "dev-1", DriverInstanceId = "drv-1", Name = "Dev1", DeviceConfig = "{}" } },
+ TagGroups = new[] { new { TagGroupId = "grp-1", ParentTagGroupId = (string?)null, DeviceId = "dev-1", Name = "Fast" } },
+ Tags = new object[]
+ {
+ new { TagId = "tag-speed", DeviceId = "dev-1", TagGroupId = "grp-1", Name = "Speed", DataType = "Float", AccessLevel = 1, TagConfig = "{\"isHistorized\":true,\"historianTagname\":\"WW.Speed\"}" },
+ new { TagId = "tag-run", DeviceId = "dev-1", TagGroupId = (string?)null, Name = "Run", DataType = "Boolean", AccessLevel = 0, TagConfig = "{}" },
+ },
+ UnsAreas = new[] { new { UnsAreaId = "a1", Name = "filling", ClusterId = "c1" } },
+ UnsLines = new[] { new { UnsLineId = "l1", UnsAreaId = "a1", Name = "line1" } },
+ Equipment = new[] { new { EquipmentId = "EQ-1", UnsLineId = "l1", Name = "station1", MachineCode = "M1" } },
+ UnsTagReferences = new[] { new { UnsTagReferenceId = "ref-1", EquipmentId = "EQ-1", TagId = "tag-speed", DisplayNameOverride = "MotorSpeed" } },
+ });
+
+ var decoded = DeploymentArtifact.ParseComposition(blob);
+
+ // Sanity: the sets are non-empty (both raw tags + the projected UNS variable materialised).
+ composed.RawTags.Count.ShouldBe(2);
+ composed.UnsReferenceVariables.Count.ShouldBe(1);
+ composed.UnsReferenceVariables[0].NodeId.ShouldBe("filling/line1/station1/MotorSpeed");
+ composed.UnsReferenceVariables[0].BackingRawPath.ShouldBe("Cell1/Modbus/Dev1/Fast/Speed");
+
+ // Byte-parity: the three dual-namespace node sets are record-equal between the two seams.
+ decoded.RawContainers.ShouldBe(composed.RawContainers);
+ decoded.RawTags.ShouldBe(composed.RawTags);
+ decoded.UnsReferenceVariables.ShouldBe(composed.UnsReferenceVariables);
+ }
+}
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 33509853..bdbeab4c 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
@@ -108,7 +108,7 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase
/// — proving the live value routed end-to-end and (in production)
/// overwrote the BadWaitingForInitialData seed.
///
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Discovered_node_materialises_at_equipment_and_polled_value_flows_Good()
{
var db = NewInMemoryDbFactory();
@@ -154,7 +154,7 @@ public sealed class DiscoveryInjectionEndToEndTests : RuntimeActorTestBase
/// (re-materialised after the rebuild) at the same NodeId, and a subsequent published value STILL
/// WriteValues Good there (the routing map was rebuilt, not left empty by the Clear()).
///
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Discovered_node_and_value_survive_a_redeploy()
{
var db = NewInMemoryDbFactory();
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorDiscoveryTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorDiscoveryTests.cs
index 085fee91..2054eb51 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorDiscoveryTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorDiscoveryTests.cs
@@ -49,12 +49,39 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(5);
private static readonly DateTime Ts = new(2026, 6, 26, 10, 0, 0, DateTimeKind.Utc);
+ /// v3 Batch 4 (review M1) — the ONE live dormancy pin: discovered-node injection is short-circuited.
+ /// A driver reports a FixedTree via , but the host must
+ /// NOT materialise anything (no reaches the publish
+ /// side) — discovered raw tags are authored via the /raw browse-commit flow, not injected at runtime.
+ /// The skipped v2 injection scenarios below are the counterpart of this guard.
+ [Fact]
+ public void Discovered_nodes_are_ignored_dormant_in_v3()
+ {
+ var db = NewInMemoryDbFactory();
+ var factory = new SubscribingDriverFactory("Modbus");
+ var deploymentId = SeedDeploymentWithEquipmentTags(db, RevA,
+ (Equip: "EQ-1", Driver: "d1", FullName: "40001", Folder: (string?)null, Name: "speed"));
+ var (actor, publish, _) = SpawnHostAndApply(db, deploymentId, factory);
+
+ actor.Tell(new DriverInstanceActor.DiscoveredNodesReady("d1", new[]
+ {
+ new DiscoveredNode(
+ FolderPathSegments: new[] { "FOCAS", "Identity" },
+ BrowseName: "Model", DisplayName: "Model", FullReference: "ft-ref-1",
+ DataType: DriverDataType.Float64, IsArray: false, ArrayDim: null,
+ Writable: false, IsHistorized: false),
+ }));
+
+ // Dormant: no discovered nodes are materialised (the guard short-circuits before any route/materialise).
+ publish.ExpectNoMsg(TimeSpan.FromMilliseconds(500));
+ }
+
/// A driver's discovered FixedTree (refs differing from the authored tag) is grafted under the
/// bound equipment: (a) the publish side receives
/// rooted at the equipment NodeId; (b) the driver re-subscribes the UNION of the authored ref + the
/// FixedTree refs; (c) a value published for a FixedTree ref now routes to its mapped NodeId (proving the
/// live-value routing map was extended).
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void DiscoveredNodes_materialise_extend_routing_and_merge_subscription()
{
var db = NewInMemoryDbFactory();
@@ -121,7 +148,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// rooted at its NodeId and the driver
/// subscribes the discovered refs (no authored ref exists to union). Previously this was skipped with
/// "no equipment/authored tags".
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Tag_less_equipment_resolved_via_EquipmentNode_grafts_discovered_nodes()
{
var db = NewInMemoryDbFactory();
@@ -198,7 +225,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// equipment), (b) the union subscription carries BOTH devices' refs, and (c) a value for each device's ref
/// routes to the right equipment's node (proving BOTH inner-map entries cached + keyed correctly). The "H1"
/// vs stored "h1" wrinkle proves the SHARED DeviceConfigIntent.NormalizeHost match.
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Multi_device_driver_partitions_fixed_tree_by_device_host_under_matching_equipment()
{
var db = NewInMemoryDbFactory();
@@ -264,7 +291,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// NO matching equipment is warn-skipped (NOT mis-grafted), while the matched partitions still graft. Here
/// d1 fans out to EQ-A("h1") + EQ-B("h2"), but a third discovered partition "h3" matches no equipment: only
/// EQ-A + EQ-B materialise (no third), and the unmatched ref routes nowhere (no crash).
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Multi_device_unmatched_device_host_is_warn_skipped_matched_still_graft()
{
var db = NewInMemoryDbFactory();
@@ -305,7 +332,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// logged at Debug, which the suite's loglevel = WARNING HOCON suppresses at source, so EventFilter
/// observes the dedup as "zero further matching warnings". (The matched EQ-A/EQ-B partitions still graft on
/// pass 1; pass 2's matched routing is short-circuited by PlansRoutingEqual.)
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Repeated_unmatched_device_host_partition_warns_once_then_is_quiet()
{
var db = NewInMemoryDbFactory();
@@ -378,7 +405,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// Dedup: a discovered node whose FullReference equals an authored equipment tag's
/// FullName is NOT injected (it would shadow the authored node) — only the genuinely-new FixedTree refs
/// are materialised.
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Discovered_node_shadowing_an_authored_ref_is_not_injected()
{
var db = NewInMemoryDbFactory();
@@ -413,7 +440,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// re-discovers each ~2s pass) is a no-op — it materialises ONCE and does not force the child to
/// re-subscribe. A GROWN set, however, DOES re-apply (materialise again + re-subscribe), so a stabilising
/// FixedTree still converges.
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Repeated_identical_discovery_does_not_reapply_but_a_grown_set_does()
{
var db = NewInMemoryDbFactory();
@@ -469,7 +496,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// FixedTree routes + materialised nodes would be lost until the driver reconnects. Asserts the cached
/// plan is (a) re-materialised under EQ-1 after the rebuild, (b) still present in the child's
/// post-redeploy subscription, and (c) still routes a published value to its mapped NodeId.
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Discovered_nodes_survive_a_redeploy_rebuild()
{
var db = NewInMemoryDbFactory();
@@ -536,7 +563,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// (its authored tags were removed) must DROP the cached discovered plan rather than re-graft it onto a
/// stale equipment. After such a redeploy the host re-applies the authored rebuild but does NOT re-tell
/// for the now-unresolved driver.
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Discovered_nodes_dropped_when_equipment_no_longer_resolves()
{
var db = NewInMemoryDbFactory();
@@ -578,7 +605,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// drops the entry. After the redeploy NO is
/// re-told. (Complements , which
/// covers the Count==0 branch; this covers the rebind/StartsWith branch.)
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Discovered_nodes_dropped_when_driver_rebound_to_a_different_equipment()
{
var db = NewInMemoryDbFactory();
@@ -627,7 +654,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// single Once pass — impossible without the trigger, since the child stays Connected so nothing else re-kicks
/// discovery) AND a fresh re-grafts the FixedTree
/// under the new equipment EQ-2.
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Config_unchanged_rebind_re_triggers_discovery_on_the_child()
{
var db = NewInMemoryDbFactory();
@@ -706,7 +733,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// guard — so they would NOT catch a future regression that sets droppedAny on a non-drop path). The
/// regression is observable here: a spurious trigger would advance DiscoverCount past the single Once
/// pass.
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void No_drop_redeploy_does_not_re_trigger_discovery()
{
var db = NewInMemoryDbFactory();
@@ -757,7 +784,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// SubscribeAsync — no de-dup in ): the count rises by EXACTLY 1 across the
/// redeploy and the final subscribed set is the union. (Pre-task: the bulk loop ALSO sent the authored-only
/// set first ⇒ the count rose by 2 and the set transiently dropped "ft-ref-1".)
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Cached_driver_survivor_redeploy_sends_exactly_one_union_subscription()
{
var db = NewInMemoryDbFactory();
@@ -823,7 +850,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// via SubscribeCount (+1) and the subscribed set ("40001" only, NOT the dropped "ft-ref-1"). (It also
/// gets a — a different message type the non-discovery
/// child no-ops, so it adds no subscribe.) Guards that the bulk-skip didn't reduce this path to ZERO sends.
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Cached_driver_fully_dropped_redeploy_sends_exactly_one_authored_only_fallback()
{
var db = NewInMemoryDbFactory();
@@ -885,7 +912,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// authored-only per redeploy (the re-inject tail
/// never runs for it). Guards that the skip didn't accidentally suppress (or double) a non-cached driver's
/// send. Observed via SubscribeCount (+1) and the subscribed set ("40001" only).
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Non_cached_driver_redeploy_sends_exactly_one_authored_only_subscription()
{
var db = NewInMemoryDbFactory();
@@ -934,7 +961,7 @@ public sealed class DriverHostActorDiscoveryTests : RuntimeActorTestBase
/// the Connected handler routes to Unsubscribe (dropping the stale FixedTree handle) — NOT a subscribe.
/// Proven by SubscribeCount staying FLAT across the redeploy (no spurious subscribe), closing the
/// SubscribeCount-proxy blind spot for the empty-set fallback.
- [Fact(Skip = DarkAddressSpaceReasons.EquipmentTagsDarkBatch4)]
+ [Fact(Skip = DarkAddressSpaceReasons.DiscoveryInjectionDormantV3)]
public void Cached_tag_less_driver_fully_dropped_redeploy_sends_empty_fallback_without_subscribing()
{
var db = NewInMemoryDbFactory();
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorPrimaryGateTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorPrimaryGateTests.cs
index cce53094..24f10784 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorPrimaryGateTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorPrimaryGateTests.cs
@@ -6,6 +6,7 @@ using Akka.Cluster.Tools.PublishSubscribe;
using Akka.TestKit;
using Microsoft.EntityFrameworkCore;
using Shouldly;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Alerts;
@@ -51,7 +52,7 @@ public sealed class DriverHostActorPrimaryGateTests : RuntimeActorTestBase
var actor = SpawnWriteHost(db, dep, recorder, driverMemberCount: 2);
var asker = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 123.0), asker.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 123.0, AddressSpaceRealm.Uns), asker.Ref);
var result = asker.ExpectMsg(Timeout);
result.Success.ShouldBeFalse();
@@ -75,7 +76,7 @@ public sealed class DriverHostActorPrimaryGateTests : RuntimeActorTestBase
AwaitAssert(() =>
{
var asker = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 123.0), asker.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 123.0, AddressSpaceRealm.Uns), asker.Ref);
var res = asker.ExpectMsg(Timeout);
res.Reason.ShouldNotBe("not primary");
res.Reason.ShouldNotBe("not primary (role unknown)");
@@ -100,7 +101,7 @@ public sealed class DriverHostActorPrimaryGateTests : RuntimeActorTestBase
AwaitAssert(() =>
{
var asker1 = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 1.0), asker1.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 1.0, AddressSpaceRealm.Uns), asker1.Ref);
var res = asker1.ExpectMsg(Timeout);
res.Reason.ShouldNotBe("not primary");
res.Success.ShouldBeTrue(res.Reason);
@@ -109,7 +110,7 @@ public sealed class DriverHostActorPrimaryGateTests : RuntimeActorTestBase
// Secondary snapshot ⇒ denied with the steady-state reason.
TellRole(actor, RedundancyRole.Secondary);
var asker2 = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 2.0), asker2.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 2.0, AddressSpaceRealm.Uns), asker2.Ref);
var denied = asker2.ExpectMsg(Timeout);
denied.Success.ShouldBeFalse();
denied.Reason.ShouldBe("not primary");
@@ -126,7 +127,7 @@ public sealed class DriverHostActorPrimaryGateTests : RuntimeActorTestBase
var actor = SpawnWriteHost(db, dep, factory, driverMemberCount: 2);
var asker = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 9.0), asker.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite("eq-1/speed", 9.0, AddressSpaceRealm.Uns), asker.Ref);
asker.ExpectMsg(Timeout).Success.ShouldBeFalse();
AwaitAssert(() =>
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorProbeResultDropTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorProbeResultDropTests.cs
index a9c7df56..6d5a211f 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorProbeResultDropTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorProbeResultDropTests.cs
@@ -3,6 +3,7 @@ using Akka.Cluster.Tools.PublishSubscribe;
using Akka.Event;
using Microsoft.EntityFrameworkCore;
using Xunit;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
@@ -87,7 +88,7 @@ public sealed class DriverHostActorProbeResultDropTests : RuntimeActorTestBase
// drained its mailbox past the OpcUaProbeResult message, so any dead-letter from an unhandled
// OpcUaProbeResult would already be on the EventStream before we assert.
var asker = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite("eq-x/speed", 0.0), asker.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite("eq-x/speed", 0.0, AddressSpaceRealm.Uns), asker.Ref);
asker.ExpectMsg(Timeout);
// Assert no dead-letter carrying an OpcUaProbeResult was published.
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorWriteRoutingTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorWriteRoutingTests.cs
index 3f9c1527..df53cfa6 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorWriteRoutingTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/DriverHostActorWriteRoutingTests.cs
@@ -8,6 +8,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Deploy;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Fleet;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Redundancy;
+using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
@@ -56,7 +57,7 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
var asker = CreateTestProbe();
// The node manager passes the FULL ns-qualified id; the host must normalise + resolve it.
- actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=3;s={UnsNodeId}", 123.0), asker.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=3;s={UnsNodeId}", 123.0, AddressSpaceRealm.Uns), asker.Ref);
asker.ExpectMsg(Timeout).Success.ShouldBeTrue();
AwaitAssert(() =>
@@ -79,7 +80,7 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
var actor = SpawnHostAndApply(db, deploymentId, recorder);
var asker = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=2;s={RawPath}", 456.0), asker.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=2;s={RawPath}", 456.0, AddressSpaceRealm.Raw), asker.Ref);
asker.ExpectMsg(Timeout).Success.ShouldBeTrue();
AwaitAssert(() =>
@@ -110,7 +111,7 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
CorrelationId.NewId()));
var asker = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=3;s={UnsNodeId}", 123.0), asker.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=3;s={UnsNodeId}", 123.0, AddressSpaceRealm.Uns), asker.Ref);
var result = asker.ExpectMsg(Timeout);
result.Success.ShouldBeFalse();
@@ -129,7 +130,7 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
var actor = SpawnHostAndApply(db, deploymentId, recorder);
var asker = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite("ns=3;s=filling/line1/station1/does-not-exist", 1.0), asker.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite("ns=3;s=filling/line1/station1/does-not-exist", 1.0, AddressSpaceRealm.Uns), asker.Ref);
var result = asker.ExpectMsg(Timeout);
result.Success.ShouldBeFalse();
@@ -149,7 +150,7 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
localRoles: new HashSet { "driver" }));
var asker = CreateTestProbe();
- actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=3;s={UnsNodeId}", 123.0), asker.Ref);
+ actor.Tell(new DriverHostActor.RouteNodeWrite($"ns=3;s={UnsNodeId}", 123.0, AddressSpaceRealm.Uns), asker.Ref);
var result = asker.ExpectMsg(TimeSpan.FromSeconds(2));
result.Success.ShouldBeFalse();
@@ -157,6 +158,46 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
result.Reason!.ShouldContain("stale");
}
+ ///
+ /// Review H1 regression — a raw RawPath and a UNS path that share a BARE s= id but back
+ /// DIFFERENT driver instances must each route to their OWN driver. Seeds raw tag A/B/C/D on
+ /// drv-1 and a UnsTagReference whose UNS NodeId is ALSO A/B/C/D (Area A / Line B / Equip C
+ /// / effective name D) but which backs a raw tag X/Y/Z/W on drv-2. A bare-only routing key
+ /// would collide (last-writer-wins → wrong device); the (realm, bareId) key keeps them distinct.
+ ///
+ [Fact]
+ public void Colliding_raw_and_uns_bare_ids_route_to_their_own_driver_by_realm()
+ {
+ var db = NewInMemoryDbFactory();
+ var factory = new PerInstanceRecordingDriverFactory("Modbus");
+ var deploymentId = SeedCollisionDeployment(db, RevA);
+
+ var actor = SpawnHostAndApply(db, deploymentId, factory);
+
+ // Write to the RAW node A/B/C/D → drv-1, forwarded by the raw tag's RawPath A/B/C/D.
+ var asker1 = CreateTestProbe();
+ actor.Tell(new DriverHostActor.RouteNodeWrite("ns=2;s=A/B/C/D", 111.0, AddressSpaceRealm.Raw), asker1.Ref);
+ asker1.ExpectMsg(Timeout).Success.ShouldBeTrue();
+
+ // Write to the UNS node A/B/C/D → drv-2, forwarded by the BACKING raw tag's RawPath X/Y/Z/W.
+ var asker2 = CreateTestProbe();
+ actor.Tell(new DriverHostActor.RouteNodeWrite("ns=3;s=A/B/C/D", 222.0, AddressSpaceRealm.Uns), asker2.Ref);
+ asker2.ExpectMsg(Timeout).Success.ShouldBeTrue();
+
+ AwaitAssert(() =>
+ {
+ // drv-1 got ONLY the raw write (A/B/C/D, 111); drv-2 got ONLY the UNS-routed write (X/Y/Z/W, 222).
+ var w1 = factory.WritesFor("drv-1");
+ var w2 = factory.WritesFor("drv-2");
+ w1.Count.ShouldBe(1);
+ w1[0].FullReference.ShouldBe("A/B/C/D");
+ w1[0].Value.ShouldBe(111.0);
+ w2.Count.ShouldBe(1);
+ w2[0].FullReference.ShouldBe("X/Y/Z/W"); // the backing RawPath, NOT the shared bare id
+ w2[0].Value.ShouldBe(222.0);
+ }, duration: Timeout);
+ }
+
/// Spawns the host with the recording driver factory, dispatches the deployment, and waits for
/// the Applied ACK so the reverse-map build in PushDesiredSubscriptions has completed before routing a
/// write. No OPC UA / mux probes are wired — the write path doesn't depend on the publish actor.
@@ -214,6 +255,52 @@ public sealed class DriverHostActorWriteRoutingTests : RuntimeActorTestBase
return id;
}
+ /// Seeds the review-H1 collision config: raw tag A/B/C/D on drv-1, and a UnsTagReference
+ /// (Area A / Line B / Equip C, effective name D → UNS NodeId A/B/C/D) backing raw tag X/Y/Z/W
+ /// on drv-2. Both drivers ENABLED (Modbus) so both children spawn.
+ private static DeploymentId SeedCollisionDeployment(IDbContextFactory db, RevisionHash rev)
+ {
+ var artifact = JsonSerializer.SerializeToUtf8Bytes(new
+ {
+ RawFolders = new[]
+ {
+ new { RawFolderId = "rf-a", ParentRawFolderId = (string?)null, Name = "A", ClusterId = "c1" },
+ new { RawFolderId = "rf-x", ParentRawFolderId = (string?)null, Name = "X", ClusterId = "c1" },
+ },
+ DriverInstances = new[]
+ {
+ new { DriverInstanceId = "drv-1", RawFolderId = "rf-a", Name = "B", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "c1", Enabled = true },
+ new { DriverInstanceId = "drv-2", RawFolderId = "rf-x", Name = "Y", DriverType = "Modbus", DriverConfig = "{}", ClusterId = "c1", Enabled = true },
+ },
+ Devices = new[]
+ {
+ new { DeviceId = "dev-c", DriverInstanceId = "drv-1", Name = "C", DeviceConfig = "{}" },
+ new { DeviceId = "dev-z", DriverInstanceId = "drv-2", Name = "Z", DeviceConfig = "{}" },
+ },
+ TagGroups = Array.Empty