fix(transport): transport template NativeAlarmSources — imports no longer amputate native alarm mirrors nor dangle instance overrides

New TemplateNativeAlarmSourceDto + init-only TemplateDto.NativeAlarmSources (empty
default, additive — IsInherited placeholders carried for the collision detector).
Exported in ToBundleContent, consumed in FromBundleContent + BuildTemplate, and a
new SyncTemplateNativeAlarmSourcesAsync runs the Overwrite add/update/delete child
sync with per-change audit rows. ArtifactDiff.CompareTemplate now DiffChildren over
native sources so Preview reports a NativeAlarmSources field change. Proven by an
end-to-end flatten test: an instance native-alarm-source override that dangled
pre-fix now resolves against the re-imported template source.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-09 16:59:37 -04:00
parent 4df5109742
commit e4136cb920
7 changed files with 589 additions and 3 deletions
@@ -202,6 +202,46 @@ public sealed class EntitySerializerTests
Assert.True(Assert.Single(rtBasic.Scripts).LockedInDerived);
}
[Fact]
public void Roundtrip_template_preserves_NativeAlarmSources()
{
var basic = new Template("Basic") { Id = 1 };
basic.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Boiler")
{
Id = 1,
TemplateId = 1,
Description = "boiler alarms",
ConnectionName = "OpcUaPrimary",
SourceReference = "ns=3;s=Boiler.Alarm",
ConditionFilter = "severity>500",
IsLocked = true,
IsInherited = false,
LockedInDerived = true,
});
var aggregate = MakeEmptyAggregate() with { Templates = new[] { basic } };
var sut = new EntitySerializer();
var dto = sut.ToBundleContent(aggregate);
var dtoSrc = Assert.Single(Assert.Single(dto.Templates).NativeAlarmSources);
Assert.Equal("Boiler", dtoSrc.Name);
Assert.Equal("OpcUaPrimary", dtoSrc.ConnectionName);
Assert.Equal("ns=3;s=Boiler.Alarm", dtoSrc.SourceReference);
Assert.Equal("severity>500", dtoSrc.ConditionFilter);
Assert.True(dtoSrc.IsLocked);
Assert.True(dtoSrc.LockedInDerived);
var rtSrc = Assert.Single(Assert.Single(sut.FromBundleContent(dto).Templates).NativeAlarmSources);
Assert.Equal("Boiler", rtSrc.Name);
Assert.Equal("boiler alarms", rtSrc.Description);
Assert.Equal("OpcUaPrimary", rtSrc.ConnectionName);
Assert.Equal("ns=3;s=Boiler.Alarm", rtSrc.SourceReference);
Assert.Equal("severity>500", rtSrc.ConditionFilter);
Assert.True(rtSrc.IsLocked);
Assert.True(rtSrc.LockedInDerived);
}
[Fact]
public void OldForm_bundle_json_without_LockedInDerived_deserializes_to_false()
{