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()
{