using System.CommandLine; using System.Text.Json; using ZB.MOM.WW.ScadaBridge.CLI; using ZB.MOM.WW.ScadaBridge.CLI.Commands; namespace ZB.MOM.WW.ScadaBridge.CLI.Tests.Commands; /// /// M9-T28b: template alarm add/update and template script add/update /// must expose a --trigger-kind option (advisory|strict) that writes /// "analysisKind":"Strict" into the trigger-config JSON for expression /// triggers, matching the T28a backend contract exactly. /// public class TemplateTriggerKindTests { // ── TriggerConfigJson.InjectAnalysisKind — the shared helper used by // alarm-update / script-add / script-update ────────────────────────────── // 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"); 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 ─────────────── [Fact] public void AlarmAdd_HasTriggerKindOption() { var add = AlarmGroup().Subcommands.Single(c => c.Name == "add"); Assert.Contains("--trigger-kind", add.Options.Select(o => o.Name)); } [Fact] public void AlarmUpdate_HasTriggerKindOption() { var update = AlarmGroup().Subcommands.Single(c => c.Name == "update"); Assert.Contains("--trigger-kind", update.Options.Select(o => o.Name)); } // ── Option surface: script add + update have --trigger-kind ───────────── [Fact] public void ScriptAdd_HasTriggerKindOption() { var add = ScriptGroup().Subcommands.Single(c => c.Name == "add"); Assert.Contains("--trigger-kind", add.Options.Select(o => o.Name)); } [Fact] public void ScriptUpdate_HasTriggerKindOption() { var update = ScriptGroup().Subcommands.Single(c => c.Name == "update"); Assert.Contains("--trigger-kind", update.Options.Select(o => o.Name)); } // ── AlarmTriggerConfigJson.Build — analysisKind injection ──────────────── [Fact] public void AlarmConfigBuild_Expression_StrictKind_InjectsAnalysisKindStrict() { // --trigger-kind strict with --expression set → config carries analysisKind:"Strict" var json = AlarmTriggerConfigJson.Build( triggerType: "Expression", attribute: null, matchValue: null, notEquals: false, min: null, max: null, thresholdPerSecond: null, windowSeconds: null, direction: null, loLo: null, lo: null, hi: null, hiHi: null, expression: "Temp > 80", analysisKind: "strict"); Assert.NotNull(json); using var doc = JsonDocument.Parse(json!); Assert.Equal("Strict", doc.RootElement.GetProperty("analysisKind").GetString()); } [Fact] public void AlarmConfigBuild_Expression_AdvisoryKind_OmitsAnalysisKindKey() { // --trigger-kind advisory (default) → key omitted var json = AlarmTriggerConfigJson.Build( triggerType: "Expression", attribute: null, matchValue: null, notEquals: false, min: null, max: null, thresholdPerSecond: null, windowSeconds: null, direction: null, loLo: null, lo: null, hi: null, hiHi: null, expression: "Temp > 80", analysisKind: "advisory"); Assert.NotNull(json); using var doc = JsonDocument.Parse(json!); Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _)); } [Fact] public void AlarmConfigBuild_Expression_NullKind_OmitsAnalysisKindKey() { // Omitted --trigger-kind → null → Advisory default → key omitted var json = AlarmTriggerConfigJson.Build( triggerType: "Expression", attribute: null, matchValue: null, notEquals: false, min: null, max: null, thresholdPerSecond: null, windowSeconds: null, direction: null, loLo: null, lo: null, hi: null, hiHi: null, expression: "Temp > 80", analysisKind: null); Assert.NotNull(json); using var doc = JsonDocument.Parse(json!); Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _)); } [Fact] public void AlarmConfigBuild_NonExpression_StrictKind_DoesNotEmitAnalysisKind() { // analysisKind is meaningless for non-expression triggers — must be suppressed var json = AlarmTriggerConfigJson.Build( triggerType: "RangeViolation", attribute: "Temp", matchValue: null, notEquals: false, min: 0, max: 100, thresholdPerSecond: null, windowSeconds: null, direction: null, loLo: null, lo: null, hi: null, hiHi: null, expression: null, analysisKind: "strict"); Assert.NotNull(json); using var doc = JsonDocument.Parse(json!); Assert.False(doc.RootElement.TryGetProperty("analysisKind", out _)); } [Fact] public void AlarmConfigBuild_Expression_StrictKind_CaseInsensitive() { // "STRICT" (uppercase) must also produce the canonical "Strict" value var json = AlarmTriggerConfigJson.Build( triggerType: "Expression", attribute: null, matchValue: null, notEquals: false, min: null, max: null, thresholdPerSecond: null, windowSeconds: null, direction: null, loLo: null, lo: null, hi: null, hiHi: null, expression: "x > 0", analysisKind: "STRICT"); Assert.NotNull(json); 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"); }