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
@@ -197,4 +197,61 @@ public sealed class TemplateScriptFidelityTests : IDisposable
a.Action == "TemplateScriptUpdated" && a.EntityName == "Pump.cadence"));
}
}
[Fact]
public async Task Add_preserves_LockedInDerived_on_attribute_alarm_script()
{
// #05-T4: LockedInDerived must survive export→import on all three child kinds.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var t = new Template("Pump") { Description = "fresh" };
t.Attributes.Add(new TemplateAttribute("Pressure")
{
DataType = DataType.Double,
LockedInDerived = true,
});
t.Alarms.Add(new TemplateAlarm("High")
{
TriggerType = AlarmTriggerType.RangeViolation,
LockedInDerived = true,
});
t.Scripts.Add(new TemplateScript("cadence", "return 1;")
{
LockedInDerived = true,
});
ctx.Templates.Add(t);
await ctx.SaveChangesAsync();
}
var sessionId = await ExportAndLoadAsync();
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
ctx.Templates.RemoveRange(ctx.Templates);
await ctx.SaveChangesAsync();
}
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
await importer.ApplyAsync(sessionId,
new List<ImportResolution> { new("Template", "Pump", ResolutionAction.Add, null) },
user: "bob");
}
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var t = await ctx.Templates
.Where(t => t.Name == "Pump")
.Include(t => t.Attributes)
.Include(t => t.Alarms)
.Include(t => t.Scripts)
.SingleAsync();
Assert.True(t.Attributes.Single(a => a.Name == "Pressure").LockedInDerived);
Assert.True(t.Alarms.Single(a => a.Name == "High").LockedInDerived);
Assert.True(t.Scripts.Single(s => s.Name == "cadence").LockedInDerived);
}
}
}