Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Transport.Tests/Import/DerivedTemplatePlanTests.cs
T
Joseph Doherty 84a050420f fix(transport): carry derived composition-slot templates and rebuild them through the compose flow (ScadaBridge#118)
Bundles did not record that a template was a derived slot template, so the
importer re-created every slot as a BASE template named after the slot.

- Export: TemplateDto gains an additive, null-omitted `Derivation`
  (owner path + base path by stable names); a derived template carries only
  its own rows (overrides/additions, not inherited placeholders). The closure
  pulls in each derived template's base/owner; base orders first.
  schemaVersion 1.1 -> 1.2, bundleFormatVersion stays 1.
- Import: base templates go through the ordinary passes; derived templates are
  ordered topologically (DerivedTemplatePlan; cycle / duplicate / malformed =
  preview Blocker + SemanticValidationException before anything is staged),
  their owner and base resolved by path (#100 root rule), and the slot is
  created with TemplateService.AddCompositionAsync, synced in place when it
  already exists with the same base, or deleted+recomposed when the base
  differs; own rows applied as overrides, then ResyncInheritedMembersAsync.
  Missing owner/base fails that record (BundleImportDerivedTemplateUnresolved
  audit + ImportResult warning), never creating a base in its place.
- Preview/diff: derived rows are keyed by qualified name and compared against
  the target slot at the same owner path (own rows + base), so an unchanged
  slot is Identical and a same-named base template is never matched.
- Pre-#118 bundles import exactly as before (new fixture
  pre-118-derived-plaintext.scadabundle, written by a13aabc7). The field is in
  content, pinned by the signed manifest's contentHash.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 05:02:06 -05:00

75 lines
3.3 KiB
C#

using ZB.MOM.WW.ScadaBridge.Transport.Import;
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;
namespace ZB.MOM.WW.ScadaBridge.Transport.Tests.Import;
/// <summary>
/// ScadaBridge#118 — <see cref="DerivedTemplatePlan"/> orders a bundle's derived templates
/// after their owner and base whatever the bundle order, and reports cycles and malformed
/// derivations as errors.
/// </summary>
public sealed class DerivedTemplatePlanTests
{
private static TemplateDto Base(string name) =>
new(name, null, null, null, Array.Empty<TemplateAttributeDto>(), Array.Empty<TemplateAlarmDto>(),
Array.Empty<TemplateScriptDto>(), Array.Empty<TemplateCompositionDto>());
private static TemplateDto Derived(string owner, string[] slots, string baseName, params string[] baseSlots) =>
Base(slots[^1]) with { Derivation = new TemplateDerivationDto(owner, slots, baseName, baseSlots) };
/// <summary>Nested slots and cascade slots come after their owner and their (derived) base.</summary>
[Fact]
public void Orders_owner_and_base_before_the_derived_template()
{
var plan = DerivedTemplatePlan.Build(new[]
{
Derived("Station", new[] { "P", "M1" }, "Pump", "M1"), // owner Station.P, base Pump.M1
Base("Station"),
Derived("Station", new[] { "P" }, "Pump"),
Derived("Pump", new[] { "M1" }, "Motor"),
});
Assert.Empty(plan.Errors);
var order = plan.Ordered.Select(DerivedTemplatePlan.Key).ToList();
Assert.Equal(3, order.Count);
Assert.True(order.IndexOf("Station.P") < order.IndexOf("Station.P.M1"));
Assert.True(order.IndexOf("Pump.M1") < order.IndexOf("Station.P.M1"));
}
/// <summary>A two-node and a one-node cycle are both reported, naming the templates.</summary>
[Fact]
public void Cycles_are_errors()
{
var two = DerivedTemplatePlan.Build(new[]
{
Derived("Pump", new[] { "X" }, "Pump", "Y"),
Derived("Pump", new[] { "Y" }, "Pump", "X"),
});
var error = Assert.Single(two.Errors);
Assert.Contains("cycle", error, StringComparison.Ordinal);
Assert.Contains("Pump.X", error, StringComparison.Ordinal);
Assert.Contains("Pump.Y", error, StringComparison.Ordinal);
var self = DerivedTemplatePlan.Build(new[] { Derived("Pump", new[] { "X" }, "Pump", "X") });
Assert.Contains("cycle", Assert.Single(self.Errors), StringComparison.Ordinal);
}
/// <summary>Malformed derivations and duplicate identities are errors; base templates are ignored.</summary>
[Fact]
public void Malformed_and_duplicate_derivations_are_errors()
{
var plan = DerivedTemplatePlan.Build(new[]
{
Base("Pump"),
Base("M1") with { Derivation = new TemplateDerivationDto("Pump", Array.Empty<string>(), "Motor", Array.Empty<string>()) },
Derived("Pump", new[] { "M2" }, "Motor"),
Derived("Pump", new[] { "M2" }, "Motor"),
});
Assert.Equal(2, plan.Errors.Count);
Assert.Contains(plan.Errors, e => e.Contains("malformed", StringComparison.Ordinal));
Assert.Contains(plan.Errors, e => e.Contains("more than once", StringComparison.Ordinal));
Assert.Equal("Pump", DerivedTemplatePlan.Key(Base("Pump")));
}
}