fix(transport): template diffs resolve folder/base/composition names — unchanged structured templates classify Identical again

#05-T23. ArtifactDiff.CompareTemplate gains optional folderNameById /
templateNameById maps; FolderNameOf / BaseTemplateNameOf / CompositionTargetNameOf
resolve the existing template's FolderId / ParentTemplateId / composition
ComposedTemplateId to real names (falling back to the <id:N> placeholder only
when a map is absent or misses, keeping existing unit-test callers valid).
PreviewAsync builds templateNameById from GetAllTemplatesAsync (parent/composition
targets can be any template, not just bundle-named ones) and threads both maps
through. Fixes spurious Modified on every re-import of a foldered/derived/composed
template. End-to-end preview test + two ArtifactDiff unit tests.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 02:22:44 -04:00
parent 523e670579
commit 92e713f1a1
4 changed files with 128 additions and 15 deletions
@@ -330,6 +330,51 @@ public sealed class BundleImporterPreviewTests : IDisposable
Assert.Null(pumpItem.FieldDiffJson);
}
[Fact]
public async Task PreviewAsync_unchanged_structured_template_classifies_Identical()
{
// #05-T23 end-to-end: a template with a base (ParentTemplateId) and a
// composition, re-imported against an UNCHANGED target, must classify
// Identical. Before T23, PreviewAsync compared "<id:N>" placeholders for
// BaseTemplateName / composition target against the bundle's real names,
// so it always read Modified — defeating the wizard's auto-skip contract.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
ctx.Templates.Add(new Template("BasePump") { Description = "base" });
ctx.Templates.Add(new Template("FeatureX") { Description = "feature" });
await ctx.SaveChangesAsync();
var basePump = await ctx.Templates.SingleAsync(t => t.Name == "BasePump");
var feature = await ctx.Templates.SingleAsync(t => t.Name == "FeatureX");
var pump = new Template("Pump") { Description = "derived", ParentTemplateId = basePump.Id };
ctx.Templates.Add(pump);
await ctx.SaveChangesAsync();
pump.Compositions.Add(new TemplateComposition("mod1")
{
TemplateId = pump.Id,
ComposedTemplateId = feature.Id,
});
await ctx.SaveChangesAsync();
}
// Export everything; do NOT wipe — the target is unchanged.
var bundleStream = await ExportTemplatesAsync();
var bytes = await StreamToBytes(bundleStream);
ImportPreview preview;
await using (var scope = _provider.CreateAsyncScope())
{
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
var session = await importer.LoadAsync(new MemoryStream(bytes), passphrase: null);
preview = await importer.PreviewAsync(session.SessionId);
}
var pumpItem = Assert.Single(preview.Items,
i => i.EntityType == "Template" && i.Name == "Pump");
Assert.Equal(ConflictKind.Identical, pumpItem.Kind);
}
[Fact]
public async Task PreviewAsync_emits_Warning_when_template_script_dependency_missing()
{