feat(transport): carry IsMesReportable / IsMesReportableOverride through bundles (round-trip guard extended)

Claude-Session: https://claude.ai/code/session_01Y5ef34iUUMjmwnMpL9xgmB
This commit is contained in:
Joseph Doherty
2026-09-11 10:00:00 -04:00
parent af10af1573
commit 43973d34de
8 changed files with 178 additions and 7 deletions
@@ -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()
{