From 9f79c2e572dcdeb15dad42b9ec96f1109b5ff41d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 02:29:22 +0000 Subject: [PATCH] fix: keep command metadata when loading saved state [patch] PlaceholderCommand passed only the change type and affected items to BaseCommand, which built fresh metadata, so a reload reset every command's timestamp to load time, its size to 1, and dropped its CustomData. Use the deserialized ChangeMetadata instead. Fixes ktsu-dev/UndoRedo#77 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01L2BLMsT5ih3DnMNxTyGUHh --- UndoRedo.Test/SerializationTests.cs | 30 +++++++++++++++++++++ UndoRedo/Services/JsonUndoRedoSerializer.cs | 3 +++ 2 files changed, 33 insertions(+) diff --git a/UndoRedo.Test/SerializationTests.cs b/UndoRedo.Test/SerializationTests.cs index 7c3237d..13c2ff3 100644 --- a/UndoRedo.Test/SerializationTests.cs +++ b/UndoRedo.Test/SerializationTests.cs @@ -141,6 +141,36 @@ public async Task UndoRedoService_SaveLoadState_PreservesStackState() Assert.AreEqual(stack.HasUnsavedChanges, newStack.HasUnsavedChanges); } + [TestMethod] + public async Task UndoRedoService_SaveLoadState_PreservesCommandMetadata() + { + // Arrange + UndoRedoService stack = CreateService(); + stack.SetSerializer(new JsonUndoRedoSerializer()); + + Dictionary customData = new() { ["author"] = "alice" }; + stack.Execute(new DelegateCommand("Big edit", () => { }, () => { }, ChangeType.Insert, ["doc"], size: 42, customData: customData)); + ChangeMetadata original = stack.Commands[0].Metadata; + + // Act + byte[] data = await stack.SaveStateAsync().ConfigureAwait(false); + + UndoRedoService newStack = CreateService(); + newStack.SetSerializer(new JsonUndoRedoSerializer()); + bool success = await newStack.LoadStateAsync(data).ConfigureAwait(false); + + // Assert + Assert.IsTrue(success); + ChangeMetadata loaded = newStack.Commands[0].Metadata; + Assert.AreEqual(original.Timestamp, loaded.Timestamp, "The timestamp must be when the change was made, not when it was loaded"); + Assert.AreEqual(42, loaded.Size); + Assert.AreEqual(ChangeType.Insert, loaded.ChangeType); + Assert.HasCount(1, loaded.AffectedItems); + Assert.AreEqual("doc", loaded.AffectedItems[0]); + Assert.IsNotNull(loaded.CustomData, "CustomData must survive a save and load"); + Assert.AreEqual("alice", loaded.CustomData["author"].ToString()); + } + [TestMethod] public async Task UndoRedoService_NoSerializer_ThrowsInvalidOperationException() { diff --git a/UndoRedo/Services/JsonUndoRedoSerializer.cs b/UndoRedo/Services/JsonUndoRedoSerializer.cs index cb327b2..7e81c44 100644 --- a/UndoRedo/Services/JsonUndoRedoSerializer.cs +++ b/UndoRedo/Services/JsonUndoRedoSerializer.cs @@ -187,6 +187,9 @@ internal sealed class PlaceholderCommand(string description, string? navigationC { public override string Description { get; } = $"[Placeholder] {description}"; + // Keep the deserialized metadata, rather than the fresh timestamp, size and custom data BaseCommand builds + public override ChangeMetadata Metadata { get; protected set; } = Ensure.NotNull(metadata); + public override void Execute() => throw new NotSupportedException("Placeholder commands cannot be executed"); public override void Undo() => throw new NotSupportedException("Placeholder commands cannot be undone");