feat(native-alarms): per-binding IsMesReportable on native alarm source bindings (ScadaBridge#59)

TemplateNativeAlarmSource.IsMesReportable + tri-state InstanceNativeAlarmSourceOverride
.IsMesReportableOverride flatten onto ResolvedNativeAlarmSource; NativeAlarmActor stamps every
mirrored condition and InstanceActor the quiet-binding placeholder, so ScriptAlarm,
Alarms.MesReportableAsync() and the #60 wire fields carry it. Additive to the 900-999 severity band.
Migration AddNativeAlarmSourceMesReportable. Management commands, CLI --mes-reportable, Central UI
(template dialog checkbox, instance override select), Transport DTOs/importer/diff and the revision
hash (true-only, so existing hashes are unchanged) carry it.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 15:09:54 -05:00
parent 69a2017000
commit f98369a786
38 changed files with 2579 additions and 43 deletions
@@ -952,6 +952,31 @@ public class FlatteningServiceTests
Assert.Equal("Override", result.Value.NativeAlarmSources[0].Source);
}
/// <summary>Gitea #59: the binding flag flattens from the template and a tri-state instance override wins (null inherits).</summary>
[Theory]
[InlineData(true, null, true)]
[InlineData(false, null, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
public void Flatten_NativeAlarmSource_IsMesReportable_TemplateFlagAndOverride(bool templateFlag, bool? ovr, bool expected)
{
var template = CreateTemplate(1, "Base");
template.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Pressure")
{ ConnectionName = "Opc", SourceReference = "ns=2;s=P1", IsMesReportable = templateFlag });
var instance = CreateInstance();
if (ovr.HasValue)
instance.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("Pressure")
{ IsMesReportableOverride = ovr });
var result = _sut.Flatten(instance, [template],
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
new Dictionary<int, IReadOnlyList<Template>>(),
new Dictionary<int, DataConnection>());
Assert.True(result.IsSuccess);
Assert.Equal(expected, result.Value.NativeAlarmSources[0].IsMesReportable);
}
/// <summary>Verifies that a locked native alarm source ignores an instance override.</summary>
[Fact]
public void Flatten_LockedNativeAlarmSource_IgnoresInstanceOverride()
@@ -413,6 +413,29 @@ public class RevisionHashServiceTests
Assert.NotEqual(_sut.ComputeHash(configBefore), _sut.ComputeHash(configAfter));
}
/// <summary>Gitea #59: the binding's MES-reportable flag participates in the hash; false hashes as before the field existed.</summary>
[Fact]
public void ComputeHash_NativeAlarmSourceIsMesReportableChange_ChangesHash()
{
var baseSource = new ResolvedNativeAlarmSource
{
CanonicalName = "Boiler.Alarms",
ConnectionName = "plc1",
SourceReference = "ns=2;s=Boiler"
};
var configFalse = new FlattenedConfiguration
{
InstanceUniqueName = "Instance1", TemplateId = 1, SiteId = 1,
NativeAlarmSources = [baseSource]
};
var configTrue = configFalse with { NativeAlarmSources = [baseSource with { IsMesReportable = true }] };
Assert.NotEqual(_sut.ComputeHash(configFalse), _sut.ComputeHash(configTrue));
// false is omitted from the canonical JSON, so adding the field cannot invalidate stored hashes.
Assert.Equal(_sut.ComputeHash(configFalse),
_sut.ComputeHash(configFalse with { NativeAlarmSources = [baseSource with { IsMesReportable = false }] }));
}
/// <summary>Verifies that changing a native alarm source's locked flag changes the hash.</summary>
[Fact]
public void ComputeHash_NativeAlarmSourceIsLockedChange_ChangesHash()