Application specific preferences saving now enabled.

This commit is contained in:
Storm Dragon committed 2026-10-28 04:51:39 -04:00
1 parent f88e5b81a2
commit 60abcc37d9
23 files changed
+1004 -248

No files matched your search

+166 -14
View File
@@ -6,10 +6,12 @@ package app
import (
"errors"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"time"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/config"
@@ -50,8 +52,8 @@ func TestOverrideRestoresSynthesizerAndKeepsSaveBlockedUntilCompletion(t *testin
if response := request_selection(t, a, "SET speech#voice=Temporary;speech#module=fixture"); response != "" {
t.Fatal(response)
}
if !reflect.DeepEqual(a.settings.saveDraft, baseline) {
t.Fatal("temporary voice modified draft")
if a.settings.saveDraft.Presentation.Speech.Voice != "Temporary" || !same_speech_selection(a.overrideCurrent.underlying.settings.saveDraft.Presentation.Speech, baseline.Presentation.Speech) {
t.Fatal("application voice did not stay in its own draft")
}
if failed {
a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return nil, errors.New("fixture unavailable") }
@@ -109,6 +111,19 @@ func override_read(t *testing.T, a *application, snapshot screen.Snapshot) {
if err := a.read_screen(); err != nil {
t.Fatal(err)
}
// Model the event loop's asynchronous profile-read completion, leaving
// speech transitions to the tests which explicitly exercise them.
if a.overrideTransition && !a.speechPending {
select {
case loaded := <-a.overrideLoads:
loaded()
case <-time.After(time.Second):
t.Fatal("profile load stalled")
}
if err := a.read_screen(); err != nil {
t.Fatal(err)
}
}
}
func TestApplicationsMenuActivatesOnlyOnEnterAndReleasesCapture(t *testing.T) {
@@ -183,7 +198,7 @@ func TestApplicationsMenuPrefixSearch(t *testing.T) {
}
}
func TestOverrideCannotSaveOrContaminateDraftAndRestoresSession(t *testing.T) {
func TestOverrideSavesOwnDraftAndRestoresSession(t *testing.T) {
a, voice := persistence_application(t, true)
a.handle_command(command.NextQuickValue) // Preserve a pre-existing unsaved adjustment.
a.handle_command(command.AutoRead)
@@ -202,20 +217,30 @@ func TestOverrideCannotSaveOrContaminateDraftAndRestoresSession(t *testing.T) {
if response := a.apply_remote_settings("SET speech#rate=0.77;keyboard#word_echo=True"); response != "" {
t.Fatal(response)
}
if !reflect.DeepEqual(a.settings.saveDraft, baseSettings.saveDraft) {
t.Fatal("override modified save draft")
if !reflect.DeepEqual(a.overrideCurrent.underlying.settings, baseSettings) {
t.Fatal("ordinary draft changed")
}
profilePath := filepath.Join(filepath.Dir(a.options.ConfigPath), "applications", "ninjam.toml")
a.save_settings()
if a.saver.busy || voice.messages[len(voice.messages)-1] != "Save unavailable." {
t.Fatal("shortcut save admitted")
finish_persistence(t, a, false)
if !strings.Contains(voice.messages[len(voice.messages)-1], "Ninjam application settings saved") {
t.Fatal(voice.messages)
}
for _, text := range []string{"SETTING SAVE", "SETTING SAVEAS " + a.options.ConfigPath} {
if got := runtime_request(a, text); got != "Save unavailable." {
t.Fatal(text, got)
}
doc, err := config.Open_application_document(profilePath, a.overrideCurrent.base)
if err != nil || doc.Settings.Presentation.Speech.Rate != a.settings.live.Presentation.Speech.Rate || !doc.Settings.Keyboard.WordEcho {
t.Fatal("profile not saved", err)
}
reply := persistence_request(a, "SAVE")
finish_persistence(t, a, false)
persistence_reply(t, reply, false)
if got := runtime_request(a, "SETTING SAVEAS "+a.options.ConfigPath); got != "SAVEAS is unavailable in application settings" {
t.Fatal(got)
}
a.open_settings()
receive_ui(t, a.ui)
if !strings.Contains(voice.messages[len(voice.messages)-1], "Ninjam application settings") {
t.Fatal(voice.messages)
}
for range 100 {
if control, ok := a.ui.session.Focused_control(); ok && control.Action == "save" {
break
@@ -223,9 +248,7 @@ func TestOverrideCannotSaveOrContaminateDraftAndRestoresSession(t *testing.T) {
a.ui.handle(interaction.Key{Name: "tab"})
}
a.ui.handle(interaction.Key{Name: "enter"})
if !a.ui.active || a.saver.busy || voice.messages[len(voice.messages)-1] != "Save unavailable." {
t.Fatal("menu save admitted", voice.messages)
}
finish_persistence(t, a, false)
a.ui.close()
a.select_override("default")
if !reflect.DeepEqual(a.settings, baseSettings) || a.quick.revision != baseQuick || !reflect.DeepEqual(a.engine.Capture_session(), baseRuntime) {
@@ -355,3 +378,132 @@ func TestActivationRejectsPendingOperationsAndRestartStartsWithoutOverride(t *te
t.Fatal("restart retained override", err)
}
}
func TestApplicationProfileReloadAndDefaultInheritance(t *testing.T) {
for _, terminalMode := range []bool{false, true} {
a, _ := persistence_application(t, terminalMode)
override_read(t, a, override_screen(1, "ninjam"))
a.select_override("ninjam")
override_read(t, a, override_screen(1, "ninjam"))
a.handle_command(command.NextQuickValue)
savedRate := a.settings.live.Presentation.Speech.Rate
a.save_settings()
// A terminal switch must wait for the admitted application write.
override_read(t, a, override_screen(3, "other terminal"))
if a.overrideCurrent == nil {
t.Fatal("save ownership lost before completion")
}
finish_persistence(t, a, false)
override_read(t, a, override_screen(3, "other terminal"))
if a.overrideCurrent != nil || a.settings.live.Presentation.Speech.Rate == savedRate {
t.Fatal("application leaked across terminal")
}
// Change an unrelated ordinary setting, then discard/reselect the adapter.
if text := a.apply_remote_settings("SET speech#pitch=0.6"); text != "" {
t.Fatal(text)
}
pitch := a.settings.live.Presentation.Speech.Pitch
override_read(t, a, override_screen(1, "ninjam"))
a.select_override("default")
a.select_override("ninjam")
override_read(t, a, override_screen(1, "ninjam"))
if a.settings.live.Presentation.Speech.Rate != savedRate || a.settings.live.Presentation.Speech.Pitch != pitch {
t.Fatal("saved rate or new inherited pitch missing", a.settings.live.Presentation.Speech)
}
if text := a.apply_remote_settings("SET speech#rate=0.1"); text != "" {
t.Fatal(text)
}
if text := a.reset_remote_settings(); text != "" {
t.Fatal(text)
}
if a.settings.live.Presentation.Speech.Rate != savedRate {
t.Fatal("RESET did not restore profile")
}
before := a.settings
if text := a.apply_remote_settings("SET speech#rate=0.2;screen#ignore_screen=1"); text == "" || !reflect.DeepEqual(a.settings, before) {
t.Fatal("global remote change was not rejected atomically")
}
override_read(t, a, override_screen(1, "[Username@host directory] $"))
if a.overrideCurrent != nil || a.settings.live.Presentation.Speech.Rate == savedRate {
t.Fatal("exit did not restore default")
}
}
}
func TestApplicationProfileVoiceLoadAndFailureLeaveDefaultIntact(t *testing.T) {
for _, failure := range []string{"", "invalid", "unavailable"} {
a, _, path := selection_application(t, true)
profilePath := filepath.Join(filepath.Dir(path), "applications", "ninjam.toml")
if err := os.MkdirAll(filepath.Dir(profilePath), 0700); err != nil {
t.Fatal(err)
}
data := "[presentation.speech]\nvoice = 'Application voice'\nrate = 12\n"
if failure == "invalid" {
data = "[presentation.speech]\nrate = 999\n"
}
if err := os.WriteFile(profilePath, []byte(data), 0600); err != nil {
t.Fatal(err)
}
a.prepareSpeech = func(config.Speech) (speech.Transport, error) {
if failure == "unavailable" {
return nil, errors.New("voice unavailable")
}
return &preparedVoice{}, nil
}
baseline := a.settings
override_read(t, a, override_screen(1, "ninjam"))
a.select_override("ninjam")
override_read(t, a, override_screen(1, "ninjam"))
if a.speechPending {
finish_selection(t, a)
}
if failure != "" {
if a.overrideCurrent != nil || !reflect.DeepEqual(a.settings, baseline) || a.saves_unavailable() {
t.Fatal("failed profile changed ordinary settings")
}
continue
}
override_read(t, a, override_screen(1, "ninjam"))
if a.settings.live.Presentation.Speech.Voice != "Application voice" || a.settings.live.Presentation.Speech.Rate != 12 {
t.Fatal("profile voice not applied")
}
a.select_override("default")
finish_selection(t, a)
if !reflect.DeepEqual(a.settings, baseline) {
t.Fatal("default voice not restored")
}
}
}
func TestApplicationMenuPreservesSessionTogglesAndLaunchChoices(t *testing.T) {
a, _ := persistence_application(t, true)
a.settings.saveDraft.Keyboard.Layout = "laptop"
a.settings.saveDraft.Presentation.Speech.Driver = "dectalk"
a.settings.saveDraft.Presentation.Speech.Voice = "Next launch voice"
override_read(t, a, override_screen(1, "ninjam"))
a.select_override("ninjam")
override_read(t, a, override_screen(1, "ninjam"))
a.handle_command(command.Mute)
a.handle_command(command.AutoRead)
a.open_settings()
receive_ui(t, a.ui)
for range 100 {
if control, ok := a.ui.session.Focused_control(); ok && control.Action == "save" {
break
}
a.ui.handle(interaction.Key{Name: "tab"})
}
a.ui.handle(interaction.Key{Name: "enter"})
finish_persistence(t, a, false)
if !a.muted || a.engine.Automatic_reading() || a.settings.live.Keyboard.Layout != "desktop" || a.settings.live.Presentation.Speech.Driver != "speech-dispatcher" || a.settings.live.Presentation.Speech.Voice != "" {
t.Fatal("menu changed session toggles or launch-only choices")
}
data, err := os.ReadFile(a.overrideCurrent.path)
if err != nil || strings.Contains(string(data), "enabled") || strings.Contains(string(data), "automatic") || strings.Contains(string(data), "driver") || strings.Contains(string(data), "layout") || strings.Contains(string(data), "voice") {
t.Fatal("menu saved inherited/session/launch settings", string(data), err)
}
a.select_override("default")
if a.settings.saveDraft.Keyboard.Layout != "laptop" || a.settings.saveDraft.Presentation.Speech.Driver != "dectalk" {
t.Fatal("ordinary next-launch choices lost")
}
}