fix(template-engine): resolve TemplateEngine-001/003/004/005, re-triage 002 — recursive composed flattening, fixed-field guard, alarm script refs, dead collision query

This commit is contained in:
Joseph Doherty
2026-05-16 19:57:28 -04:00
parent 71c0564ec0
commit 74aae53500
5 changed files with 506 additions and 130 deletions

View File

@@ -60,6 +60,23 @@ public class TemplateServiceTests
Assert.Equal(1, result.Value.ParentTemplateId);
}
[Fact]
public async Task CreateTemplate_WithParent_DoesNotRunDeadCollisionQuery()
{
// A freshly created child has no members of its own, and the parent's
// members were already collision-validated when they were added — so
// create-time collision detection on a child is a guaranteed no-op.
// The previous code allocated an unused full-table read; the fix
// removes it. This guards against the dead query being reintroduced.
var parent = new Template("Base") { Id = 1 };
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(parent);
var result = await _service.CreateTemplateAsync("Child", null, 1, "admin");
Assert.True(result.IsSuccess);
_repoMock.Verify(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task CreateTemplate_NonexistentParent_Fails()
{
@@ -668,6 +685,54 @@ public class TemplateServiceTests
Assert.True(result.Value.IsLocked);
}
[Fact]
public async Task UpdateAttribute_UnlockedAttribute_DataTypeChangeRejected()
{
// An unlocked attribute must still not be able to change its fixed DataType.
var existing = new TemplateAttribute("Temperature")
{
Id = 1, TemplateId = 1, DataType = DataType.Float, IsLocked = false
};
_repoMock.Setup(r => r.GetTemplateAttributeByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(existing);
var template = new Template("Pump") { Id = 1 };
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
var proposed = new TemplateAttribute("Temperature")
{
DataType = DataType.Int32, IsLocked = false, Value = "42"
};
var result = await _service.UpdateAttributeAsync(1, proposed, "admin");
Assert.True(result.IsFailure);
Assert.Contains("DataType", result.Error);
// The fixed field must not have been mutated.
Assert.Equal(DataType.Float, existing.DataType);
}
[Fact]
public async Task UpdateAttribute_UnlockedAttribute_DataSourceReferenceChangeRejected()
{
var existing = new TemplateAttribute("Temperature")
{
Id = 1, TemplateId = 1, DataType = DataType.Float, IsLocked = false,
DataSourceReference = "/Motor/Temp"
};
_repoMock.Setup(r => r.GetTemplateAttributeByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(existing);
var template = new Template("Pump") { Id = 1 };
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
var proposed = new TemplateAttribute("Temperature")
{
DataType = DataType.Float, IsLocked = false, Value = "42",
DataSourceReference = "/Motor/Other"
};
var result = await _service.UpdateAttributeAsync(1, proposed, "admin");
Assert.True(result.IsFailure);
Assert.Contains("DataSourceReference", result.Error);
Assert.Equal("/Motor/Temp", existing.DataSourceReference);
}
[Fact]
public async Task UpdateAttribute_ParentLocked_CannotOverride()
{