From fa01183f35af369f0b70cdf6f8e41f48365d5005 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 01:28:53 +0000 Subject: [PATCH] Refuse a persistAs key another toggle already remembers [patch] Two toggles given the same persistAs key shared one remembered value: each click overwrote it, and the next launch restored both toggles to it, running both real setters with the wrong state. Toggle now throws ArgumentException (param persistAs) for a key another toggle already uses, compared ordinally to match the store, and for an empty or white-space key. This is the preference-key counterpart of the alias-clash checks from #18. Fixes #37 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW3e2AZDcKPKkvjAga2 --- README.md | 4 ++- TrayApp.Test/TrayAppBuilderTests.cs | 40 +++++++++++++++++++++++++++++ TrayApp/TrayAppBuilder.cs | 18 +++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a5753b..192c881 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,9 @@ with the matching package references in the tool, not in the library: ``` -Only toggles given a `persistAs` key are remembered, and only the tray reads them: a command line run +Only toggles given a `persistAs` key are remembered, and each needs a key of its own: `Toggle` throws +`ArgumentException` for a blank key or one another toggle already uses, since two toggles sharing a key +would both be restored to whichever was clicked last. Only the tray reads them: a command line run says what it wants, so letting a remembered setting override an argument would be surprising. Within the tray, what was typed this time beats what the tray was left set to last time. diff --git a/TrayApp.Test/TrayAppBuilderTests.cs b/TrayApp.Test/TrayAppBuilderTests.cs index 44607aa..e020eda 100644 --- a/TrayApp.Test/TrayAppBuilderTests.cs +++ b/TrayApp.Test/TrayAppBuilderTests.cs @@ -72,6 +72,46 @@ public void Flag_WithAnAliasAnotherOptionTook_Throws() Assert.AreEqual(1, builder.Build().Options.Count); } + [TestMethod] + public void Toggle_WithAPersistAsKeyAnotherToggleTook_Throws() + { + // Both toggles would read and write one remembered value, so the next launch restores both to it. + TrayAppBuilder builder = TrayAppBuilder.Create("demo") + .Toggle("Keep awake", () => false, _ => { }, persistAs: "awake"); + + ArgumentException ex = Assert.ThrowsExactly( + () => builder.Toggle("Keep display on", () => false, _ => { }, persistAs: "awake")); + + Assert.Contains("'awake'", ex.Message, StringComparison.Ordinal); + Assert.AreEqual("persistAs", ex.ParamName); + Assert.HasCount(1, builder.Build().Toggles); + } + + [TestMethod] + public void Toggle_WithPersistAsKeysThatDifferOnlyInCase_KeepsBoth() + { + // The store compares keys ordinally, so these are two remembered values. + TrayAppDefinition app = TrayAppBuilder.Create("demo") + .Toggle("Keep awake", () => false, _ => { }, persistAs: "awake") + .Toggle("Keep display on", () => false, _ => { }, persistAs: "Awake") + .Toggle("Not remembered", () => false, _ => { }) + .Toggle("Not remembered either", () => false, _ => { }) + .Build(); + + Assert.HasCount(4, app.Toggles); + } + + [TestMethod] + [DataRow("")] + [DataRow(" ")] + public void Toggle_WithABlankPersistAsKey_Throws(string key) + { + ArgumentException ex = Assert.ThrowsExactly( + () => TrayAppBuilder.Create("demo").Toggle("Keep awake", () => false, _ => { }, persistAs: key)); + + Assert.AreEqual("persistAs", ex.ParamName); + } + [TestMethod] public void Build_CollectsItemsTogglesAndOptions() { diff --git a/TrayApp/TrayAppBuilder.cs b/TrayApp/TrayAppBuilder.cs index 73cf5ca..fdbc14c 100644 --- a/TrayApp/TrayAppBuilder.cs +++ b/TrayApp/TrayAppBuilder.cs @@ -205,9 +205,14 @@ public TrayAppBuilder ActiveWhen(Func predicate) /// /// The name to remember this toggle under between runs, or not to remember it. /// Only meaningful alongside . + /// Each remembered toggle needs a key of its own, compared ordinally: two toggles sharing one would read and + /// write a single remembered value, and the next launch would restore both to it. /// /// The builder, for chaining. /// , , or is . + /// + /// is empty or white space, or another toggle already uses it. + /// public TrayAppBuilder Toggle( string label, Func getState, @@ -215,6 +220,19 @@ public TrayAppBuilder Toggle( Func? isEnabled = null, string? persistAs = null) { + if (persistAs is not null) + { + if (string.IsNullOrWhiteSpace(persistAs)) + { + throw new ArgumentException("A preference key cannot be empty or white space.", nameof(persistAs)); + } + + if (toggles.Any(existing => string.Equals(existing.PreferenceKey, persistAs, StringComparison.Ordinal))) + { + throw new ArgumentException($"'{persistAs}' is already remembered by another toggle.", nameof(persistAs)); + } + } + TrayToggleItem toggle = new(label, getState, setState, isEnabled, persistAs); items.Add(toggle); toggles.Add(toggle);