From c9590c03d0d58a82cc65b33a518f3d2272888815 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 10 Jun 2026 14:47:24 -0400 Subject: [PATCH] fix(scripted-alarms): harden artifact boolean decode + direct helper tests (T6 review) Default HistorizeToAveva/Retain/Enabled to the entity defaults (true) when a field is absent/null/non-boolean so a partial blob decodes identically to the composer's view of a default-constructed ScriptedAlarm (byte-parity), and only call GetBoolean for a genuine true/false token. Add direct ExtractAlarmDependencyRefs unit tests (overlap dedup + reserved {{equip}} exclusion). --- .../Drivers/DeploymentArtifact.cs | 16 ++++++----- .../EquipmentScriptPathsTests.cs | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs index 3f590452..0cb20f6f 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs @@ -653,12 +653,16 @@ public static class DeploymentArtifact var severity = el.TryGetProperty("Severity", out var svEl) && svEl.TryGetInt32(out var sv) ? sv : 0; var messageTemplate = el.TryGetProperty("MessageTemplate", out var mtEl) ? mtEl.GetString() : null; var predicateScriptId = el.TryGetProperty("PredicateScriptId", out var psEl) ? psEl.GetString() : null; - var historize = el.TryGetProperty("HistorizeToAveva", out var hEl) && hEl.ValueKind != JsonValueKind.Null - ? hEl.GetBoolean() : false; - var retain = el.TryGetProperty("Retain", out var rEl) && rEl.ValueKind != JsonValueKind.Null - ? rEl.GetBoolean() : false; - var enabled = el.TryGetProperty("Enabled", out var enEl) && enEl.ValueKind != JsonValueKind.Null - ? enEl.GetBoolean() : false; + // Booleans default to the entity defaults (true) when absent / null / non-boolean, so a + // partial blob decodes the same as the composer's view of a default-constructed + // ScriptedAlarm — preserving byte-parity. GetBoolean only runs for a genuine true/false + // token (a non-bool token would otherwise throw InvalidOperationException, uncaught here). + bool ReadBool(string prop, bool dflt) => + el.TryGetProperty(prop, out var b) && b.ValueKind is JsonValueKind.True or JsonValueKind.False + ? b.GetBoolean() : dflt; + var historize = ReadBool("HistorizeToAveva", true); + var retain = ReadBool("Retain", true); + var enabled = ReadBool("Enabled", true); if (string.IsNullOrWhiteSpace(scriptedAlarmId)) continue; diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/EquipmentScriptPathsTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/EquipmentScriptPathsTests.cs index 552c3f98..27c00ac4 100644 --- a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/EquipmentScriptPathsTests.cs +++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/EquipmentScriptPathsTests.cs @@ -6,6 +6,33 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Tests; public class EquipmentScriptPathsTests { + // ---- ExtractAlarmDependencyRefs (scripted-alarm dep graph; byte-parity seam) ---- + + [Fact] + public void ExtractAlarmDependencyRefs_predicate_reads_first_then_template_tokens_deduped() + { + // A template token identical to a predicate read appears once (predicate-first); a distinct + // template token is appended after. This dedup/order is the parity contract both the composer + // and the artifact decode rely on. + EquipmentScriptPaths + .ExtractAlarmDependencyRefs( + predicateSource: "return ctx.GetTag(\"Mach.Temp\").Value > 90;", + messageTemplate: "Temp {Mach.Temp} over {Mach.Limit}") + .ShouldBe(["Mach.Temp", "Mach.Limit"]); + } + + [Fact] + public void ExtractAlarmDependencyRefs_excludes_reserved_equip_double_brace_token() + { + // The reserved {{equip}} double-brace form must NOT be picked up as a single-brace {TagPath} + // token; only the genuine {Line.Temp} token is extracted. + EquipmentScriptPaths + .ExtractAlarmDependencyRefs( + predicateSource: null, + messageTemplate: "{{equip}} too hot: {Line.Temp}") + .ShouldBe(["Line.Temp"]); + } + // ---- DeriveEquipmentBase ---- [Fact]