fix(template-engine): repeated composition no longer drops nested members — cycle guard keyed on recursion path, not global template id
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -851,4 +851,103 @@ public class FlatteningServiceTests
|
||||
Assert.Equal("List", attr.DataType); // type unchanged
|
||||
Assert.Equal("String", attr.ElementDataType); // element type preserved
|
||||
}
|
||||
|
||||
// ── Repeated composition: same template under multiple slots ──────────
|
||||
|
||||
[Fact]
|
||||
public void Flatten_SameTemplateComposedTwice_ResolvesNestedMembersUnderBothSlots()
|
||||
{
|
||||
// Root(1) composes X(2) as "x"; X composes Y(3) as "y1" AND "y2";
|
||||
// Y composes Z(4) as "z"; Z carries one member of each kind.
|
||||
// Because y1 and y2 share one recursion subtree, a globally-keyed cycle
|
||||
// guard would descend into Z under y1 but skip it under y2, dropping the
|
||||
// whole x.y2.z.* subtree. The path-keyed guard resolves both.
|
||||
var z = CreateTemplate(4, "Z");
|
||||
z.Attributes.Add(new TemplateAttribute("Val") { DataType = DataType.Double, Value = "1" });
|
||||
z.Alarms.Add(new TemplateAlarm("Alm")
|
||||
{
|
||||
TriggerType = AlarmTriggerType.RangeViolation,
|
||||
TriggerConfiguration = "{\"attributeName\":\"Val\",\"high\":100}",
|
||||
PriorityLevel = 1
|
||||
});
|
||||
z.Scripts.Add(new TemplateScript("Run", "// run") { Id = 40 });
|
||||
z.NativeAlarmSources.Add(new TemplateNativeAlarmSource("Src")
|
||||
{ ConnectionName = "Opc", SourceReference = "ns=2;s=Z" });
|
||||
|
||||
var y = CreateTemplate(3, "Y");
|
||||
var x = CreateTemplate(2, "X");
|
||||
var root = CreateTemplate(1, "Root");
|
||||
|
||||
var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
|
||||
{
|
||||
[1] = new List<TemplateComposition> { new("x") { ComposedTemplateId = 2 } },
|
||||
[2] = new List<TemplateComposition>
|
||||
{
|
||||
new("y1") { ComposedTemplateId = 3 },
|
||||
new("y2") { ComposedTemplateId = 3 }
|
||||
},
|
||||
[3] = new List<TemplateComposition> { new("z") { ComposedTemplateId = 4 } }
|
||||
};
|
||||
|
||||
var composedChains = new Dictionary<int, IReadOnlyList<Template>>
|
||||
{
|
||||
[2] = [x],
|
||||
[3] = [y],
|
||||
[4] = [z]
|
||||
};
|
||||
|
||||
var result = _sut.Flatten(
|
||||
CreateInstance(),
|
||||
[root],
|
||||
compositions,
|
||||
composedChains,
|
||||
new Dictionary<int, DataConnection>());
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "x.y1.z.Val");
|
||||
Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "x.y2.z.Val");
|
||||
|
||||
Assert.Contains(result.Value.Alarms, a => a.CanonicalName == "x.y1.z.Alm");
|
||||
Assert.Contains(result.Value.Alarms, a => a.CanonicalName == "x.y2.z.Alm");
|
||||
|
||||
Assert.Contains(result.Value.Scripts, s => s.CanonicalName == "x.y1.z.Run");
|
||||
Assert.Contains(result.Value.Scripts, s => s.CanonicalName == "x.y2.z.Run");
|
||||
|
||||
Assert.Contains(result.Value.NativeAlarmSources, s => s.CanonicalName == "x.y1.z.Src");
|
||||
Assert.Contains(result.Value.NativeAlarmSources, s => s.CanonicalName == "x.y2.z.Src");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Flatten_CompositionCycle_StillTerminates()
|
||||
{
|
||||
// X(1) composes Y(2) as "y"; Y composes X(1) as "x" — a composition
|
||||
// cycle. The path-keyed guard must still break the cycle (a template
|
||||
// already on the CURRENT path) so flattening terminates.
|
||||
var x = CreateTemplate(1, "X");
|
||||
x.Attributes.Add(new TemplateAttribute("Root") { DataType = DataType.String, Value = "r" });
|
||||
var y = CreateTemplate(2, "Y");
|
||||
|
||||
var compositions = new Dictionary<int, IReadOnlyList<TemplateComposition>>
|
||||
{
|
||||
[1] = new List<TemplateComposition> { new("y") { ComposedTemplateId = 2 } },
|
||||
[2] = new List<TemplateComposition> { new("x") { ComposedTemplateId = 1 } }
|
||||
};
|
||||
|
||||
var composedChains = new Dictionary<int, IReadOnlyList<Template>>
|
||||
{
|
||||
[1] = [x],
|
||||
[2] = [y]
|
||||
};
|
||||
|
||||
var result = _sut.Flatten(
|
||||
CreateInstance(),
|
||||
[x],
|
||||
compositions,
|
||||
composedChains,
|
||||
new Dictionary<int, DataConnection>());
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Contains(result.Value.Attributes, a => a.CanonicalName == "Root");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user