feat(template-engine): resolve TemplateEngine-002 — per-slot alarm override for derived templates

Adds IsInherited/LockedInDerived to the TemplateAlarm entity (mirroring the
attribute/script override model), an EF migration, base-alarm copy-on-derive,
inherited-alarm flattening skip, and LockedInDerived override-rejection validation.
This commit is contained in:
Joseph Doherty
2026-05-16 20:12:24 -04:00
parent bc548e1447
commit 305b42ea6d
9 changed files with 1700 additions and 21 deletions

View File

@@ -371,6 +371,110 @@ public class FlatteningServiceTests
Assert.Equal("return base;", script.Code);
}
// ── TemplateEngine-002: per-slot alarm override ────────────────────────
[Fact]
public void Flatten_InheritedAlarmOnDerived_BaseValueWins()
{
var baseTemplate = CreateTemplate(2, "Sensor");
baseTemplate.Alarms.Add(new TemplateAlarm("HighTemp")
{
TriggerType = AlarmTriggerType.RangeViolation,
TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}",
PriorityLevel = 5
});
var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
derived.Alarms.Add(new TemplateAlarm("HighTemp")
{
TriggerType = AlarmTriggerType.RangeViolation,
TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":999}",
PriorityLevel = 99,
IsInherited = true
});
var instance = CreateInstance();
var result = _sut.Flatten(
instance,
[derived, baseTemplate],
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
new Dictionary<int, IReadOnlyList<Template>>(),
new Dictionary<int, DataConnection>());
Assert.True(result.IsSuccess);
var alarm = result.Value.Alarms.First(a => a.CanonicalName == "HighTemp");
Assert.Equal(5, alarm.PriorityLevel);
Assert.Equal("{\"attributeName\":\"Temp\",\"high\":100}", alarm.TriggerConfiguration);
}
[Fact]
public void Flatten_OverriddenAlarmOnDerived_DerivedValueWins()
{
var baseTemplate = CreateTemplate(2, "Sensor");
baseTemplate.Alarms.Add(new TemplateAlarm("HighTemp")
{
TriggerType = AlarmTriggerType.RangeViolation,
TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}",
PriorityLevel = 5
});
var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
derived.Alarms.Add(new TemplateAlarm("HighTemp")
{
TriggerType = AlarmTriggerType.RangeViolation,
TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":120}",
PriorityLevel = 42,
IsInherited = false
});
var instance = CreateInstance();
var result = _sut.Flatten(
instance,
[derived, baseTemplate],
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
new Dictionary<int, IReadOnlyList<Template>>(),
new Dictionary<int, DataConnection>());
Assert.True(result.IsSuccess);
var alarm = result.Value.Alarms.First(a => a.CanonicalName == "HighTemp");
Assert.Equal(42, alarm.PriorityLevel);
Assert.Equal("{\"attributeName\":\"Temp\",\"high\":120}", alarm.TriggerConfiguration);
}
[Fact]
public void Flatten_LockedInDerivedAlarmOverride_Fails()
{
var baseTemplate = CreateTemplate(2, "Sensor");
baseTemplate.Alarms.Add(new TemplateAlarm("HighTemp")
{
TriggerType = AlarmTriggerType.RangeViolation,
TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}",
PriorityLevel = 5,
LockedInDerived = true
});
var derived = CreateTemplate(1, "Pump.TempSensor", parentId: 2);
derived.Alarms.Add(new TemplateAlarm("HighTemp")
{
TriggerType = AlarmTriggerType.RangeViolation,
TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":120}",
PriorityLevel = 42,
IsInherited = false
});
var instance = CreateInstance();
var result = _sut.Flatten(
instance,
[derived, baseTemplate],
new Dictionary<int, IReadOnlyList<TemplateComposition>>(),
new Dictionary<int, IReadOnlyList<Template>>(),
new Dictionary<int, DataConnection>());
Assert.True(result.IsFailure);
Assert.Contains("LockedInDerived", result.Error);
Assert.Contains("HighTemp", result.Error);
}
// ── TemplateEngine-001: deep composition nesting ───────────────────────
[Fact]