fix(cli): dedupe import resolutions before ApplyAsync

PreviewAsync can emit multiple ImportPreviewItem rows for the same
(EntityType, Name) -- one per modified member of a template, for
example. ApplyAsync internally calls .ToDictionary() on the
resolutions list and throws ArgumentException on duplicate keys.

The Central UI's BuildDefaultResolutions already dedupes via a
dictionary assignment (last-write-wins). Mirror that in the CLI
handler so 'bundle import' tolerates the duplicate-rows shape the
preview returns.
This commit is contained in:
Joseph Doherty
2026-05-24 08:20:34 -04:00
parent 438f59e74e
commit 1361a39770

View File

@@ -1838,22 +1838,29 @@ public class ManagementActor : ReceiveActor
$"Bundle has {blockers.Count} blocker(s); import aborted. {details}"); $"Bundle has {blockers.Count} blocker(s); import aborted. {details}");
} }
// Dedupe by (EntityType, Name) -- the preview can emit multiple rows per
// entity (e.g. one row per modified member of a template), but ApplyAsync
// requires a unique resolution per key. Last-write-wins matches the
// Central UI's TransportImport.BuildDefaultResolutions behavior.
var renameStamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss"); var renameStamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss");
var resolutions = preview.Items.Select(item => new ImportResolution( var resolutionsMap = new Dictionary<(string, string), ImportResolution>();
item.EntityType, foreach (var item in preview.Items)
item.Name, {
item.Kind switch var action = item.Kind switch
{ {
ConflictKind.New => ResolutionAction.Add, ConflictKind.New => ResolutionAction.Add,
ConflictKind.Identical => ResolutionAction.Skip, ConflictKind.Identical => ResolutionAction.Skip,
ConflictKind.Modified => policy, ConflictKind.Modified => policy,
_ => ResolutionAction.Skip, _ => ResolutionAction.Skip,
}, };
(item.Kind == ConflictKind.Modified && policy == ResolutionAction.Rename) var renameTo = (item.Kind == ConflictKind.Modified && policy == ResolutionAction.Rename)
? $"{item.Name}-imported-{renameStamp}" ? $"{item.Name}-imported-{renameStamp}"
: null)).ToList(); : null;
resolutionsMap[(item.EntityType, item.Name)] = new ImportResolution(
item.EntityType, item.Name, action, renameTo);
}
return await importer.ApplyAsync(session.SessionId, resolutions, username); return await importer.ApplyAsync(session.SessionId, resolutionsMap.Values.ToList(), username);
} }
private static byte[] DecodeBundle(string base64) private static byte[] DecodeBundle(string base64)