diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs index 136fcc34..66bd823e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs @@ -54,6 +54,9 @@ public static class TagConfigJson /// public static string SetFullName(string? json, string fullName) { + // A blank reference is never a valid Galaxy bind — surface it loudly rather than persisting + // a poisoned "FullName": null/"" that would silently fail to bind at deploy time. + ArgumentException.ThrowIfNullOrWhiteSpace(fullName); var o = ParseOrNew(json); o["FullName"] = JsonValue.Create(fullName); return Serialize(o); diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/GalaxyAddressRepickMergeTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/GalaxyAddressRepickMergeTests.cs index 109456f5..e13c2517 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/GalaxyAddressRepickMergeTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/GalaxyAddressRepickMergeTests.cs @@ -75,6 +75,31 @@ public sealed class GalaxyAddressRepickMergeTests merged.ShouldEndWith("}"); } + [Fact] + public void Repick_with_alarm_flag_does_not_overwrite_existing_alarm_object() + { + // Mirrors the handler's alarm-typed re-pick: SeedDefaultAlarm(SetFullName(existing, addr)). + // The operator authored an alarm, then re-picks an alarm-typed Galaxy attribute — the + // SeedDefaultAlarm no-op guard must leave the edited alarm intact, not re-seed a default. + const string current = + """{"FullName":"Pump_001.OldAttr","alarm":{"alarmType":"LimitAlarm","severity":250,"historizeToAveva":false}}"""; + + var merged = NativeAlarmModel.SeedDefaultAlarm(TagConfigJson.SetFullName(current, "Pump_001.NewAttr")); + + ReadFullName(merged).ShouldBe("Pump_001.NewAttr"); + var alarm = NativeAlarmModel.FromJson(merged); + alarm.IsAlarm.ShouldBeTrue(); + alarm.AlarmType.ShouldBe("LimitAlarm"); + alarm.Severity.ShouldBe(250); + alarm.HistorizeToAveva.ShouldBe(false); + } + + [Fact] + public void SetFullName_rejects_a_blank_reference() + { + Should.Throw(() => TagConfigJson.SetFullName("""{"FullName":"x"}""", " ")); + } + // Mirror of TagModal.ReadFullName — extracts FullName from a Galaxy TagConfig blob. private static string ReadFullName(string json) {