package app import ( "context" "log/slog" "os" "path/filepath" "reflect" "testing" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/speech" "git.stormux.org/storm/cthulhu/internal/wine" ) // Wine settings are live and global-only: editing the TOML and applying the // reloaded global profile changes what the Wine manager will use at its next // poll or spawn, with no reader restart, while switching focus between // applications with their own profiles never changes the governing list. func TestWineManagerReadsLiveGlobalSettingsFromTOML(t *testing.T) { path := filepath.Join(t.TempDir(), "settings.toml") applications := "[applications]\n" + "'Example Editor' = 'editor'\n" + "'Other Editor' = 'other'\n" + "[profiles.editor.wine]\n" + "controller-only = true\n" + "excluded-hook-programs = ['EditorOnly.exe']\n" + "[profiles.other.wine]\n" + "excluded-hook-programs = ['OtherOnly.exe']\n" writeSettings := func(programs string) { t.Helper() contents := "schema-version = 2\nactive-profile = 'default'\n" + "[profiles.default.wine]\n" + "excluded-hook-programs = [" + programs + "]\n" + applications if err := os.WriteFile(path, []byte(contents), 0600); err != nil { t.Fatal(err) } } writeSettings("'Game.exe'") store, err := config.NewStore(path) if err != nil { t.Fatal(err) } global, err := store.Load() if 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) worker.live.StoreGlobalWine(global.Wine) manager := newWineManager(slog.New(slog.DiscardHandler), worker.live) if manager.SettingsSource == nil { t.Fatal("Wine manager has no live settings source") } governing := func() wine.Settings { return manager.SettingsSource() } if want := (wine.Settings{ExcludedPrograms: []string{"Game.exe"}}); !reflect.DeepEqual(governing(), want) { t.Fatalf("governing Wine settings = %+v, want %+v", governing(), want) } // Edit the TOML, reload it, and apply the loaded global profile through // the same path preferences Apply uses. The same manager instance must // now govern with the addition: no reader or manager restart happened. ctx := context.Background() writeSettings("'Game.exe', 'New Game.exe'") reloaded, err := store.Load() if err != nil { t.Fatal(err) } if response := worker.applyNext(ctx, reloaded, false, false); !response.Accepted { t.Fatalf("reloaded TOML settings were not applied: %+v", response) } want := wine.Settings{ExcludedPrograms: []string{"Game.exe", "New Game.exe"}} if !reflect.DeepEqual(governing(), want) { t.Fatalf("governing Wine settings after live addition = %+v, want %+v", governing(), want) } // Moving focus to an application whose profile carries different Wine // values changes the reader's effective settings, but must never change // which Wine list governs a prefix. worker.switchApplication(ctx, "Example Editor") if effective := worker.live.Load().Wine; !effective.ControllerOnly || !reflect.DeepEqual(effective.ExcludedPrograms, []string{"EditorOnly.exe"}) { t.Fatalf("application switch did not change effective Wine settings: %+v", effective) } if !reflect.DeepEqual(governing(), want) { t.Fatalf("application profile changed governing Wine settings: %+v, want %+v", governing(), want) } worker.switchApplication(ctx, "Other Editor") if effective := worker.live.Load().Wine; effective.ControllerOnly || !reflect.DeepEqual(effective.ExcludedPrograms, []string{"OtherOnly.exe"}) { t.Fatalf("second application switch did not change effective Wine settings: %+v", effective) } if !reflect.DeepEqual(governing(), want) { t.Fatalf("second application profile changed governing Wine settings: %+v, want %+v", governing(), want) } }