fix(template-engine): resolve TemplateEngine-011,013,014 — remove dead converter, duplicate-id-safe cycle detection, unified deletion logic; TemplateEngine-012 deferred
This commit is contained in:
@@ -153,4 +153,77 @@ public class CycleDetectorTests
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TemplateEngine-013: robustness against duplicate Ids and Id 0
|
||||
// ========================================================================
|
||||
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_DuplicateIdsInList_DoesNotThrow()
|
||||
{
|
||||
// Two not-yet-saved templates both carry Id == 0. ToDictionary(t => t.Id)
|
||||
// would throw ArgumentException; the detector must tolerate it.
|
||||
var templateA = new Template("A") { Id = 0 };
|
||||
var templateB = new Template("B") { Id = 0 };
|
||||
var saved = new Template("Saved") { Id = 1 };
|
||||
var all = new List<Template> { templateA, templateB, saved };
|
||||
|
||||
var ex = Record.Exception(() => CycleDetector.DetectInheritanceCycle(1, 0, all));
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectCompositionCycle_DuplicateIdsInList_DoesNotThrow()
|
||||
{
|
||||
var templateA = new Template("A") { Id = 0 };
|
||||
var templateB = new Template("B") { Id = 0 };
|
||||
var all = new List<Template> { templateA, templateB };
|
||||
|
||||
var ex = Record.Exception(() => CycleDetector.DetectCompositionCycle(1, 2, all));
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectCrossGraphCycle_DuplicateIdsInList_DoesNotThrow()
|
||||
{
|
||||
var templateA = new Template("A") { Id = 0 };
|
||||
var templateB = new Template("B") { Id = 0 };
|
||||
var all = new List<Template> { templateA, templateB };
|
||||
|
||||
var ex = Record.Exception(() => CycleDetector.DetectCrossGraphCycle(5, 1, 2, all));
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_RealIdZero_StillDetectsCycle()
|
||||
{
|
||||
// A template legitimately stored with Id 0 (in-memory / test scenario):
|
||||
// a self-inheritance attempt must still be detected, not skipped as
|
||||
// "no parent" by a 0-as-sentinel overload.
|
||||
var template = new Template("Zero") { Id = 0 };
|
||||
var all = new List<Template> { template };
|
||||
|
||||
var result = CycleDetector.DetectInheritanceCycle(0, 0, all);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("itself", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectInheritanceCycle_ParentChainThroughIdZero_DetectsCycle()
|
||||
{
|
||||
// Child(1) -> parent Zero(0) -> parent Child(1): a cycle running through
|
||||
// a template whose real Id is 0 must be detected, not silently skipped.
|
||||
var zero = new Template("Zero") { Id = 0, ParentTemplateId = 1 };
|
||||
var child = new Template("Child") { Id = 1, ParentTemplateId = null };
|
||||
var all = new List<Template> { zero, child };
|
||||
|
||||
var result = CycleDetector.DetectInheritanceCycle(1, 0, all);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("cycle", result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,38 @@ public class RevisionHashServiceTests
|
||||
Assert.Equal(_sut.ComputeHash(config1), _sut.ComputeHash(config2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HashableRecords_PropertiesDeclaredAlphabetically()
|
||||
{
|
||||
// TemplateEngine-011: revision-hash determinism depends entirely on the
|
||||
// private Hashable* records declaring their properties in alphabetical
|
||||
// order (System.Text.Json emits properties in CLR declaration order and
|
||||
// does not sort). This guards against a contributor silently changing
|
||||
// every revision hash by adding a property out of order.
|
||||
var nested = typeof(RevisionHashService)
|
||||
.GetNestedTypes(System.Reflection.BindingFlags.NonPublic)
|
||||
.Where(t => t.Name.StartsWith("Hashable"))
|
||||
.ToList();
|
||||
|
||||
Assert.NotEmpty(nested);
|
||||
|
||||
foreach (var type in nested)
|
||||
{
|
||||
var propNames = type
|
||||
.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
|
||||
.Where(p => p.Name != "EqualityContract")
|
||||
.Select(p => p.Name)
|
||||
.ToList();
|
||||
|
||||
var sorted = propNames.OrderBy(n => n, StringComparer.Ordinal).ToList();
|
||||
|
||||
Assert.True(
|
||||
propNames.SequenceEqual(sorted),
|
||||
$"{type.Name} properties must be declared alphabetically. " +
|
||||
$"Declared: [{string.Join(", ", propNames)}] Expected: [{string.Join(", ", sorted)}]");
|
||||
}
|
||||
}
|
||||
|
||||
private static FlattenedConfiguration CreateConfig(string instanceName, string tempValue)
|
||||
{
|
||||
return new FlattenedConfiguration
|
||||
|
||||
@@ -122,11 +122,13 @@ public class TemplateServiceTests
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Instance> { new Instance("Pump1") { Id = 1, TemplateId = 1, SiteId = 1 } });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template> { template });
|
||||
|
||||
var result = await _service.DeleteTemplateAsync(1, "admin");
|
||||
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Contains("referenced by", result.Error);
|
||||
Assert.Contains("instance(s) reference it", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -143,7 +145,7 @@ public class TemplateServiceTests
|
||||
var result = await _service.DeleteTemplateAsync(1, "admin");
|
||||
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Contains("inherited by", result.Error);
|
||||
Assert.Contains("child template(s) inherit from it", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -162,7 +164,36 @@ public class TemplateServiceTests
|
||||
var result = await _service.DeleteTemplateAsync(1, "admin");
|
||||
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Contains("composed by", result.Error);
|
||||
Assert.Contains("template(s) compose it", result.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTemplate_MultipleConstraints_ReportsAllNotJustFirst()
|
||||
{
|
||||
// TemplateEngine-014: DeleteTemplateAsync delegates its constraint check
|
||||
// to the single TemplateDeletionService implementation, which accumulates
|
||||
// every blocking reason instead of returning on the first failing category.
|
||||
var template = new Template("Busy") { Id = 1 };
|
||||
var composer = new Template("Composer") { Id = 3 };
|
||||
composer.Compositions.Add(new TemplateComposition("Module") { Id = 1, TemplateId = 3, ComposedTemplateId = 1 });
|
||||
|
||||
_repoMock.Setup(r => r.GetTemplateByIdAsync(1, It.IsAny<CancellationToken>())).ReturnsAsync(template);
|
||||
_repoMock.Setup(r => r.GetInstancesByTemplateIdAsync(1, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Instance> { new Instance("Inst1") { Id = 1, TemplateId = 1, SiteId = 1 } });
|
||||
_repoMock.Setup(r => r.GetAllTemplatesAsync(It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<Template>
|
||||
{
|
||||
template,
|
||||
new Template("Child") { Id = 2, ParentTemplateId = 1 },
|
||||
composer
|
||||
});
|
||||
|
||||
var result = await _service.DeleteTemplateAsync(1, "admin");
|
||||
|
||||
Assert.True(result.IsFailure);
|
||||
Assert.Contains("instance(s) reference it", result.Error);
|
||||
Assert.Contains("child template(s) inherit from it", result.Error);
|
||||
Assert.Contains("template(s) compose it", result.Error);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
|
||||
Reference in New Issue
Block a user