fix(code-review): resolve OpcUaServer-001 — UNS Area/Line rename refreshes folder DisplayName
A rename-only deploy produced an IsEmpty plan that short-circuited before MaterialiseHierarchy, leaving the OPC UA folder DisplayName stale. AddressSpacePlanner now diffs UnsAreas/UnsLines by stable id into a RenamedFolders set (counted in IsEmpty); the applier refreshes the folder in place via a new UpdateFolderDisplayName on ISurgicalAddressSpaceSink (forwarded through DeferredAddressSpaceSink so it is NOT inert on driver hosts; falls back to rebuild when the sink is non-surgical). DeploymentArtifact byte-parity untouched (rename rides the existing Name round-trip). No EF migration, no serialized wire/proto contract change. +13 OpcUaServer tests, Runtime rebuild test.
This commit is contained in:
@@ -139,6 +139,40 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>OpcUaServer-001 regression: the deferred wrapper MUST forward the folder-rename surgical
|
||||
/// capability to a surgical inner sink — otherwise <c>AddressSpaceApplier</c> (which injects THIS
|
||||
/// wrapper on every driver-role host) never sees the capability and the in-place UNS Area/Line rename
|
||||
/// refresh is inert in production (silently falls back to a full rebuild).</summary>
|
||||
[Fact]
|
||||
public void UpdateFolderDisplayName_forwards_to_a_surgical_inner_sink()
|
||||
{
|
||||
var deferred = new DeferredAddressSpaceSink();
|
||||
var inner = new SurgicalRecordingSink { Result = true };
|
||||
deferred.SetSink(inner);
|
||||
|
||||
((ISurgicalAddressSpaceSink)deferred).UpdateFolderDisplayName("area-1", "Plant South")
|
||||
.ShouldBeTrue();
|
||||
|
||||
var call = inner.FolderRenameCalls.ShouldHaveSingleItem();
|
||||
call.FolderNodeId.ShouldBe("area-1");
|
||||
call.DisplayName.ShouldBe("Plant South");
|
||||
}
|
||||
|
||||
/// <summary>The folder-rename forward returns the inner's own result (false ⇒ folder missing) so the
|
||||
/// caller falls back to a full rebuild; and returns false when the inner is not surgical at all.</summary>
|
||||
[Fact]
|
||||
public void UpdateFolderDisplayName_returns_inner_result_and_false_when_not_surgical()
|
||||
{
|
||||
var deferred = new DeferredAddressSpaceSink();
|
||||
// Inner reports the folder missing ⇒ forward returns false.
|
||||
deferred.SetSink(new SurgicalRecordingSink { Result = false });
|
||||
((ISurgicalAddressSpaceSink)deferred).UpdateFolderDisplayName("area-1", "X").ShouldBeFalse();
|
||||
|
||||
// Non-surgical inner (the null sink before swap-in) ⇒ false.
|
||||
deferred.SetSink(null);
|
||||
((ISurgicalAddressSpaceSink)deferred).UpdateFolderDisplayName("area-1", "X").ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <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) =>
|
||||
@@ -182,10 +216,12 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
|
||||
private sealed class SurgicalRecordingSink : IOpcUaAddressSpaceSink, ISurgicalAddressSpaceSink
|
||||
{
|
||||
/// <summary>Gets or sets the value <see cref="UpdateTagAttributes"/> returns.</summary>
|
||||
/// <summary>Gets or sets the value <see cref="UpdateTagAttributes"/> + <see cref="UpdateFolderDisplayName"/> return.</summary>
|
||||
public bool Result { get; set; } = true;
|
||||
/// <summary>Gets the recorded surgical calls (incl. the FB-7 DataType/array-shape args).</summary>
|
||||
public List<(string NodeId, bool Writable, string? Historian, string DataType, bool IsArray, uint? ArrayLength)> SurgicalCalls { get; } = new();
|
||||
/// <summary>Gets the recorded surgical folder-rename calls (OpcUaServer-001).</summary>
|
||||
public List<(string FolderNodeId, string DisplayName)> FolderRenameCalls { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||
@@ -194,6 +230,13 @@ public sealed class DeferredAddressSpaceSinkTests
|
||||
return Result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||
{
|
||||
FolderRenameCalls.Add((folderNodeId, displayName));
|
||||
return Result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { }
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user