package app import ( "context" "log/slog" "path/filepath" "testing" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/preferences" "git.stormux.org/storm/cthulhu/internal/speech" ) func TestApplicationPreferencesSwitchAndPersistIndependently(t *testing.T) { path := filepath.Join(t.TempDir(), "settings.toml") global := config.DefaultSettings() global.Path = path store, err := config.NewStore(path) if err != nil { t.Fatal(err) } if err := store.Save(global); err != nil { t.Fatal(err) } worker := newPreferencesWorker(slog.New(slog.DiscardHandler), global, settingsUpdateFunc(func(context.Context, speech.Settings) error { return nil })) worker.live = &liveSettings{} worker.live.Store(global) ctx := context.Background() worker.switchApplication(ctx, "Example Editor") if !worker.beginEdit(ctx, "\x00") || worker.editApp != "Example Editor" { t.Fatal("application shortcut did not select focused application") } values := preferences.FromSettings(worker.editing) values.TimeFormat = "24-hour" values.Reading.AutomaticallyReadClipboardChanges = true response := worker.apply(ctx, preferences.Request{Values: values, Save: true}) if !response.Accepted || worker.settings.TimeFormat != "24-hour" || !worker.settings.Reading.AutomaticallyReadClipboardChanges { t.Fatalf("application settings were not applied: %+v", response) } worker.editingActive = false worker.switchApplication(ctx, "Other Editor") if worker.settings.TimeFormat != global.TimeFormat || worker.settings.Reading.AutomaticallyReadClipboardChanges { t.Fatal("application settings leaked to another application") } worker.switchApplication(ctx, "Example Editor") if worker.settings.TimeFormat != "24-hour" || worker.live.Load().TimeFormat != "24-hour" { t.Fatal("application settings were not restored on return") } worker.handleSpeechCommand(ctx, input.IncreaseSpeechSetting) worker.handleSpeechCommand(ctx, input.SaveSpeechSettings) if !worker.beginEdit(ctx, "") { t.Fatal("global preferences did not open") } globalValues := preferences.FromSettings(worker.editing) globalValues.DateFormat = "numbers-mdy" if response := worker.apply(ctx, preferences.Request{Values: globalValues, Save: true}); !response.Accepted { t.Fatalf("global setting was not applied: %+v", response) } worker.editingActive = false if worker.settings.TimeFormat != "24-hour" || worker.settings.DateFormat != "numbers-mdy" { t.Fatalf("application session did not inherit global date: %+v", worker.settings) } loaded, err := store.Load() if err != nil || loaded.TimeFormat != global.TimeFormat || loaded.DateFormat != "numbers-mdy" { t.Fatalf("global profile changed: %+v, %v", loaded, err) } appLoaded, mapped, err := store.LoadForApplication("Example Editor") if err != nil || !mapped || appLoaded.TimeFormat != "24-hour" || appLoaded.DateFormat != "numbers-mdy" || appLoaded.DefaultVoice.Rate == nil || !appLoaded.Reading.AutomaticallyReadClipboardChanges { t.Fatalf("application profile not saved: %+v, %t, %v", appLoaded, mapped, err) } if loaded.DefaultVoice.Rate != nil || loaded.Reading.AutomaticallyReadClipboardChanges { t.Fatal("application speech shortcut overwrote global voice") } restarted := newPreferencesWorker(slog.New(slog.DiscardHandler), loaded, settingsUpdateFunc(func(context.Context, speech.Settings) error { return nil })) sessionGlobal := loaded sessionGlobal.DateFormat = "numbers-dmy" if response := restarted.applyNext(ctx, sessionGlobal, false, false); !response.Accepted { t.Fatalf("session global change rejected: %+v", response) } restarted.switchApplication(ctx, "Example Editor") if restarted.settings.DateFormat != "numbers-dmy" || restarted.settings.TimeFormat != "24-hour" || !restarted.settings.Reading.AutomaticallyReadClipboardChanges { t.Fatalf("first application load lost session inheritance: %+v", restarted.settings) } }