fix(template-engine): DiffService covers native alarm sources — Deployments diff view no longer blind to native-alarm edits
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -12,8 +12,8 @@ public sealed record ConfigurationDiff
|
|||||||
public string? OldRevisionHash { get; init; }
|
public string? OldRevisionHash { get; init; }
|
||||||
/// <summary>Revision hash of the new configuration being compared.</summary>
|
/// <summary>Revision hash of the new configuration being compared.</summary>
|
||||||
public string? NewRevisionHash { get; init; }
|
public string? NewRevisionHash { get; init; }
|
||||||
/// <summary>True when any attribute, alarm, script, or connection changes are present.</summary>
|
/// <summary>True when any attribute, alarm, script, native alarm source, or connection changes are present.</summary>
|
||||||
public bool HasChanges => AttributeChanges.Count > 0 || AlarmChanges.Count > 0 || ScriptChanges.Count > 0 || ConnectionChanges.Count > 0;
|
public bool HasChanges => AttributeChanges.Count > 0 || AlarmChanges.Count > 0 || ScriptChanges.Count > 0 || NativeAlarmSourceChanges.Count > 0 || ConnectionChanges.Count > 0;
|
||||||
|
|
||||||
/// <summary>Diff entries for resolved attributes.</summary>
|
/// <summary>Diff entries for resolved attributes.</summary>
|
||||||
public IReadOnlyList<DiffEntry<ResolvedAttribute>> AttributeChanges { get; init; } = [];
|
public IReadOnlyList<DiffEntry<ResolvedAttribute>> AttributeChanges { get; init; } = [];
|
||||||
@@ -22,6 +22,13 @@ public sealed record ConfigurationDiff
|
|||||||
/// <summary>Diff entries for resolved scripts.</summary>
|
/// <summary>Diff entries for resolved scripts.</summary>
|
||||||
public IReadOnlyList<DiffEntry<ResolvedScript>> ScriptChanges { get; init; } = [];
|
public IReadOnlyList<DiffEntry<ResolvedScript>> ScriptChanges { get; init; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<DiffEntry<ResolvedNativeAlarmSource>> NativeAlarmSourceChanges { get; init; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Diff entries for connection configurations, keyed by connection name.
|
/// Diff entries for connection configurations, keyed by connection name.
|
||||||
/// Surfaces standalone endpoint/protocol/failover drift that does not show
|
/// Surfaces standalone endpoint/protocol/failover drift that does not show
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compares two FlattenedConfigurations (deployed vs current) and produces a ConfigurationDiff
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DiffService
|
public class DiffService
|
||||||
{
|
{
|
||||||
@@ -42,6 +43,12 @@ public class DiffService
|
|||||||
s => s.CanonicalName,
|
s => s.CanonicalName,
|
||||||
ScriptsEqual);
|
ScriptsEqual);
|
||||||
|
|
||||||
|
var nativeAlarmSourceChanges = ComputeEntityDiff(
|
||||||
|
oldConfig?.NativeAlarmSources ?? [],
|
||||||
|
newConfig.NativeAlarmSources,
|
||||||
|
n => n.CanonicalName,
|
||||||
|
NativeAlarmSourcesEqual);
|
||||||
|
|
||||||
// Surface standalone connection endpoint/protocol/
|
// Surface standalone connection endpoint/protocol/
|
||||||
// failover drift. Per-attribute binding changes already show up under
|
// failover drift. Per-attribute binding changes already show up under
|
||||||
// AttributeChanges, but a connection's own ConfigurationJson /
|
// AttributeChanges, but a connection's own ConfigurationJson /
|
||||||
@@ -57,6 +64,7 @@ public class DiffService
|
|||||||
AttributeChanges = attributeChanges,
|
AttributeChanges = attributeChanges,
|
||||||
AlarmChanges = alarmChanges,
|
AlarmChanges = alarmChanges,
|
||||||
ScriptChanges = scriptChanges,
|
ScriptChanges = scriptChanges,
|
||||||
|
NativeAlarmSourceChanges = nativeAlarmSourceChanges,
|
||||||
ConnectionChanges = connectionChanges
|
ConnectionChanges = connectionChanges
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -138,6 +146,13 @@ public class DiffService
|
|||||||
a.TriggerConfiguration == b.TriggerConfiguration &&
|
a.TriggerConfiguration == b.TriggerConfiguration &&
|
||||||
a.OnTriggerScriptCanonicalName == b.OnTriggerScriptCanonicalName;
|
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) =>
|
private static bool ScriptsEqual(ResolvedScript a, ResolvedScript b) =>
|
||||||
a.CanonicalName == b.CanonicalName &&
|
a.CanonicalName == b.CanonicalName &&
|
||||||
a.Code == b.Code &&
|
a.Code == b.Code &&
|
||||||
|
|||||||
@@ -221,6 +221,114 @@ public class DiffServiceTests
|
|||||||
Assert.Equal(DiffChangeType.Changed, diff.AlarmChanges[0].ChangeType);
|
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]
|
[Fact]
|
||||||
public void ConnectionsEqual_IdenticalConfigs_ReturnsTrue()
|
public void ConnectionsEqual_IdenticalConfigs_ReturnsTrue()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user