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:
+45
@@ -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()
|
||||
{
|
||||
|
||||
@@ -150,6 +150,52 @@ public sealed class ArtifactDiffTests
|
||||
Assert.Contains(("add", "new2"), hunks);
|
||||
}
|
||||
|
||||
// ============ #05-T23: placeholder-identity resolution via name maps ============
|
||||
|
||||
[Fact]
|
||||
public void CompareTemplate_MatchingFolderBaseAndComposition_IsIdentical()
|
||||
{
|
||||
// A structured template (FolderId, ParentTemplateId, one composition) that
|
||||
// the incoming bundle matches by NAME must classify Identical. Before T23,
|
||||
// FolderNameOf/BaseTemplateNameOf/CompositionTargetNameOf returned "<id:N>"
|
||||
// placeholders that never equalled the incoming names, so every re-import
|
||||
// of an unchanged structured template spuriously read Modified.
|
||||
var existing = MakeTemplate("Pump");
|
||||
existing.FolderId = 5;
|
||||
existing.ParentTemplateId = 7;
|
||||
existing.Compositions.Add(new TemplateComposition("Mod1") { ComposedTemplateId = 9 });
|
||||
|
||||
var folderNameById = new Dictionary<int, string> { [5] = "Machines" };
|
||||
var templateNameById = new Dictionary<int, string> { [7] = "BasePump", [9] = "FeatureX" };
|
||||
|
||||
var incoming = MakeTemplateDto("Pump") with
|
||||
{
|
||||
FolderName = "Machines",
|
||||
BaseTemplateName = "BasePump",
|
||||
Compositions = new[] { new TemplateCompositionDto("Mod1", "FeatureX") },
|
||||
};
|
||||
|
||||
var item = _diff.CompareTemplate(incoming, existing, folderNameById, templateNameById);
|
||||
|
||||
Assert.Equal(ConflictKind.Identical, item.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompareTemplate_DivergentBaseTemplateName_IsModified()
|
||||
{
|
||||
// Sanity: the maps must not mask a REAL change. Existing base = "BasePump",
|
||||
// incoming names a different base → Modified.
|
||||
var existing = MakeTemplate("Pump");
|
||||
existing.ParentTemplateId = 7;
|
||||
var templateNameById = new Dictionary<int, string> { [7] = "BasePump" };
|
||||
|
||||
var incoming = MakeTemplateDto("Pump") with { BaseTemplateName = "DifferentBase" };
|
||||
|
||||
var item = _diff.CompareTemplate(incoming, existing, folderNameById: null, templateNameById: templateNameById);
|
||||
|
||||
Assert.Equal(ConflictKind.Modified, item.Kind);
|
||||
}
|
||||
|
||||
// ============ #05-T6: single-source child equality — diff sees the full field set ============
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user