diff --git a/CLAUDE.md b/CLAUDE.md index ced80f2..465ac50 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,8 +98,9 @@ The type is `TrayAppBuilder` rather than `TrayApp` because a type whose name mat its own namespace makes every `TrayApp.Something` reference ambiguous - which is the reason CA1724 is suppressed across ktsu.Sdk, and not a reason to spend the name here. -A run goes: parse, then `--help`/`--version`, then restore preferences (tray only), then apply the -tool's own flags, then `--status`, then the run. The order of the middle two is the point: a remembered +A run goes: parse, then `--help`/`--version`, then restore preferences (tray only, and not for +`--status`, which reports the live state and must not run a setter), then apply the tool's own flags, +then `--status`, then the run. The order of the middle two is the point: a remembered setting is restored first so that what was typed this time beats what the tray was left set to. The parser records matches rather than invoking handlers, which is what makes that order possible. diff --git a/TrayApp.Test/TrayAppBuilderTests.cs b/TrayApp.Test/TrayAppBuilderTests.cs index d563747..f657120 100644 --- a/TrayApp.Test/TrayAppBuilderTests.cs +++ b/TrayApp.Test/TrayAppBuilderTests.cs @@ -251,6 +251,43 @@ public async Task RunAsync_WithStatus_ReportsWithoutStarting() Assert.IsFalse(started); } + [TestMethod] + public async Task RunAsync_WithStatusAndARememberedToggle_RunsNoSetterAndReportsTheLiveState() + { + InMemoryPersistenceProvider provider = new(); + + using (DebouncedPreferenceStore seed = new(provider, "tray", Debounce)) + { + await seed.LoadAsync().ConfigureAwait(false); + seed.Set("awake", true); + await seed.FlushAsync().ConfigureAwait(false); + } + + bool awake = false; + int setterCalls = 0; + bool? awakeWhenReported = null; + + int exitCode = await TrayAppBuilder.Create("demo") + .Toggle("Keep awake", () => awake, value => + { + setterCalls++; + awake = value; + }, persistAs: "awake") + .Status(() => + { + awakeWhenReported = awake; + return awake ? "Keeping awake" : "Idle"; + }) + .Preferences(provider) + .RunAsync(["--tray", "--status"]) + .ConfigureAwait(false); + + // A status query must not take what the toggle guards: it exits without OnStop to give it back. + Assert.AreEqual(0, exitCode); + Assert.AreEqual(0, setterCalls); + Assert.IsFalse(awakeWhenReported); + } + [TestMethod] public void Menu_BuiltFromADefinition_CarriesTheToolsItems() { diff --git a/TrayApp/TrayAppBuilder.cs b/TrayApp/TrayAppBuilder.cs index 85299cf..6aa6aa4 100644 --- a/TrayApp/TrayAppBuilder.cs +++ b/TrayApp/TrayAppBuilder.cs @@ -390,7 +390,9 @@ public Task RunAsync(string[] args) { // Preferences are the tray's: a command line run says what it wants, so letting a remembered // setting override an argument would be surprising, and the console path never reads them. - if (useTray && app.Persistence is not null) + // --status is a read-only report, so it restores nothing: a restore runs toggle setters that can + // take a resource, and the report returns without the OnStop that would release it. + if (useTray && !options.ShowStatus && app.Persistence is not null) { store = new DebouncedPreferenceStore(app.Persistence, app.PreferenceKey, app.PreferenceDebounce); restoreError = LoadPreferences(app, store);