162 lines
5.7 KiB
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")
|
|
}
|
|
}
|