fix(template-engine): resolve TemplateEngine-015,016 — cascade-rename nested derived templates, correct composed-script ParentPath
This commit is contained in:
@@ -624,4 +624,46 @@ public class FlatteningServiceTests
|
||||
var alarm = result.Value.Alarms.First(a => a.CanonicalName == "MainPump.PumpFault");
|
||||
Assert.Equal("MainPump.PumpAlarmHandler", alarm.OnTriggerScriptCanonicalName);
|
||||
}
|
||||
|
||||
// ── TemplateEngine-016: composed-script ScriptScope.ParentPath ─────────
|
||||
|
||||
[Fact]
|
||||
public void Flatten_NestedComposedScript_ScopeCarriesCorrectParentPath()
|
||||
{
|
||||
// Station composes Pump (level 1); Pump composes Motor (level 2).
|
||||
// The depth-1 script's parent is the root template (ParentPath "");
|
||||
// the depth-2 script's parent is the Pump module (ParentPath "MainPump").
|
||||
var motor = CreateTemplate(3, "Motor");
|
||||
motor.Scripts.Add(new TemplateScript("MonitorMotor", "// m") { Id = 70 });
|
||||
|
||||
var pump = CreateTemplate(2, "Pump");
|
||||
pump.Scripts.Add(new TemplateScript("MonitorPump", "// p") { Id = 71 });
|
||||
|
||||
var station = CreateTemplate(1, "Station");
|
||||
|
||||
var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
|
||||
{
|
||||
[1] = new List<TemplateComposition> { new("MainPump") { ComposedTemplateId = 2 } },
|
||||
[2] = new List<TemplateComposition> { new("DriveMotor") { ComposedTemplateId = 3 } },
|
||||
};
|
||||
var composedChains = new Dictionary<int, IReadOnlyList<Template>>
|
||||
{
|
||||
[2] = [pump], [3] = [motor],
|
||||
};
|
||||
|
||||
var instance = CreateInstance();
|
||||
var result = _sut.Flatten(instance, [station], compositions, composedChains,
|
||||
new Dictionary<int, DataConnection>());
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
var depth1 = result.Value.Scripts.First(s => s.CanonicalName == "MainPump.MonitorPump");
|
||||
Assert.Equal("MainPump", depth1.Scope.SelfPath);
|
||||
Assert.Equal("", depth1.Scope.ParentPath);
|
||||
|
||||
var depth2 = result.Value.Scripts.First(s => s.CanonicalName == "MainPump.DriveMotor.MonitorMotor");
|
||||
Assert.Equal("MainPump.DriveMotor", depth2.Scope.SelfPath);
|
||||
// Parent module of a depth-2 script is the enclosing Pump module.
|
||||
Assert.Equal("MainPump", depth2.Scope.ParentPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user