From 1d13277d79748bab1142772287729b272349d86b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 23:29:41 +0000 Subject: [PATCH] Track only this manager's commands as persisted after a save [patch] SaveAsync recorded every command it wrote as persisted by this manager, including stored commands it only carried through for another manager. The next save then read those as unregistered here and dropped them from commands.json, so a second save deleted the other manager's commands and left their bindings dead. Record only the commands registered here, as the profile path already does. Fixes ktsu-dev/Keybinding#214 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5MyZsxEdt96yVWhKn --- Keybinding.Test/SaveWithoutInitializeTests.cs | 69 +++++++++++++++++++ Keybinding/KeybindingManager.cs | 5 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/Keybinding.Test/SaveWithoutInitializeTests.cs b/Keybinding.Test/SaveWithoutInitializeTests.cs index ff09e5d..1359249 100644 --- a/Keybinding.Test/SaveWithoutInitializeTests.cs +++ b/Keybinding.Test/SaveWithoutInitializeTests.cs @@ -113,6 +113,75 @@ public async Task SaveAsync_WithoutInitialize_KeepsStoredCommandsAndActiveProfil Assert.AreEqual("file.save", reloaded.Keybindings.ExecuteChord("default", Chord.Parse("Ctrl+S"))); } + [TestMethod] + public async Task SaveAsync_TwiceWithoutInitialize_KeepsStoredCommands() + { + { + using KeybindingManager manager = new(_testDataDirectory); + await manager.InitializeAsync().ConfigureAwait(false); + manager.Commands.RegisterCommand(new Command("file.save", "Save")); + manager.CreateDefaultProfile(); + manager.Keybindings.BindChord("file.save", Chord.Parse("Ctrl+S")); + await manager.SaveAsync().ConfigureAwait(false); + } + + { + using KeybindingManager manager = new(_testDataDirectory); + manager.Profiles.CreateProfile("other", "Other"); + await manager.SaveAsync().ConfigureAwait(false); + + // The first save carried file.save through; the second must not read it as unregistered here + await manager.SaveAsync().ConfigureAwait(false); + } + + using KeybindingManager reloaded = new(_testDataDirectory); + await reloaded.InitializeAsync().ConfigureAwait(false); + Assert.IsTrue(reloaded.Commands.IsCommandRegistered("file.save")); + Assert.AreEqual("file.save", reloaded.Keybindings.ExecuteChord("default", Chord.Parse("Ctrl+S"))); + } + + [TestMethod] + public async Task SaveAsync_TwiceAfterAnotherManagerSaved_KeepsItsCommand() + { + using KeybindingManager first = new(_testDataDirectory); + using KeybindingManager second = new(_testDataDirectory); + await first.InitializeAsync().ConfigureAwait(false); + await second.InitializeAsync().ConfigureAwait(false); + + first.Commands.RegisterCommand(new Command("a.cmd", "A")); + second.Commands.RegisterCommand(new Command("b.cmd", "B")); + second.Profiles.CreateProfile("bprof", "B profile"); + await second.SaveAsync().ConfigureAwait(false); + + await first.SaveAsync().ConfigureAwait(false); + await first.SaveAsync().ConfigureAwait(false); + + using KeybindingManager reloaded = new(_testDataDirectory); + await reloaded.InitializeAsync().ConfigureAwait(false); + Assert.IsTrue(reloaded.Commands.IsCommandRegistered("a.cmd")); + Assert.IsTrue(reloaded.Commands.IsCommandRegistered("b.cmd")); + Assert.IsTrue(reloaded.Profiles.ProfileExists("bprof")); + } + + [TestMethod] + public async Task SaveAsync_CommandUnregisteredAfterItsFirstSave_IsRemovedOnTheNext() + { + { + using KeybindingManager manager = new(_testDataDirectory); + manager.Commands.RegisterCommand(new Command("file.save", "Save")); + manager.Commands.RegisterCommand(new Command("tools.legacy", "Legacy")); + await manager.SaveAsync().ConfigureAwait(false); + + Assert.IsTrue(manager.Commands.UnregisterCommand("tools.legacy")); + await manager.SaveAsync().ConfigureAwait(false); + } + + using KeybindingManager reloaded = new(_testDataDirectory); + await reloaded.InitializeAsync().ConfigureAwait(false); + Assert.IsTrue(reloaded.Commands.IsCommandRegistered("file.save")); + Assert.IsFalse(reloaded.Commands.IsCommandRegistered("tools.legacy")); + } + [TestMethod] public async Task SaveAsync_CommandUnregisteredByManagerThatLoadedIt_IsRemoved() { diff --git a/Keybinding/KeybindingManager.cs b/Keybinding/KeybindingManager.cs index 268a3bf..ff77d97 100644 --- a/Keybinding/KeybindingManager.cs +++ b/Keybinding/KeybindingManager.cs @@ -152,7 +152,10 @@ .. storedCommands.Where(c => !commandIds.Contains(c.Id.ToString()) && !_persiste lock (_persistedStateLock) { _persistedCommandIds.Clear(); - _persistedCommandIds.UnionWith(commandsToSave.Select(c => c.Id.ToString())); + // Only the commands registered here count as persisted by this manager. The stored ones it carried through + // belong to someone else; marking them too would make the next save read them as unregistered here and + // drop them from the file. + _persistedCommandIds.UnionWith(commandIds); } // Remove stored profiles that were deleted in memory, so they do not come back on the next load. Only