feat(transport): carry IsMesReportable / IsMesReportableOverride through bundles (round-trip guard extended)
Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
This commit is contained in:
@@ -238,6 +238,7 @@ public sealed class RoundTripEquivalenceTests : IDisposable
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
TriggerConfiguration = "{\"threshold\":100}",
|
||||
LockedInDerived = true,
|
||||
IsMesReportable = true,
|
||||
});
|
||||
tpl.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Boiler")
|
||||
{
|
||||
@@ -809,6 +810,9 @@ public sealed class RoundTripEquivalenceTests : IDisposable
|
||||
{
|
||||
TriggerConfigurationOverride = "{\"hi\":90}",
|
||||
PriorityLevelOverride = 7,
|
||||
// Non-default for a bool? (whose default is null), so the reflection
|
||||
// sweep fails loudly if the DTO drops the field.
|
||||
IsMesReportableOverride = false,
|
||||
});
|
||||
instance.NativeAlarmSourceOverrides.Add(new InstanceNativeAlarmSourceOverride("NativeSrc")
|
||||
{
|
||||
|
||||
@@ -396,6 +396,53 @@ public sealed class ArtifactDiffTests
|
||||
ChangeFor(item, "Alarms.A"); // OnTriggerScriptName diverged — previously missed.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareTemplate_AlarmIsMesReportableChange_IsModified()
|
||||
{
|
||||
var existing = MakeTemplate("T1");
|
||||
existing.Alarms.Add(new TemplateAlarm("A")
|
||||
{
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
IsMesReportable = false,
|
||||
});
|
||||
var incoming = MakeTemplateDto("T1", alarms:
|
||||
[
|
||||
// every other alarm field identical; only IsMesReportable differs.
|
||||
new TemplateAlarmDto("A", null, 0, AlarmTriggerType.RangeViolation, null, false, null,
|
||||
IsMesReportable: true),
|
||||
]);
|
||||
|
||||
var item = _diff.CompareTemplate(incoming, existing);
|
||||
|
||||
Assert.Equal(ConflictKind.Modified, item.Kind);
|
||||
ChangeFor(item, "Alarms.A"); // IsMesReportable diverged.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareInstance_AlarmOverrideIsMesReportableChange_IsModified()
|
||||
{
|
||||
var existing = new Instance("inst-1") { State = InstanceState.Enabled };
|
||||
existing.AlarmOverrides.Add(new InstanceAlarmOverride("HiAlarm")
|
||||
{
|
||||
TriggerConfigurationOverride = "{\"hi\":90}",
|
||||
PriorityLevelOverride = 7,
|
||||
IsMesReportableOverride = null,
|
||||
});
|
||||
|
||||
var dto = MakeInstanceDto("inst-1", state: InstanceState.Enabled, alarmOverrides:
|
||||
[
|
||||
// identical apart from the MES-reportable override.
|
||||
new InstanceAlarmOverrideDto("HiAlarm", "{\"hi\":90}", 7, IsMesReportableOverride: false),
|
||||
]);
|
||||
|
||||
var item = _diff.CompareInstance(dto, existing);
|
||||
|
||||
Assert.Equal(ConflictKind.Modified, item.Kind);
|
||||
var modified = ChangeFor(item, "AlarmOverrides.HiAlarm");
|
||||
Assert.Equal("<modified>", modified.GetProperty("oldValue").GetString());
|
||||
Assert.Equal("<modified>", modified.GetProperty("newValue").GetString());
|
||||
}
|
||||
|
||||
// ============ M8: CompareSite ============
|
||||
|
||||
[Fact]
|
||||
@@ -699,7 +746,8 @@ public sealed class ArtifactDiffTests
|
||||
string template = "Tmpl",
|
||||
string site = "site-a",
|
||||
InstanceState state = InstanceState.Enabled,
|
||||
IReadOnlyList<InstanceAttributeOverrideDto>? attributeOverrides = null) =>
|
||||
IReadOnlyList<InstanceAttributeOverrideDto>? attributeOverrides = null,
|
||||
IReadOnlyList<InstanceAlarmOverrideDto>? alarmOverrides = null) =>
|
||||
new(
|
||||
UniqueName: uniqueName,
|
||||
TemplateName: template,
|
||||
@@ -707,7 +755,7 @@ public sealed class ArtifactDiffTests
|
||||
AreaName: null,
|
||||
State: state,
|
||||
AttributeOverrides: attributeOverrides ?? Array.Empty<InstanceAttributeOverrideDto>(),
|
||||
AlarmOverrides: Array.Empty<InstanceAlarmOverrideDto>(),
|
||||
AlarmOverrides: alarmOverrides ?? Array.Empty<InstanceAlarmOverrideDto>(),
|
||||
NativeAlarmSourceOverrides: Array.Empty<InstanceNativeAlarmSourceOverrideDto>(),
|
||||
ConnectionBindings: Array.Empty<InstanceConnectionBindingDto>());
|
||||
}
|
||||
|
||||
@@ -294,6 +294,106 @@ public sealed class EntitySerializerTests
|
||||
Assert.False(Assert.Single(template.Scripts).LockedInDerived);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Roundtrip_preserves_IsMesReportable_on_alarm_and_instance_override()
|
||||
{
|
||||
var basic = new Template("Basic") { Id = 1 };
|
||||
basic.Alarms.Add(new TemplateAlarm("High")
|
||||
{
|
||||
Id = 1,
|
||||
TemplateId = 1,
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
IsMesReportable = true,
|
||||
});
|
||||
|
||||
var instance = new Instance("Pump-01") { Id = 1, TemplateId = 1, SiteId = 1 };
|
||||
instance.AlarmOverrides.Add(new InstanceAlarmOverride("High")
|
||||
{
|
||||
InstanceId = 1,
|
||||
PriorityLevelOverride = 7,
|
||||
IsMesReportableOverride = false,
|
||||
});
|
||||
|
||||
var aggregate = MakeEmptyAggregate() with
|
||||
{
|
||||
Templates = new[] { basic },
|
||||
Sites = new[] { new Site("Site A", "site-a") { Id = 1 } },
|
||||
Instances = new[] { instance },
|
||||
};
|
||||
|
||||
var sut = new EntitySerializer();
|
||||
var dto = sut.ToBundleContent(aggregate);
|
||||
|
||||
Assert.True(Assert.Single(Assert.Single(dto.Templates).Alarms).IsMesReportable);
|
||||
Assert.False(Assert.Single(Assert.Single(dto.Instances).AlarmOverrides).IsMesReportableOverride);
|
||||
|
||||
var restored = sut.FromBundleContent(dto);
|
||||
Assert.True(Assert.Single(Assert.Single(restored.Templates).Alarms).IsMesReportable);
|
||||
Assert.False(Assert.Single(Assert.Single(restored.Instances).AlarmOverrides).IsMesReportableOverride);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OldForm_bundle_json_without_IsMesReportable_deserializes_to_false_and_null()
|
||||
{
|
||||
// A bundle written before the MES-reportable flag existed: the property is
|
||||
// absent from the template alarm and from the instance alarm override. The
|
||||
// DTO fields are additive trailing parameters, so the alarm flag must land
|
||||
// on false and the override on null (= "keep inherited"), never throw.
|
||||
const string json = """
|
||||
{
|
||||
"TemplateFolders": [],
|
||||
"Templates": [
|
||||
{
|
||||
"Name": "Legacy",
|
||||
"FolderName": null,
|
||||
"BaseTemplateName": null,
|
||||
"Description": null,
|
||||
"Attributes": [],
|
||||
"Alarms": [
|
||||
{ "Name": "Alm", "Description": null, "PriorityLevel": 1,
|
||||
"TriggerType": "RangeViolation", "TriggerConfiguration": null, "IsLocked": false,
|
||||
"OnTriggerScriptName": null, "LockedInDerived": true }
|
||||
],
|
||||
"Scripts": [],
|
||||
"Compositions": []
|
||||
}
|
||||
],
|
||||
"SharedScripts": [],
|
||||
"ExternalSystems": [],
|
||||
"DatabaseConnections": [],
|
||||
"NotificationLists": [],
|
||||
"SmtpConfigs": [],
|
||||
"ApiMethods": [],
|
||||
"Instances": [
|
||||
{
|
||||
"UniqueName": "Pump-01",
|
||||
"TemplateName": "Legacy",
|
||||
"SiteIdentifier": "site-a",
|
||||
"AreaName": null,
|
||||
"State": "Enabled",
|
||||
"AttributeOverrides": [],
|
||||
"AlarmOverrides": [
|
||||
{ "AlarmCanonicalName": "Alm", "TriggerConfigurationOverride": null,
|
||||
"PriorityLevelOverride": 3 }
|
||||
],
|
||||
"NativeAlarmSourceOverrides": [],
|
||||
"ConnectionBindings": []
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
|
||||
var content = JsonSerializer.Deserialize<BundleContentDto>(json, BundleJsonOptions.Default)!;
|
||||
|
||||
var alarm = Assert.Single(Assert.Single(content.Templates).Alarms);
|
||||
Assert.True(alarm.LockedInDerived); // pre-existing trailing field still read
|
||||
Assert.False(alarm.IsMesReportable); // new trailing field defaults
|
||||
|
||||
var over = Assert.Single(Assert.Single(content.Instances).AlarmOverrides);
|
||||
Assert.Equal(3, over.PriorityLevelOverride);
|
||||
Assert.Null(over.IsMesReportableOverride);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Roundtrip_template_folder_preserves_hierarchy()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user