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:
@@ -566,6 +566,123 @@ public class TemplateServiceTests
|
||||
Assert.Contains("locked by base template 'Sensor'", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddComposition_CopiesAlarmsAsInherited()
|
||||
{
|
||||
var moduleTemplate = new Template("Module") { Id = 2 };
|
||||
moduleTemplate.Alarms.Add(new TemplateAlarm("HighTemp")
|
||||
{
|
||||
Id = 30,
|
||||
TemplateId = 2,
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
TriggerConfiguration = "{\"attributeName\":\"Temp\",\"high\":100}",
|
||||
PriorityLevel = 5,
|
||||
Description = "Too hot"
|
||||
});
|
||||
var template = new Template("Parent") { Id = 1 };
|
||||
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(moduleTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template, moduleTemplate });
|
||||
|
||||
Template? captured = null;
|
||||
_repoMock.Setup(r => r.AddTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()))
|
||||
.Callback<Template, CancellationToken>((t, _) => captured = t)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var result = await _service.AddCompositionAsync(1, 2, "myModule", "admin");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotNull(captured);
|
||||
Assert.Single(captured!.Alarms);
|
||||
var copied = captured.Alarms.First();
|
||||
Assert.Equal("HighTemp", copied.Name);
|
||||
Assert.True(copied.IsInherited);
|
||||
Assert.False(copied.LockedInDerived);
|
||||
Assert.Equal(AlarmTriggerType.RangeViolation, copied.TriggerType);
|
||||
Assert.Equal(5, copied.PriorityLevel);
|
||||
Assert.Equal("Too hot", copied.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAlarm_LockedInDerivedBase_RejectsOnDerived()
|
||||
{
|
||||
var existing = new TemplateAlarm("HighTemp")
|
||||
{
|
||||
Id = 300,
|
||||
TemplateId = 77,
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
PriorityLevel = 5,
|
||||
IsInherited = true
|
||||
};
|
||||
var baseTemplate = new Template("Sensor") { Id = 2 };
|
||||
baseTemplate.Alarms.Add(new TemplateAlarm("HighTemp")
|
||||
{
|
||||
Id = 30,
|
||||
TemplateId = 2,
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
LockedInDerived = true
|
||||
});
|
||||
var derived = new Template("Parent.slot") { Id = 77, ParentTemplateId = 2, IsDerived = true };
|
||||
|
||||
_repoMock.Setup(r => r.GetTemplateAlarmByIdAsync(300, It.IsAny<CancellationToken>())).ReturnsAsync(existing);
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(77, It.IsAny<CancellationToken>())).ReturnsAsync(derived);
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(baseTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
|
||||
var proposed = new TemplateAlarm("HighTemp")
|
||||
{
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
PriorityLevel = 99,
|
||||
IsInherited = false
|
||||
};
|
||||
var result = await _service.UpdateAlarmAsync(300, proposed, "admin");
|
||||
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Contains("locked by base template 'Sensor'", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAlarm_DerivedOverride_PersistsIsInheritedFalse()
|
||||
{
|
||||
var existing = new TemplateAlarm("HighTemp")
|
||||
{
|
||||
Id = 300,
|
||||
TemplateId = 77,
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
PriorityLevel = 5,
|
||||
IsInherited = true
|
||||
};
|
||||
var baseTemplate = new Template("Sensor") { Id = 2 };
|
||||
baseTemplate.Alarms.Add(new TemplateAlarm("HighTemp")
|
||||
{
|
||||
Id = 30,
|
||||
TemplateId = 2,
|
||||
TriggerType = AlarmTriggerType.RangeViolation
|
||||
});
|
||||
var derived = new Template("Parent.slot") { Id = 77, ParentTemplateId = 2, IsDerived = true };
|
||||
|
||||
_repoMock.Setup(r => r.GetTemplateAlarmByIdAsync(300, It.IsAny<CancellationToken>())).ReturnsAsync(existing);
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(77, It.IsAny<CancellationToken>())).ReturnsAsync(derived);
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(2, It.IsAny<CancellationToken>())).ReturnsAsync(baseTemplate);
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { baseTemplate, derived });
|
||||
|
||||
var proposed = new TemplateAlarm("HighTemp")
|
||||
{
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
PriorityLevel = 99,
|
||||
IsInherited = false
|
||||
};
|
||||
var result = await _service.UpdateAlarmAsync(300, proposed, "admin");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.False(result.Value.IsInherited);
|
||||
Assert.Equal(99, result.Value.PriorityLevel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAttribute_DerivedOverride_PersistsIsInheritedFalse()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user