diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs index f67ce854..5fd6b3a3 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/Commands/TemplateCommands.cs @@ -421,6 +421,9 @@ public static class TemplateCommands updateCmd.Add(updateTriggerKindOption); updateCmd.SetAction(async (ParseResult result) => { + var triggerConfig = TriggerConfigJson.InjectAnalysisKind( + result.GetValue(updateTriggerConfigOption), + result.GetValue(updateTriggerKindOption)); return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new UpdateTemplateAlarmCommand( @@ -429,7 +432,7 @@ public static class TemplateCommands result.GetValue(updateTriggerTypeOption)!, result.GetValue(updatePriorityOption)!, result.GetValue(updateDescOption), - result.GetValue(updateTriggerConfigOption), + triggerConfig, result.GetValue(updateLockedOption))); }); group.Add(updateCmd); @@ -549,6 +552,9 @@ public static class TemplateCommands addCmd.Add(scriptTriggerKindOption); addCmd.SetAction(async (ParseResult result) => { + var triggerConfig = TriggerConfigJson.InjectAnalysisKind( + result.GetValue(triggerConfigOption), + result.GetValue(scriptTriggerKindOption)); return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new AddTemplateScriptCommand( @@ -556,7 +562,7 @@ public static class TemplateCommands result.GetValue(nameOption)!, result.GetValue(codeOption)!, result.GetValue(triggerTypeOption)!, - result.GetValue(triggerConfigOption), + triggerConfig, result.GetValue(lockedOption), result.GetValue(paramsOption), result.GetValue(returnOption))); @@ -591,6 +597,9 @@ public static class TemplateCommands updateCmd.Add(updateScriptTriggerKindOption); updateCmd.SetAction(async (ParseResult result) => { + var triggerConfig = TriggerConfigJson.InjectAnalysisKind( + result.GetValue(updateTriggerConfigOption), + result.GetValue(updateScriptTriggerKindOption)); return await CommandHelpers.ExecuteCommandAsync( result, urlOption, formatOption, usernameOption, passwordOption, new UpdateTemplateScriptCommand( @@ -598,7 +607,7 @@ public static class TemplateCommands result.GetValue(updateNameOption)!, result.GetValue(updateCodeOption)!, result.GetValue(updateTriggerTypeOption)!, - result.GetValue(updateTriggerConfigOption), + triggerConfig, result.GetValue(updateLockedOption), result.GetValue(updateParamsOption), result.GetValue(updateReturnOption))); diff --git a/src/ZB.MOM.WW.ScadaBridge.CLI/TriggerConfigJson.cs b/src/ZB.MOM.WW.ScadaBridge.CLI/TriggerConfigJson.cs new file mode 100644 index 00000000..f1be4900 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.CLI/TriggerConfigJson.cs @@ -0,0 +1,61 @@ +using System.Text; +using System.Text.Json; + +namespace ZB.MOM.WW.ScadaBridge.CLI; + +/// +/// Shared trigger-config JSON helpers for CLI paths that receive a raw +/// --trigger-config JSON blob and need to inject or strip the +/// "analysisKind" key based on --trigger-kind. +/// Used by alarm update, script add, and script update +/// (alarm add uses instead). +/// +internal static class TriggerConfigJson +{ + /// + /// Copies every property from into a new object, + /// adding or replacing "analysisKind" when + /// is "strict" (case-insensitive), + /// or omitting it for any other value (including ). + /// Returns when is + /// or empty, preserving the caller's null-means-no-config + /// semantics. + /// + /// Raw trigger-config JSON object, or null. + /// + /// Value of --trigger-kind: "strict" → write + /// "analysisKind":"Strict"; anything else → omit the key. + /// + /// + /// The rewritten JSON string, or when + /// is null/empty. + /// + internal static string? InjectAnalysisKind(string? json, string? analysisKind) + { + if (string.IsNullOrWhiteSpace(json)) + return null; + + var isStrict = string.Equals(analysisKind?.Trim(), "Strict", StringComparison.OrdinalIgnoreCase); + + using var inputDoc = JsonDocument.Parse(json); + using var stream = new MemoryStream(); + using (var writer = new Utf8JsonWriter(stream)) + { + writer.WriteStartObject(); + + // Copy all existing properties except "analysisKind" (we'll re-add it below if needed) + foreach (var prop in inputDoc.RootElement.EnumerateObject()) + { + if (!prop.NameEquals("analysisKind")) + prop.WriteTo(writer); + } + + if (isStrict) + writer.WriteString("analysisKind", "Strict"); + + writer.WriteEndObject(); + } + + return Encoding.UTF8.GetString(stream.ToArray()); + } +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/TemplateTriggerKindTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/TemplateTriggerKindTests.cs index 2b831e78..a1a6a39a 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/TemplateTriggerKindTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CLI.Tests/Commands/TemplateTriggerKindTests.cs @@ -13,18 +13,78 @@ namespace ZB.MOM.WW.ScadaBridge.CLI.Tests.Commands; /// public class TemplateTriggerKindTests { - private static readonly Option Url = new("--url") { Recursive = true }; - private static readonly Option Username = new("--username") { Recursive = true }; - private static readonly Option Password = new("--password") { Recursive = true }; - private static readonly Option Format = CliOptions.CreateFormatOption(); + // ── TriggerConfigJson.InjectAnalysisKind — the shared helper used by + // alarm-update / script-add / script-update ────────────────────────────── - private static Command AlarmGroup() - => TemplateCommands.Build(Url, Format, Username, Password) - .Subcommands.Single(c => c.Name == "alarm"); + // Inject strict into an Expression config that has no prior analysisKind + [Fact] + public void InjectAnalysisKind_Strict_AddsKey() + { + var json = "{\"expression\":\"x > 0\"}"; + var result = TriggerConfigJson.InjectAnalysisKind(json, "strict"); - private static Command ScriptGroup() - => TemplateCommands.Build(Url, Format, Username, Password) - .Subcommands.Single(c => c.Name == "script"); + Assert.NotNull(result); + using var doc = JsonDocument.Parse(result!); + Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString()); + } + + // Inject advisory (null) into config that had Strict → key removed + [Fact] + public void InjectAnalysisKind_Advisory_RemovesKey() + { + var json = "{\"expression\":\"x > 0\",\"analysisKind\":\"Strict\"}"; + var result = TriggerConfigJson.InjectAnalysisKind(json, null); + + Assert.NotNull(result); + using var doc = JsonDocument.Parse(result!); + Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _)); + } + + // Inject strict into config that already had Strict → still Strict (idempotent) + [Fact] + public void InjectAnalysisKind_Strict_Idempotent() + { + var json = "{\"expression\":\"x > 0\",\"analysisKind\":\"Strict\"}"; + var result = TriggerConfigJson.InjectAnalysisKind(json, "strict"); + + Assert.NotNull(result); + using var doc = JsonDocument.Parse(result!); + Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString()); + } + + // Null config → returns null (no config to inject into) + [Fact] + public void InjectAnalysisKind_NullConfig_ReturnsNull() + { + var result = TriggerConfigJson.InjectAnalysisKind(null, "strict"); + Assert.Null(result); + } + + // Case-insensitive: "STRICT" → canonical "Strict" + [Fact] + public void InjectAnalysisKind_Strict_CaseInsensitive() + { + var json = "{\"expression\":\"x > 0\"}"; + var result = TriggerConfigJson.InjectAnalysisKind(json, "STRICT"); + + Assert.NotNull(result); + using var doc = JsonDocument.Parse(result!); + Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString()); + } + + // Other keys preserved when injecting + [Fact] + public void InjectAnalysisKind_OtherKeysPreserved() + { + var json = "{\"expression\":\"x > 0\",\"mode\":\"OnTrue\"}"; + var result = TriggerConfigJson.InjectAnalysisKind(json, "strict"); + + Assert.NotNull(result); + using var doc = JsonDocument.Parse(result!); + Assert.Equal("x > 0", doc.RootElement.GetProperty("expression").GetString()); + Assert.Equal("OnTrue", doc.RootElement.GetProperty("mode").GetString()); + Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString()); + } // ── Option surface: alarm add + update have --trigger-kind ─────────────── @@ -154,4 +214,17 @@ public class TemplateTriggerKindTests using var doc = JsonDocument.Parse(json!); Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString()); } + + private static readonly Option Url = new("--url") { Recursive = true }; + private static readonly Option Username = new("--username") { Recursive = true }; + private static readonly Option Password = new("--password") { Recursive = true }; + private static readonly Option Format = CliOptions.CreateFormatOption(); + + private static Command AlarmGroup() + => TemplateCommands.Build(Url, Format, Username, Password) + .Subcommands.Single(c => c.Name == "alarm"); + + private static Command ScriptGroup() + => TemplateCommands.Build(Url, Format, Username, Password) + .Subcommands.Single(c => c.Name == "script"); } diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/TriggerAnalysisKindTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/TriggerAnalysisKindTests.cs index b04e40c6..eab61f80 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/TriggerAnalysisKindTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/TriggerAnalysisKindTests.cs @@ -240,8 +240,21 @@ public class AlarmTriggerEditorAnalysisKindTests : BunitContext .Add(p => p.Value, @"{""expression"":""x > 0""}")); var select = cut.Find("#alarm-trigger-kind"); - // The selected option must be Advisory (the default) - Assert.Contains("Advisory", select.InnerHtml); + // The bound value must be "Advisory" — not just present in the HTML + Assert.Equal("Advisory", select.GetAttribute("value")); + } + + // Selector defaults to Advisory when config has no analysisKind (ScriptTriggerEditor) + [Fact] + public void ScriptTriggerEditor_Expression_NoAnalysisKindInConfig_SelectorDefaultsAdvisory() + { + var cut = Render(ps => ps + .Add(p => p.TriggerType, "Expression") + .Add(p => p.TriggerConfig, @"{""expression"":""x > 0"",""mode"":""OnTrue""}")); + + var select = cut.Find("#script-trigger-kind"); + // The bound value must be "Advisory" — not just present in the HTML + Assert.Equal("Advisory", select.GetAttribute("value")); } // Choosing Strict writes analysisKind:"Strict" into emitted config