Files
fenrir/internal/app/remote_runtime_test.go

177 lines
8.1 KiB
Go

// 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)
}
}
}