From fc8121cbf3a874f637ab031b85ea09a7c5b7c2f3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 15 Jun 2026 10:17:05 -0400 Subject: [PATCH] =?UTF-8?q?feat(vtags):=20carry=20VirtualTag.Historize=20o?= =?UTF-8?q?nto=20EquipmentVirtualTagPlan=20(H5a,=20stillpending=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Phase7Composer.cs | 16 ++++- .../Phase7ComposerVirtualTagHistorizeTests.cs | 70 +++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerVirtualTagHistorizeTests.cs diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs index 4ac1e333..481d21ac 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs @@ -113,6 +113,11 @@ public sealed record EquipmentTagAlarmInfo(string AlarmType, int Severity); /// that NodeId. = the distinct ctx.GetTag("…") literals in /// the script source. /// +/// 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 — +/// which keeps existing positional+named ctor call sites compiling and preserves byte-parity. public sealed record EquipmentVirtualTagPlan( string VirtualTagId, string EquipmentId, @@ -120,12 +125,14 @@ public sealed record EquipmentVirtualTagPlan( string Name, string DataType, string Expression, - IReadOnlyList DependencyRefs) + IReadOnlyList DependencyRefs, + bool Historize = false) { /// Structural equality: the auto-generated record equality would compare /// (an interface-typed list) BY REFERENCE, flagging every /// VirtualTag as "changed" on every parse (fresh list instances). Compare it element-wise - /// so a no-op redeploy diffs empty. + /// so a no-op redeploy diffs empty. is included so a Historize-only + /// toggle is detected as a change. public bool Equals(EquipmentVirtualTagPlan? other) => other is not null && VirtualTagId == other.VirtualTagId && @@ -134,6 +141,7 @@ public sealed record EquipmentVirtualTagPlan( Name == other.Name && DataType == other.DataType && Expression == other.Expression && + Historize == other.Historize && DependencyRefs.SequenceEqual(other.DependencyRefs, StringComparer.Ordinal); public override int GetHashCode() @@ -141,6 +149,7 @@ public sealed record EquipmentVirtualTagPlan( var hash = new HashCode(); hash.Add(VirtualTagId); hash.Add(EquipmentId); hash.Add(FolderPath); hash.Add(Name); hash.Add(DataType); hash.Add(Expression); + hash.Add(Historize); foreach (var r in DependencyRefs) hash.Add(r, StringComparer.Ordinal); return hash.ToHashCode(); } @@ -385,7 +394,8 @@ public static class Phase7Composer Name: v.Name, DataType: v.DataType, Expression: expanded, - DependencyRefs: EquipmentScriptPaths.ExtractDependencyRefs(expanded)); + DependencyRefs: EquipmentScriptPaths.ExtractDependencyRefs(expanded), + Historize: v.Historize); }) .ToList(); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerVirtualTagHistorizeTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerVirtualTagHistorizeTests.cs new file mode 100644 index 00000000..a48499d4 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/Phase7ComposerVirtualTagHistorizeTests.cs @@ -0,0 +1,70 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.Configuration.Entities; +using ZB.MOM.WW.OtOpcUa.Configuration.Enums; + +namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests; + +/// +/// Verifies the live-edit compose seam () carries the +/// entity column onto the resulting +/// (H5a). The flag was authored in the UI but +/// never threaded onto the plan, leaving equipment-namespace runtime historization dead. Both a +/// true and a false case are asserted so the default (false) and the explicit set both prove out. +/// +public sealed class Phase7ComposerVirtualTagHistorizeTests +{ + [Fact] + public void Compose_carries_virtual_tag_historize_flag_onto_plan() + { + var ns = new Namespace + { + NamespaceId = "ns-eq", + ClusterId = "c1", + Kind = NamespaceKind.Equipment, + NamespaceUri = "urn:eq", + }; + var driver = new DriverInstance + { + DriverInstanceId = "drv-1", + ClusterId = "c1", + NamespaceId = "ns-eq", + Name = "Modbus1", + DriverType = "Modbus", + DriverConfig = "{}", + }; + var area = new UnsArea { UnsAreaId = "area-1", ClusterId = "c1", Name = "filling" }; + var line = new UnsLine { UnsLineId = "line-1", UnsAreaId = "area-1", Name = "line-1" }; + var equip = new Equipment { EquipmentId = "eq-1", DriverInstanceId = "drv-1", UnsLineId = "line-1", Name = "TestMachine_001", MachineCode = "TESTMACHINE_001" }; + var tag = new Tag + { + TagId = "tag-1", + DriverInstanceId = "drv-1", + EquipmentId = "eq-1", + Name = "Source", + DataType = "Int32", + AccessLevel = TagAccessLevel.Read, + TagConfig = "{\"FullName\":\"TestMachine_001.Source\",\"DataType\":\"Int32\"}", + }; + var script = new Script + { + ScriptId = "s-1", + Name = "passthru", + SourceCode = "return ctx.GetTag(\"TestMachine_001.Source\").Value;", + SourceHash = "hash-1", + }; + // Two VirtualTags off the same script: one historized, one not. + var vtHist = new VirtualTag { VirtualTagId = "vt-hist", EquipmentId = "eq-1", Name = "Historized", DataType = "Int32", ScriptId = "s-1", Historize = true }; + var vtPlain = new VirtualTag { VirtualTagId = "vt-plain", EquipmentId = "eq-1", Name = "Plain", DataType = "Int32", ScriptId = "s-1", Historize = false }; + + var result = Phase7Composer.Compose( + new[] { area }, new[] { line }, new[] { equip }, + new[] { driver }, Array.Empty(), + new[] { tag }, new[] { ns }, + virtualTags: new[] { vtHist, vtPlain }, + scripts: new[] { script }); + + result.EquipmentVirtualTags.Single(p => p.VirtualTagId == "vt-hist").Historize.ShouldBeTrue(); + result.EquipmentVirtualTags.Single(p => p.VirtualTagId == "vt-plain").Historize.ShouldBeFalse(); + } +}