Files
cthulhu/internal/app/wine_preferences_test.go
hjozwiak 75f34ea767 Add global Wine preferences tab
Phase 3 of the Wine/Proton stage (decisions 6, 9, 13, 19 in docs/testing/wine-proton.md): a global-only Wine tab modeled on the Pronunciation page, with the controller-only checkbox and a typed editable program list. Wine values flow through the preferences protocol so a global Apply reaches the live Wine settings from Phase 2 with no reader restart. Entries are validated as edited and named in-tab, blocking Apply until fixed; case-insensitive duplicates collapse. Per-application preferences expose no Wine editing and an app-scoped Apply cannot change the governing list.
2026-11-02 02:49:33 -05:00

109 lines
4.4 KiB
Go

package app
import (
"context"
"log/slog"
"path/filepath"
"reflect"
"strings"
"testing"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/preferences"
"git.stormux.org/storm/cthulhu/internal/speech"
"git.stormux.org/storm/cthulhu/internal/wine"
)
// The Wine tab is global-only: a global preferences Apply carries the edited
// Wine values through the preferences protocol into the live global Wine
// source the manager reads, persists them through TOML, and needs no reader
// restart. An application-scoped Apply never changes the governing values.
func TestWinePreferencesGlobalApplyGovernsAndPersists(t *testing.T) {
path := filepath.Join(t.TempDir(), "settings.toml")
global := config.DefaultSettings()
global.Path = path
global.Wine = config.WineSettings{ExcludedPrograms: []string{"Game.exe"}}
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)
worker.live.StoreGlobalWine(global.Wine)
manager := newWineManager(slog.New(slog.DiscardHandler), worker.live)
governing := func() wine.Settings { return manager.SettingsSource() }
ctx := context.Background()
// An invalid entry is named and blocks the global Apply.
if !worker.beginEdit(ctx, "") {
t.Fatal("global preferences did not open")
}
invalid := preferences.FromSettings(worker.editing)
invalid.Wine.ExcludedPrograms = []string{"Game.exe", `C:\Games\Sneaky.exe`}
response := worker.apply(ctx, preferences.Request{Values: invalid, Save: true})
if response.Accepted || !strings.Contains(response.Message, `C:\Games\Sneaky.exe`) {
t.Fatalf("invalid Wine entry was not named and blocked: %+v", response)
}
if want := (wine.Settings{ExcludedPrograms: []string{"Game.exe"}}); !reflect.DeepEqual(governing(), want) {
t.Fatalf("rejected Apply changed governing Wine settings: %+v", governing())
}
// A valid global Apply reaches the same manager with no restart and
// persists through TOML.
values := preferences.FromSettings(worker.editing)
values.Wine = config.WineSettings{ControllerOnly: true,
ExcludedPrograms: []string{"Game.exe", "New Game.exe", "new game.EXE"}}
response = worker.apply(ctx, preferences.Request{Values: values, Save: true})
if !response.Accepted {
t.Fatalf("global Wine Apply rejected: %+v", response)
}
want := wine.Settings{ControllerOnly: true, ExcludedPrograms: []string{"Game.exe", "New Game.exe"}}
if !reflect.DeepEqual(governing(), want) {
t.Fatalf("governing Wine settings after Apply = %+v, want %+v", governing(), want)
}
loaded, err := store.Load()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(loaded.Wine, config.WineSettings(want)) {
t.Fatalf("persisted Wine settings = %+v, want %+v", loaded.Wine, want)
}
worker.editingActive = false
// An application-scoped Apply has no Wine editing surface. Even a
// request carrying different Wine values cannot change the governing
// list, the global profile, or the application profile's Wine values.
worker.switchApplication(ctx, "Example Editor")
if !worker.beginEdit(ctx, "\x00") || worker.editApp != "Example Editor" {
t.Fatal("application preferences did not open")
}
appValues := preferences.FromSettings(worker.editing)
appValues.TimeFormat = "24-hour"
appValues.Wine = config.WineSettings{ExcludedPrograms: []string{"Sneaky.exe"}}
response = worker.apply(ctx, preferences.Request{Values: appValues, Save: true})
if !response.Accepted || worker.settings.TimeFormat != "24-hour" {
t.Fatalf("application Apply rejected: %+v", response)
}
if !reflect.DeepEqual(governing(), want) {
t.Fatalf("application Apply changed governing Wine settings: %+v", governing())
}
if !reflect.DeepEqual(worker.globalSettings.Wine, config.WineSettings(want)) {
t.Fatalf("application Apply changed global Wine settings: %+v", worker.globalSettings.Wine)
}
if !reflect.DeepEqual(worker.appSettings["Example Editor"].Wine, config.WineSettings(want)) {
t.Fatalf("application Apply rewrote Wine values: %+v", worker.appSettings["Example Editor"].Wine)
}
reloaded, err := store.Load()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(reloaded.Wine, config.WineSettings(want)) {
t.Fatalf("application Apply persisted into the global Wine profile: %+v", reloaded.Wine)
}
}