fix(transport): transport LockedInDerived on template attributes/alarms/scripts (additive DTO fields)

Adds a trailing optional `bool LockedInDerived = false` to TemplateAttributeDto,
TemplateAlarmDto and TemplateScriptDto (mirrors the ExecutionTimeoutSeconds
additive precedent — no schemaVersion bump). Populated at export
(ToBundleContent), consumed on import (FromBundleContent, BuildTemplate, and all
three SyncTemplate*Async changed-predicate + copy + new-entity paths, incl. audit
payloads). Old-form bundles that lack the property still deserialize to false.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-09 16:46:33 -04:00
parent ea9c5709ac
commit bf17f60a04
5 changed files with 194 additions and 9 deletions
@@ -5,6 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
@@ -158,6 +159,99 @@ public sealed class EntitySerializerTests
Assert.Equal("MotorA", rtComp.InstanceName);
}
[Fact]
public void Roundtrip_template_preserves_LockedInDerived_on_attribute_alarm_script()
{
var basic = new Template("Basic") { Id = 1 };
basic.Attributes.Add(new TemplateAttribute("Pressure")
{
Id = 1,
TemplateId = 1,
DataType = DataType.Double,
LockedInDerived = true,
});
basic.Alarms.Add(new TemplateAlarm("High")
{
Id = 1,
TemplateId = 1,
TriggerType = AlarmTriggerType.RangeViolation,
LockedInDerived = true,
});
basic.Scripts.Add(new TemplateScript("OnUpdate", "return 1;")
{
Id = 1,
TemplateId = 1,
LockedInDerived = true,
});
var aggregate = MakeEmptyAggregate() with { Templates = new[] { basic } };
var sut = new EntitySerializer();
var dto = sut.ToBundleContent(aggregate);
// Export carries the flag on all three child DTOs.
var dtoBasic = Assert.Single(dto.Templates);
Assert.True(Assert.Single(dtoBasic.Attributes).LockedInDerived);
Assert.True(Assert.Single(dtoBasic.Alarms).LockedInDerived);
Assert.True(Assert.Single(dtoBasic.Scripts).LockedInDerived);
// FromBundleContent restores it onto the entities.
var rtBasic = Assert.Single(sut.FromBundleContent(dto).Templates);
Assert.True(Assert.Single(rtBasic.Attributes).LockedInDerived);
Assert.True(Assert.Single(rtBasic.Alarms).LockedInDerived);
Assert.True(Assert.Single(rtBasic.Scripts).LockedInDerived);
}
[Fact]
public void OldForm_bundle_json_without_LockedInDerived_deserializes_to_false()
{
// A bundle written before LockedInDerived existed: the property is absent
// from every template-child object. It must deserialize to false, not throw.
// PascalCase keys + string enum names — the on-wire form BundleJsonOptions
// produces (no naming policy, JsonStringEnumConverter, case-sensitive). All
// required collections present; LockedInDerived absent on every child.
const string json = """
{
"TemplateFolders": [],
"Templates": [
{
"Name": "Legacy",
"FolderName": null,
"BaseTemplateName": null,
"Description": null,
"Attributes": [
{ "Name": "A", "Value": "1", "DataType": "Double", "IsLocked": false,
"Description": null, "DataSourceReference": null }
],
"Alarms": [
{ "Name": "Alm", "Description": null, "PriorityLevel": 1,
"TriggerType": "RangeViolation", "TriggerConfiguration": null, "IsLocked": false,
"OnTriggerScriptName": null }
],
"Scripts": [
{ "Name": "S", "Code": "return 1;", "TriggerType": null,
"TriggerConfiguration": null, "ParameterDefinitions": null,
"ReturnDefinition": null, "IsLocked": false }
],
"Compositions": []
}
],
"SharedScripts": [],
"ExternalSystems": [],
"DatabaseConnections": [],
"NotificationLists": [],
"SmtpConfigs": [],
"ApiMethods": []
}
""";
var content = JsonSerializer.Deserialize<BundleContentDto>(json, BundleJsonOptions.Default)!;
var template = Assert.Single(content.Templates);
Assert.False(Assert.Single(template.Attributes).LockedInDerived);
Assert.False(Assert.Single(template.Alarms).LockedInDerived);
Assert.False(Assert.Single(template.Scripts).LockedInDerived);
}
[Fact]
public void Roundtrip_template_folder_preserves_hierarchy()
{