fix(template-engine): resolve TemplateEngine-015,016 — cascade-rename nested derived templates, correct composed-script ParentPath

This commit is contained in:
Joseph Doherty
2026-05-17 03:18:41 -04:00
parent 0135a6b2a6
commit d6221419c6
5 changed files with 177 additions and 14 deletions

View File

@@ -466,6 +466,67 @@ public class TemplateServiceTests
Assert.Equal("Pump.NewSlot", derived.Name);
}
[Fact]
public async Task RenameComposition_CascadesRenameToNestedDerivedTemplates()
{
// Pump.TempSensor is the slot-owned derived; Pump.TempSensor.Probe1 is a
// cascaded inner derived under it. Renaming the TempSensor slot to
// MainSensor must rename BOTH derived templates so the dotted-path
// naming invariant holds: Pump.MainSensor and Pump.MainSensor.Probe1.
var innerComp = new TemplateComposition("Probe1") { Id = 51, TemplateId = 77, ComposedTemplateId = 78 };
var innerDerived = new Template("Pump.TempSensor.Probe1") { Id = 78, IsDerived = true, OwnerCompositionId = 51, ParentTemplateId = 11 };
var composition = new TemplateComposition("TempSensor") { Id = 50, TemplateId = 1, ComposedTemplateId = 77 };
var owner = new Template("Pump") { Id = 1 };
owner.Compositions.Add(composition);
var derived = new Template("Pump.TempSensor") { Id = 77, IsDerived = true, OwnerCompositionId = 50, ParentTemplateId = 2 };
derived.Compositions.Add(innerComp);
_repoMock.Setup(r => r.GetTemplateCompositionByIdAsync(50, It.IsAny<CancellationToken>())).ReturnsAsync(composition);
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(owner);
_repoMock.Setup(r => r.GetTemplateByIdAsync(77, It.IsAny<CancellationToken>())).ReturnsAsync(derived);
_repoMock.Setup(r => r.GetTemplateByIdAsync(78, It.IsAny<CancellationToken>())).ReturnsAsync(innerDerived);
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Template> { owner, derived, innerDerived });
var result = await _service.RenameCompositionAsync(50, "MainSensor", "admin");
Assert.True(result.IsSuccess);
Assert.Equal("MainSensor", result.Value.InstanceName);
Assert.Equal("Pump.MainSensor", derived.Name);
Assert.Equal("Pump.MainSensor.Probe1", innerDerived.Name);
_repoMock.Verify(r => r.UpdateTemplateAsync(
It.Is<Template>(t => t.Id == 78), It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task RenameComposition_NestedCascadeNameCollision_Fails()
{
// A pre-existing template occupies the name the nested cascade would
// produce (Pump.MainSensor.Probe1). The rename must abort before any
// row mutates, so the full cascade name set must be pre-checked.
var innerComp = new TemplateComposition("Probe1") { Id = 51, TemplateId = 77, ComposedTemplateId = 78 };
var innerDerived = new Template("Pump.TempSensor.Probe1") { Id = 78, IsDerived = true, OwnerCompositionId = 51, ParentTemplateId = 11 };
var composition = new TemplateComposition("TempSensor") { Id = 50, TemplateId = 1, ComposedTemplateId = 77 };
var owner = new Template("Pump") { Id = 1 };
owner.Compositions.Add(composition);
var derived = new Template("Pump.TempSensor") { Id = 77, IsDerived = true, OwnerCompositionId = 50, ParentTemplateId = 2 };
derived.Compositions.Add(innerComp);
var collider = new Template("Pump.MainSensor.Probe1") { Id = 99 };
_repoMock.Setup(r => r.GetTemplateCompositionByIdAsync(50, It.IsAny<CancellationToken>())).ReturnsAsync(composition);
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(owner);
_repoMock.Setup(r => r.GetTemplateByIdAsync(77, It.IsAny<CancellationToken>())).ReturnsAsync(derived);
_repoMock.Setup(r => r.GetTemplateByIdAsync(78, It.IsAny<CancellationToken>())).ReturnsAsync(innerDerived);
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Template> { owner, derived, innerDerived, collider });
var result = await _service.RenameCompositionAsync(50, "MainSensor", "admin");
Assert.True(result.IsFailure);
Assert.Contains("already exists", result.Error);
_repoMock.Verify(r => r.UpdateTemplateAsync(It.IsAny<Template>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task RenameComposition_DuplicateName_Fails()
{