From d833219920c9300b7c44bfd5c06dfbfa18a57b71 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 9 Jul 2026 16:42:00 -0400 Subject: [PATCH] =?UTF-8?q?fix(template-engine):=20DiffService=20covers=20?= =?UTF-8?q?native=20alarm=20sources=20=E2=80=94=20Deployments=20diff=20vie?= =?UTF-8?q?w=20no=20longer=20blind=20to=20native-alarm=20edits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj --- .../Types/Flattening/ConfigurationDiff.cs | 11 +- .../Flattening/DiffService.cs | 17 ++- .../Flattening/DiffServiceTests.cs | 108 ++++++++++++++++++ 3 files changed, 133 insertions(+), 3 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ConfigurationDiff.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ConfigurationDiff.cs index d36b169c..46730a91 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ConfigurationDiff.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Flattening/ConfigurationDiff.cs @@ -12,8 +12,8 @@ public sealed record ConfigurationDiff public string? OldRevisionHash { get; init; } /// Revision hash of the new configuration being compared. public string? NewRevisionHash { get; init; } - /// True when any attribute, alarm, script, or connection changes are present. - public bool HasChanges => AttributeChanges.Count > 0 || AlarmChanges.Count > 0 || ScriptChanges.Count > 0 || ConnectionChanges.Count > 0; + /// True when any attribute, alarm, script, native alarm source, or connection changes are present. + public bool HasChanges => AttributeChanges.Count > 0 || AlarmChanges.Count > 0 || ScriptChanges.Count > 0 || NativeAlarmSourceChanges.Count > 0 || ConnectionChanges.Count > 0; /// Diff entries for resolved attributes. public IReadOnlyList> AttributeChanges { get; init; } = []; @@ -22,6 +22,13 @@ public sealed record ConfigurationDiff /// Diff entries for resolved scripts. public IReadOnlyList> ScriptChanges { get; init; } = []; + /// + /// Diff entries for resolved native alarm source bindings, keyed by canonical + /// name. Surfaces native-alarm-source edits (connection, source reference, + /// condition filter, lock state) in the Deployments diff view. + /// + public IReadOnlyList> NativeAlarmSourceChanges { get; init; } = []; + /// /// Diff entries for connection configurations, keyed by connection name. /// Surfaces standalone endpoint/protocol/failover drift that does not show diff --git a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs index 97f1270d..7cea9049 100644 --- a/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Flattening/DiffService.cs @@ -4,7 +4,8 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening; /// /// Compares two FlattenedConfigurations (deployed vs current) and produces a ConfigurationDiff -/// showing Added, Removed, and Changed entries for attributes, alarms, and scripts. +/// showing Added, Removed, and Changed entries for attributes, alarms, scripts, +/// and native alarm sources. /// public class DiffService { @@ -42,6 +43,12 @@ public class DiffService s => s.CanonicalName, ScriptsEqual); + var nativeAlarmSourceChanges = ComputeEntityDiff( + oldConfig?.NativeAlarmSources ?? [], + newConfig.NativeAlarmSources, + n => n.CanonicalName, + NativeAlarmSourcesEqual); + // Surface standalone connection endpoint/protocol/ // failover drift. Per-attribute binding changes already show up under // AttributeChanges, but a connection's own ConfigurationJson / @@ -57,6 +64,7 @@ public class DiffService AttributeChanges = attributeChanges, AlarmChanges = alarmChanges, ScriptChanges = scriptChanges, + NativeAlarmSourceChanges = nativeAlarmSourceChanges, ConnectionChanges = connectionChanges }; } @@ -138,6 +146,13 @@ public class DiffService a.TriggerConfiguration == b.TriggerConfiguration && a.OnTriggerScriptCanonicalName == b.OnTriggerScriptCanonicalName; + private static bool NativeAlarmSourcesEqual(ResolvedNativeAlarmSource a, ResolvedNativeAlarmSource b) => + a.CanonicalName == b.CanonicalName && + a.ConnectionName == b.ConnectionName && + a.SourceReference == b.SourceReference && + a.ConditionFilter == b.ConditionFilter && + a.IsLocked == b.IsLocked; + private static bool ScriptsEqual(ResolvedScript a, ResolvedScript b) => a.CanonicalName == b.CanonicalName && a.Code == b.Code && diff --git a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DiffServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DiffServiceTests.cs index ef83f798..e1e7e6a3 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DiffServiceTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests/Flattening/DiffServiceTests.cs @@ -221,6 +221,114 @@ public class DiffServiceTests Assert.Equal(DiffChangeType.Changed, diff.AlarmChanges[0].ChangeType); } + [Fact] + public void ComputeDiff_NativeAlarmSourceReferenceChange_DetectedAsChanged() + { + // PLAN-05 Task 12: native alarm source edits must surface in the diff so + // the Deployments diff view is not blind to native-alarm binding changes. + var oldConfig = new FlattenedConfiguration + { + InstanceUniqueName = "Instance1", + NativeAlarmSources = + [ + new ResolvedNativeAlarmSource { CanonicalName = "PumpAlarms", ConnectionName = "Opc1", SourceReference = "ns=2;s=PumpA" } + ] + }; + var newConfig = new FlattenedConfiguration + { + InstanceUniqueName = "Instance1", + NativeAlarmSources = + [ + new ResolvedNativeAlarmSource { CanonicalName = "PumpAlarms", ConnectionName = "Opc1", SourceReference = "ns=2;s=PumpB" } + ] + }; + + var diff = _sut.ComputeDiff(oldConfig, newConfig); + + Assert.True(diff.HasChanges); + Assert.Single(diff.NativeAlarmSourceChanges); + Assert.Equal(DiffChangeType.Changed, diff.NativeAlarmSourceChanges[0].ChangeType); + Assert.Equal("PumpAlarms", diff.NativeAlarmSourceChanges[0].CanonicalName); + } + + [Fact] + public void ComputeDiff_NativeAlarmSourceAdded_DetectedAsAdded() + { + var oldConfig = new FlattenedConfiguration { InstanceUniqueName = "Instance1" }; + var newConfig = new FlattenedConfiguration + { + InstanceUniqueName = "Instance1", + NativeAlarmSources = + [ + new ResolvedNativeAlarmSource { CanonicalName = "PumpAlarms", ConnectionName = "Opc1", SourceReference = "ns=2;s=PumpA" } + ] + }; + + var diff = _sut.ComputeDiff(oldConfig, newConfig); + + Assert.True(diff.HasChanges); + Assert.Single(diff.NativeAlarmSourceChanges); + Assert.Equal(DiffChangeType.Added, diff.NativeAlarmSourceChanges[0].ChangeType); + } + + [Fact] + public void ComputeDiff_NativeAlarmSourceRemoved_DetectedAsRemoved() + { + var oldConfig = new FlattenedConfiguration + { + InstanceUniqueName = "Instance1", + NativeAlarmSources = + [ + new ResolvedNativeAlarmSource { CanonicalName = "PumpAlarms", ConnectionName = "Opc1", SourceReference = "ns=2;s=PumpA" } + ] + }; + var newConfig = new FlattenedConfiguration { InstanceUniqueName = "Instance1" }; + + var diff = _sut.ComputeDiff(oldConfig, newConfig); + + Assert.True(diff.HasChanges); + Assert.Single(diff.NativeAlarmSourceChanges); + Assert.Equal(DiffChangeType.Removed, diff.NativeAlarmSourceChanges[0].ChangeType); + } + + [Fact] + public void ComputeDiff_NativeAlarmSourceLockChange_DetectedAsChanged() + { + var oldConfig = new FlattenedConfiguration + { + InstanceUniqueName = "Instance1", + NativeAlarmSources = + [ + new ResolvedNativeAlarmSource { CanonicalName = "PumpAlarms", ConnectionName = "Opc1", SourceReference = "ns=2;s=PumpA", IsLocked = false } + ] + }; + var newConfig = new FlattenedConfiguration + { + InstanceUniqueName = "Instance1", + NativeAlarmSources = + [ + new ResolvedNativeAlarmSource { CanonicalName = "PumpAlarms", ConnectionName = "Opc1", SourceReference = "ns=2;s=PumpA", IsLocked = true } + ] + }; + + var diff = _sut.ComputeDiff(oldConfig, newConfig); + + Assert.Single(diff.NativeAlarmSourceChanges); + Assert.Equal(DiffChangeType.Changed, diff.NativeAlarmSourceChanges[0].ChangeType); + } + + [Fact] + public void ComputeDiff_NativeAlarmSourceUnchanged_NoDiff() + { + var source = new ResolvedNativeAlarmSource { CanonicalName = "PumpAlarms", ConnectionName = "Opc1", SourceReference = "ns=2;s=PumpA", ConditionFilter = "HighHigh" }; + var oldConfig = new FlattenedConfiguration { InstanceUniqueName = "Instance1", NativeAlarmSources = [source] }; + var newConfig = new FlattenedConfiguration { InstanceUniqueName = "Instance1", NativeAlarmSources = [source with { }] }; + + var diff = _sut.ComputeDiff(oldConfig, newConfig); + + Assert.Empty(diff.NativeAlarmSourceChanges); + } + [Fact] public void ConnectionsEqual_IdenticalConfigs_ReturnsTrue() {