Files
cthulhu/internal/app/preferences_session_test.go

162 lines
5.7 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"encoding/json"
"io"
"log/slog"
"path/filepath"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/preferences"
"git.stormux.org/storm/cthulhu/internal/speech"
)
func TestPreferencesSessionRestoresPreviewAfterRejectedApplyAndReopen(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
settings := config.DefaultSettings()
settings.Path = filepath.Join(t.TempDir(), "settings.toml")
updates := make(chan speech.Settings, 3)
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), settings, settingsUpdateFunc(func(_ context.Context, value speech.Settings) error {
updates <- value
return nil
}))
worker.announceApply = func(message string) { t.Errorf("preview or rejected Apply announced success: %q", message) }
reader, writer := io.Pipe()
defer reader.Close()
defer writer.Close()
stopIO := context.AfterFunc(ctx, func() {
reader.CloseWithError(ctx.Err())
writer.CloseWithError(ctx.Err())
})
defer stopIO()
requests := make(chan preferences.Request)
readDone := make(chan error, 1)
done := make(chan error, 1)
go func() {
done <- worker.serve_preferences(ctx, ctx, json.NewEncoder(writer), requests, readDone)
}()
decoder := json.NewDecoder(reader)
read_response := func() preferences.Response {
t.Helper()
var response preferences.Response
if err := decoder.Decode(&response); err != nil {
t.Fatal(err)
}
return response
}
rate := 75.0
values := preferences.FromSettings(settings)
values.Rate = &rate
select {
case requests <- preferences.Request{Values: values, Preview: true}:
case <-ctx.Done():
t.Fatal(ctx.Err())
}
if response := read_response(); !response.Accepted || !response.Preview {
t.Fatalf("preview was rejected: %+v", response)
}
if len(updates) != 1 {
t.Fatal("preview did not produce exactly one speech update")
}
if update := <-updates; update.Rate == nil || *update.Rate == 0 {
t.Fatalf("preview did not reach speech: %+v", update)
}
worker.Open()
if response := read_response(); !response.Present || response.Close {
t.Fatalf("reopen did not raise the existing window: %+v", response)
}
values.Punctuation = 99
select {
case requests <- preferences.Request{Values: values}:
case <-ctx.Done():
t.Fatal(ctx.Err())
}
if response := read_response(); response.Accepted || response.Close {
t.Fatalf("invalid draft committed or closed: %+v", response)
}
readDone <- io.EOF
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-ctx.Done():
t.Fatal("preferences session did not join after helper EOF")
}
if len(updates) != 1 {
t.Fatal("exit did not produce exactly one restoration update")
}
if update := <-updates; update.Rate != nil {
t.Fatalf("helper exit did not restore the server default rate: %+v", update)
}
if len(updates) != 0 || worker.settings.DefaultVoice.Rate != nil || worker.settings.VerbalizePunctuationStyle != settings.VerbalizePunctuationStyle {
t.Fatal("reopen or rejected apply committed preview settings")
}
if loaded, err := config.Load(settings.Path); err != nil || loaded.Loaded {
t.Fatalf("preview/rejection wrote settings: loaded=%t, err=%v", loaded.Loaded, err)
}
}
func TestChooserSelectionSavesSelectedProfileWhileAnotherAppIsActive(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
settings := config.DefaultSettings()
settings.Path = filepath.Join(t.TempDir(), "settings.toml")
store, err := config.NewStore(settings.Path)
if err != nil {
t.Fatal(err)
}
if err = store.Save(settings); err != nil {
t.Fatal(err)
}
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), settings, settingsUpdateFunc(func(context.Context, speech.Settings) error { return nil }))
worker.discover = func(context.Context, string) (speech.Catalog, error) { return nil, nil }
worker.switchApplication(ctx, "Focused Application")
worker.choosingApplication = true
reader, writer := io.Pipe()
defer reader.Close()
defer writer.Close()
stopIO := context.AfterFunc(ctx, func() { reader.CloseWithError(ctx.Err()); writer.CloseWithError(ctx.Err()) })
defer stopIO()
requests := make(chan preferences.Request)
readDone := make(chan error, 1)
done := make(chan error, 1)
go func() { done <- worker.serve_preferences(ctx, ctx, json.NewEncoder(writer), requests, readDone) }()
requests <- preferences.Request{Application: "Chosen Application"}
decoder := json.NewDecoder(reader)
var initial preferences.Response
if err = decoder.Decode(&initial); err != nil {
t.Fatal(err)
}
if initial.Application != "Chosen Application" || initial.ChooseApplication || !initial.CanSave {
t.Fatalf("selected application preferences = %+v", initial)
}
values := initial.Values
values.Reading.AutomaticallyReadClipboardChanges = true
requests <- preferences.Request{Values: values, Save: true}
var response preferences.Response
if err = decoder.Decode(&response); err != nil || !response.Accepted {
t.Fatalf("apply = %+v, %v", response, err)
}
readDone <- io.EOF
if err = <-done; err != nil {
t.Fatal(err)
}
chosen, mapped, err := store.LoadForApplication("Chosen Application")
if err != nil || !mapped || !chosen.Reading.AutomaticallyReadClipboardChanges {
t.Fatalf("chosen override = %t, mapped %t, %v", chosen.Reading.AutomaticallyReadClipboardChanges, mapped, err)
}
focused, _, err := store.LoadForApplication("Focused Application")
if err != nil || focused.Reading.AutomaticallyReadClipboardChanges || worker.settings.Reading.AutomaticallyReadClipboardChanges {
t.Fatal("chosen application's preference leaked into focused application")
}
}