diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs index 481d21ac..f6933f00 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs @@ -115,8 +115,8 @@ public sealed record EquipmentTagAlarmInfo(string AlarmType, int Severity); /// /// When true, this VirtualTag's values are historized (carried from the /// VirtualTag.Historize entity column). Threaded through the deploy-diff equality below so a -/// Historize-only toggle is detected as a change. Defaults to false — matching both the entity -/// default for an unset column and the artifact-decode default when the flag is absent/non-bool — +/// Historize-only toggle is detected as a change. Defaults to false — matching both the CLR +/// default of the bool VirtualTag.Historize column and the artifact-decode default when the flag is absent/non-bool — /// which keeps existing positional+named ctor call sites compiling and preserves byte-parity. public sealed record EquipmentVirtualTagPlan( string VirtualTagId, diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7PlannerTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7PlannerTests.cs index cd7d1452..5bdb7bb8 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7PlannerTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7PlannerTests.cs @@ -139,6 +139,41 @@ public sealed class Phase7PlannerTests plan.RemovedEquipmentVirtualTags.ShouldBeEmpty(); } + /// H5a — a VirtualTag with the same id but a toggled Historize flag (and otherwise + /// identical fields) must route to ChangedEquipmentVirtualTags. This pins that Historize is + /// part of so a Historize-only deploy is not a silent + /// no-op at the diff/IsEmpty gate. + [Fact] + public void Same_id_with_toggled_historize_routes_to_ChangedEquipmentVirtualTags() + { + var prev = new Phase7CompositionResult( + Array.Empty(), Array.Empty(), Array.Empty()) + { + EquipmentVirtualTags = new[] + { + new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float", + Expression: "a + b", DependencyRefs: new[] { "a", "b" }, Historize: false), + }, + }; + var next = new Phase7CompositionResult( + Array.Empty(), Array.Empty(), Array.Empty()) + { + EquipmentVirtualTags = new[] + { + new EquipmentVirtualTagPlan("vt-1", "eq-1", FolderPath: "", Name: "Efficiency", DataType: "Float", + Expression: "a + b", DependencyRefs: new[] { "a", "b" }, Historize: true), + }, + }; + + var plan = Phase7Planner.Compute(prev, next); + + plan.IsEmpty.ShouldBeFalse(); + plan.ChangedEquipmentVirtualTags.Single().Previous.Historize.ShouldBeFalse(); + plan.ChangedEquipmentVirtualTags.Single().Current.Historize.ShouldBeTrue(); + plan.AddedEquipmentVirtualTags.ShouldBeEmpty(); + plan.RemovedEquipmentVirtualTags.ShouldBeEmpty(); + } + /// Regression guard for structural equality on : /// two snapshots containing the SAME VirtualTag built from SEPARATE list instances must diff to an empty plan /// (IReadOnlyList equality is BY REFERENCE without the custom Equals override, so every VirtualTag with