From 1dc713693a5fb9bfdc20febdc406e90e10468e42 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 15 Jun 2026 10:01:37 -0400 Subject: [PATCH] fix(deploy): count removed equipment tags/vtags in RemovedNodes (H1a review follow-up) --- .../Phase7Applier.cs | 4 ++ .../Phase7ApplierTests.cs | 44 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs index eda6f98f..d922ed72 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs @@ -67,6 +67,10 @@ public sealed class Phase7Applier SafeWriteAlarmCondition(alarm.ScriptedAlarmId, RemovedConditionState, ts); removedCount++; } + // Removed equipment tags / VirtualTags are plain variable nodes (no Part 9 condition to write + // before tear-down), but they ARE real removals — count them so Phase7ApplyOutcome.RemovedNodes + // is accurate on a removed-tag-only deploy, which now reaches the rebuild path below. + removedCount += plan.RemovedEquipmentTags.Count + plan.RemovedEquipmentVirtualTags.Count; var changedCount = plan.ChangedEquipment.Count + plan.ChangedDrivers.Count + plan.ChangedAlarms.Count + diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs index c794bfce..6bdc71b3 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ApplierTests.cs @@ -622,6 +622,50 @@ public sealed class Phase7ApplierTests sink.RebuildCalls.ShouldBe(0); } + /// H1a (review follow-up) — a deploy that ONLY removes existing equipment tag / VirtualTag + /// nodes must rebuild AND tally the removals. Removed tags/VirtualTags are plain variable nodes (no + /// Part 9 condition to write), so before the fix they reached the rebuild path but were never added + /// to removedCountPhase7ApplyOutcome.RemovedNodes reported 0, a misleading audit + /// entry. This pins both the rebuild and the accurate count. + [Fact] + public void Removed_equipment_tags_and_virtual_tags_only_rebuild_and_are_counted() + { + var sink = new RecordingSink(); + var applier = new Phase7Applier(sink, NullLogger.Instance); + + var previous = new Phase7CompositionResult( + Array.Empty(), Array.Empty(), Array.Empty()) + { + EquipmentTags = new[] + { + new EquipmentTagPlan("tag-1", "eq-1", "drv", FolderPath: "", Name: "Speed", DataType: "Float", FullName: "40001", Writable: false, Alarm: null), + }, + EquipmentVirtualTags = new[] + { + new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float64", + Expression: "ctx.GetTag(\"a\")", DependencyRefs: new[] { "a" }), + }, + }; + var next = new Phase7CompositionResult( + Array.Empty(), Array.Empty(), Array.Empty()); + + var plan = Phase7Planner.Compute(previous, next); + + // Guard the arrange: ONLY the two Removed sets are populated. + plan.RemovedEquipmentTags.Count.ShouldBe(1); + plan.RemovedEquipmentVirtualTags.Count.ShouldBe(1); + plan.AddedEquipmentTags.ShouldBeEmpty(); + plan.ChangedEquipmentTags.ShouldBeEmpty(); + plan.RemovedEquipment.ShouldBeEmpty(); + plan.RemovedAlarms.ShouldBeEmpty(); + + var outcome = applier.Apply(plan); + + outcome.RebuildCalled.ShouldBeTrue(); + sink.RebuildCalls.ShouldBe(1); + outcome.RemovedNodes.ShouldBe(2); // both removals counted (was 0 before the fix) + } + private static Phase7CompositionResult CompositionWithTags(params EquipmentTagPlan[] tags) => new( Array.Empty(), Array.Empty(), Array.Empty())