From 5f1f3a9e1b4ab9b9a5f666ace7e3acfb9684109b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 03:23:55 +0000 Subject: [PATCH] Return a snapshot from GetCommandsToUndo instead of a live query [patch] GetCommandsToUndo returned a deferred Skip/Take over the live command list, so a result enumerated after the stack branched listed the new command in place of the one that was due to be undone. Materialize it, as GetChangeVisualizations already does, and cover both with a test that changes the stack between the call and the enumeration. Fixes #95 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TXZMdofSRgS8oXS9RgTRhM --- UndoRedo.Test/UndoRedoStackTests.cs | 40 ++++++++++++++++++++++++ UndoRedo/Services/SaveBoundaryManager.cs | 3 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/UndoRedo.Test/UndoRedoStackTests.cs b/UndoRedo.Test/UndoRedoStackTests.cs index fd42f6e..2fecb17 100644 --- a/UndoRedo.Test/UndoRedoStackTests.cs +++ b/UndoRedo.Test/UndoRedoStackTests.cs @@ -882,6 +882,46 @@ public async Task UndoToSaveBoundary_BoundaryRemovedByBranching_ReturnsFalse() Assert.IsEmpty(stack.GetCommandsToUndo(removedBoundary)); } + [TestMethod] + public void GetCommandsToUndo_EnumeratedAfterBranching_ReflectsStateAtCall() + { + // Arrange + UndoRedoService stack = CreateService(); + stack.MarkAsSaved(); + stack.Execute(new DelegateCommand("A", () => { }, () => { })); + stack.Execute(new DelegateCommand("B", () => { }, () => { })); + SaveBoundary boundary = stack.SaveBoundaries[0]; + + // Act + IEnumerable commandsToUndo = stack.GetCommandsToUndo(boundary); + stack.Undo(); + stack.Execute(new DelegateCommand("C", () => { }, () => { })); // Replaces B in the live stack + + // Assert + string descriptions = string.Join(",", commandsToUndo.Select(c => c.Description)); + Assert.AreEqual("A,B", descriptions, "The result should be a snapshot of the stack when it was requested"); + } + + [TestMethod] + public void GetChangeVisualizations_EnumeratedAfterExecute_ReflectsStateAtCall() + { + // Arrange + UndoRedoService stack = CreateService(); + stack.Execute(new DelegateCommand("A", () => { }, () => { })); + + // Act + IEnumerable visualizations = stack.GetChangeVisualizations(); + stack.MarkAsSaved(); + stack.Execute(new DelegateCommand("C", () => { }, () => { })); + + // Assert + List snapshot = [.. visualizations]; + Assert.HasCount(1, snapshot, "Commands executed after the call should not appear"); + Assert.AreEqual("A", snapshot[0].Command.Description); + Assert.IsTrue(snapshot[0].IsExecuted); + Assert.IsFalse(snapshot[0].HasSaveBoundary, "A save boundary created after the call should not appear"); + } + [TestMethod] public async Task UndoToSaveBoundary_WhenAlreadyAtPosition_ReturnsFalse() { diff --git a/UndoRedo/Services/SaveBoundaryManager.cs b/UndoRedo/Services/SaveBoundaryManager.cs index 3d92029..d9d4ba0 100644 --- a/UndoRedo/Services/SaveBoundaryManager.cs +++ b/UndoRedo/Services/SaveBoundaryManager.cs @@ -97,9 +97,10 @@ public IEnumerable GetCommandsToUndo(SaveBoundary saveBoundary, int cu Ensure.NotNull(saveBoundary); Ensure.NotNull(commands); + // Materialize so the result is a snapshot, not a view that changes with the live stack return currentPosition <= saveBoundary.Position ? [] - : commands.Skip(saveBoundary.Position + 1).Take(currentPosition - saveBoundary.Position); + : [.. commands.Skip(saveBoundary.Position + 1).Take(currentPosition - saveBoundary.Position)]; } ///