Speech settings updated, no large decimals for settings. Also changing with quick settings keys now applies across window changes.

This commit is contained in:
Storm Dragon committed 2026-10-25 19:35:55 -04:00
1 parent 7ae2801e53
commit 6ee0454fbe
17 files changed
+406 -15

No files matched your search

+210
View File
@@ -7,11 +7,14 @@ import (
"context"
"errors"
"log/slog"
"os"
"path/filepath"
"reflect"
"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"
)
@@ -80,6 +83,7 @@ func TestSpeechShortcutsApplySaveAndFailure(t *testing.T) {
t.Fatalf("saved settings did not retain speech choices: %+v", saved.DefaultVoice)
}
worker.settings.Path = t.TempDir()
worker.globalSettings.Path = worker.settings.Path
command(input.SaveSpeechSettings)
if message != "Speech settings could not be saved" {
t.Fatal(message)
@@ -91,6 +95,212 @@ func TestSpeechShortcutsApplySaveAndFailure(t *testing.T) {
}
}
func TestQuickSpeechFollowsWindowSwitchesAndSavesGlobally(t *testing.T) {
for index, name := range []string{"rate", "pitch", "volume", "synthesizer", "voice"} {
t.Run(name, func(t *testing.T) {
global := config.DefaultSettings()
global.Path = filepath.Join(t.TempDir(), "settings.toml")
rate, pitch, gain := 50.0, 5.0, 5.0
global.DefaultVoice = config.Voice{Rate: &rate, AveragePitch: &pitch, Gain: &gain,
Family: &config.VoiceFamily{Name: "one", Lang: "en"}}
global.SpeechOutputModule = "alpha"
application := global
appRate, appPitch, appGain := 25.0, 2.0, 2.0
application.DefaultVoice = config.Voice{Rate: &appRate, AveragePitch: &appPitch, Gain: &appGain,
Family: &config.VoiceFamily{Name: "three", Lang: "en"}}
application.SpeechOutputModule = "beta"
application.TimeFormat = "24-hour"
store, err := config.NewStore(global.Path)
if err != nil {
t.Fatal(err)
}
if err := store.Save(global); err != nil {
t.Fatal(err)
}
for _, app := range []string{"Cached Application", "New Application"} {
if err := store.SaveForApplication(app, application); err != nil {
t.Fatal(err)
}
}
original, err := os.ReadFile(global.Path)
if err != nil {
t.Fatal(err)
}
var applied speech.Settings
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), global,
settingsUpdateFunc(func(_ context.Context, next speech.Settings) error { applied = next; return nil }))
worker.live = &liveSettings{}
worker.live.Store(global)
worker.catalog = speech.Catalog{"alpha": {{Name: "one", Language: "en"}, {Name: "two", Language: "en"}},
"beta": {{Name: "three", Language: "en"}}}
ctx := t.Context()
worker.switchApplication(ctx, "Cached Application")
worker.switchApplication(ctx, "Presentation")
worker.speechSetting = index
worker.handleSpeechCommand(ctx, input.IncreaseSpeechSetting)
adjusted := worker.settings
check := func(base config.Settings) {
t.Helper()
switch index {
case 0:
value := 51.0
base.DefaultVoice.Rate = &value
case 1:
value := 5.1
base.DefaultVoice.AveragePitch = &value
case 2:
value := 5.1
base.DefaultVoice.Gain = &value
case 3:
base.SpeechOutputModule = "beta"
base.DefaultVoice.Family = &config.VoiceFamily{Name: "three", Lang: "en"}
case 4:
base.SpeechOutputModule = "alpha"
base.DefaultVoice.Family = &config.VoiceFamily{Name: "two", Lang: "en"}
}
if !reflect.DeepEqual(speechSettingsFromConfig(worker.settings), speechSettingsFromConfig(base)) ||
!reflect.DeepEqual(speechSettingsFromConfig(worker.live.Load()), speechSettingsFromConfig(base)) ||
!reflect.DeepEqual(applied, speechSettingsFromConfig(base)) {
t.Fatalf("speech choice lost in %q: got %+v, want %+v", worker.activeApp, worker.settings.DefaultVoice, base.DefaultVoice)
}
if worker.settings.TimeFormat != base.TimeFormat {
t.Fatal("quick speech changed unrelated application preferences")
}
}
check(global)
for _, app := range []string{"Cached Application", "New Application", "Other Application", "Presentation"} {
worker.switchApplication(ctx, app)
base := global
if app == "Cached Application" || app == "New Application" {
base = application
}
check(base)
}
contents, err := os.ReadFile(global.Path)
if err != nil || string(contents) != string(original) {
t.Fatalf("unsaved shortcuts wrote settings: %v", err)
}
// Saving while an application profile is active must update the global
// speech choice without copying its other preferences into the default.
worker.switchApplication(ctx, "Cached Application")
worker.handleSpeechCommand(ctx, input.SaveSpeechSettings)
loaded, err := store.Load()
if err != nil || !reflect.DeepEqual(speechSettingsFromConfig(loaded), speechSettingsFromConfig(adjusted)) || loaded.TimeFormat != global.TimeFormat {
t.Fatalf("global speech settings were not saved: %+v, %v", loaded, err)
}
appLoaded, mapped, err := store.LoadForApplication("Cached Application")
if err != nil || !mapped || !reflect.DeepEqual(speechSettingsFromConfig(appLoaded), speechSettingsFromConfig(application)) {
t.Fatalf("save shortcut overwrote application profile: %+v, %v", appLoaded, err)
}
if _, mapped, err := store.LoadForApplication("Presentation"); err != nil || mapped {
t.Fatalf("quick speech created an application profile: %t, %v", mapped, err)
}
worker.switchApplication(ctx, "Presentation")
check(global)
worker.switchApplication(ctx, "Cached Application")
check(application)
restarted := newPreferencesWorker(slog.New(slog.DiscardHandler), loaded,
settingsUpdateFunc(func(_ context.Context, next speech.Settings) error { applied = next; return nil }))
restarted.switchApplication(ctx, "Presentation")
if !reflect.DeepEqual(speechSettingsFromConfig(restarted.settings), speechSettingsFromConfig(adjusted)) {
t.Fatal("saved global speech did not survive restart")
}
})
}
}
func TestQuickSpeechFailureAndPreferencesReplacement(t *testing.T) {
global := config.DefaultSettings()
global.Path = filepath.Join(t.TempDir(), "settings.toml")
rate := 50.0
global.DefaultVoice.Rate = &rate
var updateError error
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), global,
settingsUpdateFunc(func(context.Context, speech.Settings) error { return updateError }))
application := global
appRate := 25.0
application.DefaultVoice.Rate = &appRate
worker.appSettings["Example Editor"] = application
ctx := t.Context()
worker.switchApplication(ctx, "Presentation")
worker.handleSpeechCommand(ctx, input.IncreaseSpeechSetting)
worker.handleSpeechCommand(ctx, input.SelectNextSpeechSetting)
worker.handleSpeechCommand(ctx, input.IncreaseSpeechSetting)
message := ""
worker.announce = func(text string) { message = text }
updateError = errors.New("backend unavailable")
worker.handleSpeechCommand(ctx, input.IncreaseSpeechSetting)
updateError = nil
path := worker.globalSettings.Path
worker.globalSettings.Path = t.TempDir()
worker.handleSpeechCommand(ctx, input.SaveSpeechSettings)
if message != "Speech settings could not be saved" {
t.Fatal(message)
}
worker.globalSettings.Path = path
worker.switchApplication(ctx, "Example Editor")
if *worker.settings.DefaultVoice.Rate != 51 || *worker.settings.DefaultVoice.AveragePitch != 5.1 {
t.Fatal("failed adjustment or save lost prior session choices")
}
if !worker.beginEdit(ctx, "") {
t.Fatal("could not open global preferences")
}
values := preferences.FromSettings(worker.editing)
appliedRate := 60.0
values.Rate = &appliedRate
updateError = errors.New("backend unavailable")
if response := worker.apply(ctx, preferences.Request{Values: values}); response.Accepted {
t.Fatal("failed preferences apply was accepted")
}
if settings, err := worker.applicationSettings("Example Editor"); err != nil || *settings.DefaultVoice.Rate != 51 {
t.Fatalf("failed preferences apply discarded quick speech: %v", err)
}
updateError = nil
if response := worker.apply(ctx, preferences.Request{Values: values}); !response.Accepted {
t.Fatal(response.Message)
}
if *worker.settings.DefaultVoice.Rate != 25 || *worker.globalSettings.DefaultVoice.Rate != 60 {
t.Fatal("explicit preferences apply did not restore application profile ownership")
}
worker.editingActive = false
worker.switchApplication(ctx, "Presentation")
if *worker.settings.DefaultVoice.Rate != 60 {
t.Fatal("old quick speech choice replaced applied preferences")
}
}
func TestSpeechShortcutReadsAndSavesRoundedNumbers(t *testing.T) {
settings := config.DefaultSettings()
settings.Path = filepath.Join(t.TempDir(), "settings.toml")
rate, pitch, gain := 89.99999999999999, 4.499999999999998, 4.999999999999999
settings.DefaultVoice = config.Voice{Rate: &rate, AveragePitch: &pitch, Gain: &gain}
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), settings, nil)
message := ""
worker.announce = func(text string) { message = text }
for index, want := range []string{"Rate 90", "Pitch 4.5", "Volume 5"} {
command := input.SelectNextSpeechSetting
if index == 0 {
command = input.SelectPreviousSpeechSetting
}
worker.handleSpeechCommand(t.Context(), command)
if message != want {
t.Fatalf("got %q, want %q", message, want)
}
}
if _, err := os.Stat(settings.Path); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("checking speech settings created a file: %v", err)
}
worker.handleSpeechCommand(t.Context(), input.SaveSpeechSettings)
store, err := config.NewStore(settings.Path)
if err != nil {
t.Fatal(err)
}
saved, err := store.Load()
if err != nil || *saved.DefaultVoice.Rate != 90 || *saved.DefaultVoice.AveragePitch != 4.5 || *saved.DefaultVoice.Gain != 5 {
t.Fatalf("speech numbers were not saved rounded: %+v, %v", saved.DefaultVoice, err)
}
}
func TestSpeechShortcutNumericBounds(t *testing.T) {
for index, maximum := range []float64{99, 9, 9} {
settings := config.DefaultSettings()