177 lines
8.1 KiB
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)
|
|
}
|
|
}
|
|
}
|