test(historian): assert Deferred sink forwards historianTagname + doc nits

I1: DeferredAddressSpaceSinkTests.RecordingSink now captures HistorianTagname
per EnsureVariable call (HistorianQueue/HistorianCalls, matching the
Phase7ApplierTests pattern); new test EnsureVariable_forwards_historianTagname_to_inner_sink
asserts the arg is forwarded unchanged through DeferredAddressSpaceSink.

M1: OtOpcUaNodeManager.EnsureVariable doc-comment notes that a changed
historize intent on an already-registered node is silently ignored until
a RebuildAddressSpace (rebuild precondition for Task 3 implementers).

N2: DeploymentArtifact.ExtractTagHistorize doc wording: "The live-edit
side" → "The live-edit composer side".
This commit is contained in:
Joseph Doherty
2026-06-14 19:16:23 -04:00
parent 6041dc202b
commit 50e1141fc2
3 changed files with 31 additions and 3 deletions
@@ -832,7 +832,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
/// <paramref name="parentFolderNodeId"/> (or root when null). Initial value=null, quality=Bad,
/// timestamp=epoch — <see cref="WriteValue"/> 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 <c>_variables.ContainsKey</c> 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
/// <see cref="RebuildAddressSpace"/> (which the planner triggers on an equipment-tag delta).
/// </summary>
/// <param name="variableNodeId">The node identifier of the variable.</param>
/// <param name="parentFolderNodeId">The node identifier of the parent folder; null to use the namespace root.</param>
@@ -680,7 +680,7 @@ public static class DeploymentArtifact
/// <c>false</c>) and the optional <c>historianTagname</c> string override (absent / not a string /
/// whitespace-or-empty ⇒ <c>null</c>, meaning the historian tagname defaults to the tag's FullName,
/// resolved later). The raw string value is used — not trimmed — matching <c>ExtractTagFullName</c> /
/// <c>ExtractTagAlarm</c>. Never throws. The live-edit side
/// <c>ExtractTagAlarm</c>. Never throws. The live-edit composer side
/// (<c>Phase7Composer.ExtractTagHistorize</c>) MUST parse identically (byte-parity).</summary>
private static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig)
{
@@ -67,6 +67,22 @@ public sealed class DeferredAddressSpaceSinkTests
second.Calls.Single().ShouldBe("WV:b");
}
/// <summary>Verifies that <see cref="DeferredAddressSpaceSink.EnsureVariable"/> forwards the
/// <c>historianTagname</c> argument to the inner sink unchanged.</summary>
[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");
}
/// <summary>Builds a minimal <see cref="AlarmConditionSnapshot"/> for the forwarding tests (the
/// inner sink only records the node id, so the exact state values don't matter here).</summary>
private static AlarmConditionSnapshot Snapshot(bool active = false) =>
@@ -80,6 +96,12 @@ public sealed class DeferredAddressSpaceSinkTests
/// <summary>Gets the list of recorded calls.</summary>
public List<string> Calls => CallQueue.ToList();
/// <summary>Gets the queue of (NodeId, HistorianTagname) pairs captured per
/// <c>EnsureVariable</c> call, in call order.</summary>
public ConcurrentQueue<(string NodeId, string? HistorianTagname)> HistorianQueue { get; } = new();
/// <summary>Gets the list of (NodeId, HistorianTagname) pairs captured per EnsureVariable call.</summary>
public List<(string NodeId, string? HistorianTagname)> HistorianCalls => HistorianQueue.ToList();
/// <inheritdoc />
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}");
/// <inheritdoc />
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));
}
/// <inheritdoc />
public void RebuildAddressSpace() => CallQueue.Enqueue("RB");
}