diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
index 9e700438..71a11f0c 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs
@@ -832,7 +832,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// (or root when null). Initial value=null, quality=Bad,
/// timestamp=epoch — fills these in once driver data flows.
/// Idempotent. Materialises equipment-namespace tags so they're browseable before drivers
- /// issue SubscribeBulk.
+ /// issue SubscribeBulk. Note: because of the early _variables.ContainsKey return, a
+ /// re-apply of an EXISTING node with a changed historize intent (e.g. non-historized →
+ /// historized) is silently ignored — a historize-intent change only takes effect after a
+ /// (which the planner triggers on an equipment-tag delta).
///
/// The node identifier of the variable.
/// The node identifier of the parent folder; null to use the namespace root.
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs
index 115e631a..a99f5c22 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs
@@ -680,7 +680,7 @@ public static class DeploymentArtifact
/// false) and the optional historianTagname string override (absent / not a string /
/// whitespace-or-empty ⇒ null, meaning the historian tagname defaults to the tag's FullName,
/// resolved later). The raw string value is used — not trimmed — matching ExtractTagFullName /
- /// ExtractTagAlarm. Never throws. The live-edit side
+ /// ExtractTagAlarm. Never throws. The live-edit composer side
/// (Phase7Composer.ExtractTagHistorize) MUST parse identically (byte-parity).
private static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig)
{
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs
index 76d03ab2..cd6793ab 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/DeferredAddressSpaceSinkTests.cs
@@ -67,6 +67,22 @@ public sealed class DeferredAddressSpaceSinkTests
second.Calls.Single().ShouldBe("WV:b");
}
+ /// Verifies that forwards the
+ /// historianTagname argument to the inner sink unchanged.
+ [Fact]
+ public void EnsureVariable_forwards_historianTagname_to_inner_sink()
+ {
+ var deferred = new DeferredAddressSpaceSink();
+ var inner = new RecordingSink();
+ deferred.SetSink(inner);
+
+ deferred.EnsureVariable("v-1", null, "MyVar", "Float", writable: false, historianTagname: "MyTag.PV");
+
+ var call = inner.HistorianCalls.ShouldHaveSingleItem();
+ call.NodeId.ShouldBe("v-1");
+ call.HistorianTagname.ShouldBe("MyTag.PV");
+ }
+
/// Builds a minimal for the forwarding tests (the
/// inner sink only records the node id, so the exact state values don't matter here).
private static AlarmConditionSnapshot Snapshot(bool active = false) =>
@@ -80,6 +96,12 @@ public sealed class DeferredAddressSpaceSinkTests
/// Gets the list of recorded calls.
public List Calls => CallQueue.ToList();
+ /// Gets the queue of (NodeId, HistorianTagname) pairs captured per
+ /// EnsureVariable call, in call order.
+ public ConcurrentQueue<(string NodeId, string? HistorianTagname)> HistorianQueue { get; } = new();
+ /// Gets the list of (NodeId, HistorianTagname) pairs captured per EnsureVariable call.
+ public List<(string NodeId, string? HistorianTagname)> HistorianCalls => HistorianQueue.ToList();
+
///
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
=> CallQueue.Enqueue($"WV:{nodeId}");
@@ -94,7 +116,10 @@ public sealed class DeferredAddressSpaceSinkTests
=> CallQueue.Enqueue($"EF:{folderNodeId}");
///
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null)
- => CallQueue.Enqueue($"EV:{variableNodeId}");
+ {
+ CallQueue.Enqueue($"EV:{variableNodeId}");
+ HistorianQueue.Enqueue((variableNodeId, historianTagname));
+ }
///
public void RebuildAddressSpace() => CallQueue.Enqueue("RB");
}