Files
fenrir/internal/app/runtime_settings_test.go

120 lines
4.9 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"path/filepath"
"slices"
"strings"
"testing"
"time"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/config"
"git.stormux.org/storm/fenrir/internal/control"
"git.stormux.org/storm/fenrir/internal/input"
"git.stormux.org/storm/fenrir/internal/screen"
)
func TestMenuRemoteAndQuickSavePreserveNextLaunchChoices(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
path := filepath.Join(t.TempDir(), "settings.toml")
base := config.Defaults()
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 1, Active: true, Rows: 1, Cols: 5, Cells: []rune("alpha")}}
voice := &fakeVoice{}
keys := &fakeKeys{poll: func(bool, func(input.Decision)) error { return nil }}
a, err := new_application(ctx, source, keys, voice, RunOptions{Settings: base, ConfigPath: path})
if err != nil {
t.Fatal(err)
}
defer a.ui.close()
// Exercise the successful menu-save callback with both live and next-launch
// choices, then interleave session commands, remote changes and a quick save.
menuSaved := base
menuSaved.Keyboard.Layout = "laptop"
menuSaved.Presentation.Speech.Module = "next-module"
menuSaved.Presentation.Speech.Rate = 60
menuSaved.Review.LineBreak = false
menuSaved.Presentation.Sound.Driver = "alsa"
menuSaved.Presentation.Sound.Theme = "fixture-pack"
menuSaved.Presentation.Sound.Device = "null"
if err := config.Save(path, menuSaved); err != nil {
t.Fatal(err)
}
a.apply_saved_settings(menuSaved, false)
if a.settings.live.Keyboard.Layout != base.Keyboard.Layout || a.settings.live.Presentation.Speech.Module != base.Presentation.Speech.Module || a.settings.live.Review != base.Review || a.settings.live.Presentation.Sound != base.Presentation.Sound {
t.Fatal("next-launch choices changed the running configuration")
}
if len(voice.parameters) == 0 || voice.parameters[len(voice.parameters)-1].Rate != 60 {
t.Fatal("menu speech adjustment did not apply live", voice.parameters)
}
for _, id := range []command.ID{command.Mute, command.AutoRead, command.ToggleAttributes} {
a.handle_input(input.Decision{Command: id})
}
for _, request := range []struct {
text string
want string
}{
{"SETTING SET speech#enabled=True;unknown#setting=True", "Unsupported remote setting"},
{"SETTING SET keyboard#word_echo=True", ""},
} {
replies := make(chan string, 1)
a.remote_request(control.Request{UID: 0, Text: request.text, Reply: replies})
if reply := <-replies; !strings.HasPrefix(reply, request.want) || request.want == "" && reply != "" {
t.Fatal("unexpected remote response", reply)
}
if !voice.muted || a.engine.Automatic_reading() {
t.Fatal("remote assignment changed unrelated session state")
}
}
for _, id := range []command.ID{command.NextQuickValue, command.SaveSettings, command.Quit} {
a.handle_input(input.Decision{Command: id})
}
if err := a.loop(nil); err != nil || ctx.Err() != nil {
t.Fatal("save and quit failed", err, ctx.Err())
}
saved, _, err := config.Load(path, nil)
if err != nil {
t.Fatal(err)
}
want := menuSaved
want.Keyboard.WordEcho = true
want.Presentation.Speech.Rate = 70
if saved.Keyboard.Layout != want.Keyboard.Layout || saved.Presentation.Speech.Module != want.Presentation.Speech.Module || saved.Review != want.Review || saved.Presentation.Speech.Rate != want.Presentation.Speech.Rate || !saved.Keyboard.WordEcho || saved.Presentation.Sound != want.Presentation.Sound {
t.Fatal("mixed settings paths lost saved choices", saved)
}
if !saved.Presentation.Speech.Enabled || !saved.Reading.Automatic || saved.Reading.Attributes != base.Reading.Attributes {
t.Fatal("session overrides leaked into the save", saved)
}
freshVoice := &fakeVoice{}
fresh, err := new_application(ctx, source, &fakeKeys{}, freshVoice, RunOptions{Settings: saved})
if err != nil {
t.Fatal(err)
}
defer fresh.ui.close()
if err := fresh.read_screen(); err != nil || !slices.Contains(freshVoice.messages, "alpha") {
t.Fatal("saved defaults did not restore startup reading", err, freshVoice.messages)
}
}
func TestRemoteCannotEnableSpeechWithoutStartupConnection(t *testing.T) {
base := config.Defaults()
base.Presentation.Speech.Enabled = false
a, err := new_application(context.Background(), &fakeSource{}, &fakeKeys{}, disabledVoice{}, RunOptions{Settings: base})
if err != nil {
t.Fatal(err)
}
defer a.ui.close()
response := a.apply_remote_settings("SET speech#enabled=True;keyboard#word_echo=True")
if response != "Speech was disabled at startup; enable it in settings and relaunch" {
t.Fatal(response)
}
if !a.muted || a.settings.live.Presentation.Speech.Enabled || a.settings.live.Keyboard.WordEcho || a.settings.saveDraft.Presentation.Speech.Enabled || a.settings.saveDraft.Keyboard.WordEcho {
t.Fatal("rejected request partially changed live or saved settings")
}
}