// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "os" "reflect" "slices" "testing" "git.stormux.org/storm/fenrir/internal/config" "git.stormux.org/storm/fenrir/internal/control" "git.stormux.org/storm/fenrir/internal/presentation" ) func runtime_request(a *application, text string) string { reply := make(chan string, 1) a.remote_request(control.Request{UID: os.Geteuid(), Text: text, Reply: reply}) return <-reply } func TestRemoteTrackingSelectionAndAtomicFlags(t *testing.T) { for _, baseMode := range []string{"cursor", "table", "highlight"} { base := config.Defaults() base.Reading.Mode = baseMode for _, trial := range []struct{ text, mode string }{ {"reading#mode=cursor", "cursor"}, {"reading#mode=table", "table"}, {"reading#mode=highlight", "highlight"}, {"focus#cursor=False;focus#highlight=True", "highlight"}, {"focus#highlight=True;focus#cursor=False", "highlight"}, {"focus#highlight=False,focus#cursor=True", "cursor"}, {"focus#cursor=False;focus#highlight=False", "table"}, } { updated, err := remote_settings(base, "SET "+trial.text) if err != nil || updated.Reading.Mode != trial.mode { t.Fatal(baseMode, trial, updated.Reading.Mode, err) } } for _, invalid := range []string{ "reading#mode=", "reading#mode=invalid", "reading#mode=CURSOR", "focus#cursor=invalid", "focus#cursor=True;focus#highlight=True", "reading#mode=cursor;focus#highlight=False", } { updated, err := remote_settings(base, "SET speech#enabled=False;"+invalid) if err == nil || !reflect.DeepEqual(updated, base) { t.Fatal("invalid selection partially applied", baseMode, invalid, updated, err) } } } } func TestRemoteTrackingLiveSaveResetAndGates(t *testing.T) { for _, terminalMode := range []bool{false, true} { a, _ := persistence_application(t, terminalMode) // A Python flag update must use the current mode for both live and // saved state, even when a menu draft chose a different startup mode. a.settings.saveDraft.Reading.Mode = "highlight" for _, text := range []string{"SETTING SET focus#cursor=False;focus#highlight=True", "SETTING SET reading#mode=highlight"} { a.settings.live.Remote.EnableSettingsRemote = false if runtime_request(a, text) == "" || a.engine.Tracking_mode() != "cursor" { t.Fatal("disabled remote changed mode") } a.settings.live.Remote.EnableSettingsRemote = true reply := make(chan string, 1) a.remote_request(control.Request{UID: os.Geteuid() + 1, Text: text, Reply: reply}) if <-reply == "" || a.engine.Tracking_mode() != "cursor" { t.Fatal("unauthorized caller changed mode") } if response := runtime_request(a, text); response != "" || a.engine.Tracking_mode() != "highlight" || a.settings.saveDraft.Reading.Mode != "highlight" { t.Fatal("tracking not applied to live/draft", response) } if response := runtime_request(a, "SETTING RESET"); response != "" || a.engine.Tracking_mode() != "cursor" || a.settings.saveDraft.Reading.Mode != "cursor" { t.Fatal("reset did not restore tracking", response) } } a.settings.saveDraft.Reading.Mode = "highlight" if response := runtime_request(a, "SETTING SET focus#cursor=False"); response != "" || a.engine.Tracking_mode() != "table" || a.settings.saveDraft.Reading.Mode != "table" { t.Fatal("partial flag update used a stale saved mode", response) } reply := persistence_request(a, "SAVE") finish_persistence(t, a, false) persistence_reply(t, reply, false) saved, _, err := config.Load(a.options.ConfigPath, nil) if err != nil || saved.Reading.Mode != "table" { t.Fatal("remote tracking not saved", saved.Reading.Mode, err) } if response := runtime_request(a, "SETTING SET reading#mode=highlight"); response != "" { t.Fatal(response) } if response := runtime_request(a, "SETTING RESET"); response != "" || a.engine.Tracking_mode() != "table" { t.Fatal("reset lost saved tracking baseline", response) } } } func TestRemoteSpeechRestorationFeedbackAndAdmission(t *testing.T) { for _, terminalMode := range []bool{false, true} { for _, soundMode := range []string{"enabled", "disabled", "missing", "unavailable"} { a, voice := persistence_application(t, terminalMode) sounds := a.cues.(*fakeSounds) a.settings.live.Presentation.Sound.Enabled = soundMode != "disabled" if soundMode == "missing" { sounds.missing = map[presentation.Cue]bool{presentation.SpeechOn: true, presentation.SpeechOff: true} } else if soundMode == "unavailable" { a.cues = nil } voice.messages, sounds.cues = nil, nil for _, text := range []string{"SETTING SET speech#enabled=False", "COMMAND SAY discarded", "SETTING SET speech#enabled=True"} { if response := runtime_request(a, text); response != "" { t.Fatal(response) } } if a.muted || voice.muted || slices.Contains(voice.messages, "discarded") { t.Fatal("remote mute/restoration or discard failed", voice.messages) } if soundMode == "enabled" { if !reflect.DeepEqual(sounds.cues, []presentation.Cue{presentation.SpeechOff, presentation.SpeechOn}) || len(voice.messages) != 0 { t.Fatal("missing or duplicated mute feedback", sounds.cues, voice.messages) } } else if !reflect.DeepEqual(voice.messages, []string{"speech enabled"}) { t.Fatal("missing restoration speech fallback", soundMode, voice.messages) } voice.messages, sounds.cues = nil, nil if response := runtime_request(a, "SETTING SET speech#enabled=True"); response != "" || len(voice.messages)+len(sounds.cues) != 0 { t.Fatal("identical enable repeated feedback", response) } if response := runtime_request(a, "COMMAND TEMPDISABLESPEECH"); response != "" || !a.temporarySilence { t.Fatal("remote keypad action did not silence", response) } voice.messages, sounds.cues = nil, nil before := a.settings.saveDraft reply := make(chan string, 1) a.remote_request(control.Request{UID: os.Geteuid() + 1, Text: "SETTING SET speech#enabled=True", Reply: reply}) if <-reply == "" || !a.temporarySilence { t.Fatal("unauthorized enable restored temporary silence") } a.settings.live.Remote.EnableSettingsRemote = false if response := runtime_request(a, "SETTING SET speech#enabled=True"); response == "" || !a.temporarySilence { t.Fatal("disabled remote enable restored temporary silence", response) } a.settings.live.Remote.EnableSettingsRemote = true for _, text := range []string{"SETTING SET speech#enabled=True;reading#mode=bad", "COMMAND TEMPDISABLESPEECH extra"} { if response := runtime_request(a, text); response == "" || !a.temporarySilence || !reflect.DeepEqual(a.settings.saveDraft, before) || len(voice.messages)+len(sounds.cues) != 0 { t.Fatal("invalid request restored speech or changed state", text, response) } } for _, text := range []string{"COMMAND SAY discarded", "SETTING SET reading#mode=highlight"} { if response := runtime_request(a, text); response != "" || !a.temporarySilence || len(voice.messages) != 0 { t.Fatal("unrelated request cleared temporary silence", text, response) } } if response := runtime_request(a, "SETTING SET speech#enabled=True"); response != "" || a.temporarySilence || a.muted { t.Fatal("explicit enable failed to restore temporary silence", response) } if soundMode == "enabled" { if !reflect.DeepEqual(sounds.cues, []presentation.Cue{presentation.SpeechOn}) || len(voice.messages) != 0 { t.Fatal("temporary restoration feedback", sounds.cues, voice.messages) } } else if !reflect.DeepEqual(voice.messages, []string{"speech enabled"}) { t.Fatal("temporary restoration fallback", soundMode, voice.messages) } } } } func TestRemoteNoArgumentCommandsRejectExtraParameters(t *testing.T) { a, voice := persistence_application(t, true) for _, verb := range []string{"INTERRUPT", "TEMPDISABLESPEECH", "QUITAPPLICATION", "EXPORTCLIPBOARD"} { before := voice.interrupts if response := runtime_request(a, "COMMAND "+verb+" extra"); response == "" || a.quitting || a.temporarySilence || voice.interrupts != before { t.Fatal("malformed command had side effects", verb, response) } } }