fix(address-space): announce surgically-added raw nodes; equipment-with-references removal stays surgical (#599)
- AnnounceAddedNodes raises NodeAdded in the Raw realm for the existing parents of added raw containers/tags (ComputeRawAddAnnouncements); Uns side unchanged. - ApplyPureRemove removes UNS reference variables BEFORE the equipment subtree, so a reference the subtree's id-prefix scope reaches no longer ratchets to a full rebuild (03/P5). A NotSubsumed filter was not used: in production a reference NodeId is Area/Line/Equipment/Name, outside the subtree scope, so filtering would leak it. The matching RemoveEquipmentSubtree severing (03/S20) is in the node-manager commit. - UnsReferenceBackingParityTests: reflection guard discovering the classifier's surgical raw-tag whitelist and requiring a same-typed mirror field (03/U11). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -1459,6 +1459,62 @@ public sealed class AddressSpaceApplierTests
|
||||
new[] { "eq-1", V3NodeIds.Uns("eq-1", "Diag"), "line-7" }.OrderBy(x => x));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gitea #599 (archreview 01/U-8 = 03/P6) — surgically-added RAW nodes are announced in the RAW realm.
|
||||
/// A new tag under an existing device announces that device; a new TagGroup carrying a new tag under a
|
||||
/// new Device announces only the Device's existing parent — the inner parents are themselves new, and a
|
||||
/// client re-browsing the outer one discovers the whole new subtree. Before the fix only Uns-realm
|
||||
/// parents were announced, so these adds were invisible to connected model-aware clients.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AnnounceAddedNodes_announces_added_raw_nodes_in_the_raw_realm()
|
||||
{
|
||||
var sink = new RecordingSink();
|
||||
var applier = new AddressSpaceApplier(sink, NullLogger<AddressSpaceApplier>.Instance);
|
||||
|
||||
var plan = EmptyPlan with
|
||||
{
|
||||
AddedRawContainers = new[]
|
||||
{
|
||||
new RawContainerNode("Plant/Modbus/dev2", "Plant/Modbus", "dev2", RawNodeKind.Device),
|
||||
new RawContainerNode("Plant/Modbus/dev2/grp", "Plant/Modbus/dev2", "grp", RawNodeKind.TagGroup),
|
||||
},
|
||||
AddedRawTags = new[]
|
||||
{
|
||||
RawTag("Plant/Modbus/dev2/grp/speed") with { ParentNodeId = "Plant/Modbus/dev2/grp" },
|
||||
RawTag("Plant/Modbus/dev1/temp"), // parent Plant/Modbus/dev1 already exists
|
||||
},
|
||||
// A UNS add in the same plan still announces in the Uns realm.
|
||||
AddedUnsReferenceVariables = new[] { UnsRef("filling/line-1/station-1/speed", "speed") },
|
||||
};
|
||||
|
||||
applier.AnnounceAddedNodes(plan);
|
||||
|
||||
sink.ModelChangeRealmQueue.OrderBy(x => x.NodeId, StringComparer.Ordinal).ToArray().ShouldBe(new[]
|
||||
{
|
||||
("Plant/Modbus", AddressSpaceRealm.Raw),
|
||||
("Plant/Modbus/dev1", AddressSpaceRealm.Raw),
|
||||
("eq-1", AddressSpaceRealm.Uns),
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>A top-level raw container (no parent: it hangs under the shared root, which has no
|
||||
/// realm-qualified id) announces itself; duplicates collapse; the Uns-side computation is unaffected.</summary>
|
||||
[Fact]
|
||||
public void ComputeRawAddAnnouncements_top_level_container_announces_itself_and_dedups()
|
||||
{
|
||||
var applier = new AddressSpaceApplier(new RecordingSink(), NullLogger<AddressSpaceApplier>.Instance);
|
||||
|
||||
var plan = EmptyPlan with
|
||||
{
|
||||
AddedRawContainers = new[] { new RawContainerNode("Plant2", null, "Plant2", RawNodeKind.Folder) },
|
||||
AddedRawTags = new[] { RawTag("Plant/Modbus/dev1/a"), RawTag("Plant/Modbus/dev1/b") },
|
||||
};
|
||||
|
||||
applier.ComputeRawAddAnnouncements(plan).ShouldBe(new[] { "Plant/Modbus/dev1", "Plant2" });
|
||||
applier.ComputeAddAnnouncements(plan).ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// ----- Surgical v3 deltas: raw-tag attribute edits (Raw realm) + UNS-reference remove -----
|
||||
// Task 4 taught AddressSpaceChangeClassifier that a non-alarm RawTagDelta and any UnsReferenceDelta are
|
||||
// surgical-eligible; these pin the applier actually APPLYING them (before this, such a plan classified
|
||||
@@ -1963,7 +2019,13 @@ public sealed class AddressSpaceApplierTests
|
||||
public List<string> ModelChangeCalls => ModelChangeQueue.ToList();
|
||||
/// <summary>Records a NodeAdded model-change announcement.</summary>
|
||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns) => ModelChangeQueue.Enqueue(affectedNodeId);
|
||||
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm = AddressSpaceRealm.Uns)
|
||||
{
|
||||
ModelChangeQueue.Enqueue(affectedNodeId);
|
||||
ModelChangeRealmQueue.Enqueue((affectedNodeId, realm));
|
||||
}
|
||||
/// <summary>Gets the NodeAdded announcements WITH the realm each was raised in (Gitea #599).</summary>
|
||||
public ConcurrentQueue<(string NodeId, AddressSpaceRealm Realm)> ModelChangeRealmQueue { get; } = new();
|
||||
public void WireAlarmNotifiers(string alarmNodeId, AddressSpaceRealm alarmRealm, IReadOnlyList<string> notifierFolderNodeIds, AddressSpaceRealm notifierFolderRealm) { }
|
||||
public void AddReference(string sourceNodeId, AddressSpaceRealm sourceRealm, string targetNodeId, AddressSpaceRealm targetRealm, string referenceType = "Organizes") { }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user