599 lines
23 KiB
Go
599 lines
23 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
||
package app
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"reflect"
|
||
"slices"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.stormux.org/storm/fenrir/internal/command"
|
||
"git.stormux.org/storm/fenrir/internal/config"
|
||
"git.stormux.org/storm/fenrir/internal/input"
|
||
"git.stormux.org/storm/fenrir/internal/interaction"
|
||
"git.stormux.org/storm/fenrir/internal/screen"
|
||
"git.stormux.org/storm/fenrir/internal/speech"
|
||
)
|
||
|
||
func override_screen(terminal int, kind string) screen.Snapshot {
|
||
s := screen.Snapshot{Terminal: terminal, Active: true, Rows: 24, Cols: 100, Y: 23, Cells: make([]rune, 2400), Attributes: make([]screen.Attribute, 2400)}
|
||
for index := range s.Cells {
|
||
s.Cells[index] = ' '
|
||
}
|
||
switch kind {
|
||
case "ninjam":
|
||
copy(s.Line(0), []rune("LOCAL"))
|
||
copy(s.Line(1), []rune(" master: [ ]mute [0.0dB center] | metronome: [ ]mute [0.0dB center]"))
|
||
copy(s.Line(16), []rune("CHAT"))
|
||
copy(s.Line(23), []rune("[QUIT NINJAM] : fixture.invalid : 120.0BPM 16BPI"))
|
||
s.Attributes[111] = screen.Attribute{Foreground: screen.ColorIndexed, Background: screen.ColorIndexed | 7}
|
||
case "networkmanager":
|
||
copy(s.Line(2), []rune("┌──────── NetworkManager TUI ─────────┐"))
|
||
case "rogue":
|
||
copy(s.Line(5), []rune("|..@...+"))
|
||
copy(s.Line(23), []rune("Level: 1 Gold: 0 Hp: 12(12) Str: 16(16) Arm: 4 Exp: 1/0"))
|
||
default:
|
||
copy(s.Line(23), []rune(kind))
|
||
}
|
||
return s
|
||
}
|
||
|
||
func TestRogueMenuSelectionNamedReadProfileAndShellExit(t *testing.T) {
|
||
a, voice := persistence_application(t, true)
|
||
override_read(t, a, override_screen(1, "rogue"))
|
||
a.open_applications()
|
||
a.ui.handle(interaction.Key{Text: "r"})
|
||
if got := voice.messages[len(voice.messages)-1]; got != "Rogue" {
|
||
t.Fatal(got)
|
||
}
|
||
a.ui.handle(interaction.Key{Name: "enter"})
|
||
override_read(t, a, override_screen(1, "rogue"))
|
||
if a.overrideCurrent == nil || a.overrideCurrent.id != "rogue" {
|
||
t.Fatal("Rogue not activated")
|
||
}
|
||
a.handle_command(command.ReadLiveRegion1)
|
||
if got := voice.messages[len(voice.messages)-1]; got != "Health: 12 of 12" {
|
||
t.Fatal(got)
|
||
}
|
||
base := a.overrideCurrent.underlying.settings.live.Presentation.Speech.Rate
|
||
a.handle_command(command.NextQuickValue)
|
||
rate := a.settings.live.Presentation.Speech.Rate
|
||
if rate == base {
|
||
t.Fatal("application rate unchanged")
|
||
}
|
||
a.save_settings()
|
||
finish_persistence(t, a, false)
|
||
path := filepath.Join(filepath.Dir(a.options.ConfigPath), "applications", "rogue.toml")
|
||
doc, err := config.Open_application_document(path, a.overrideCurrent.base)
|
||
if err != nil || doc.Settings.Presentation.Speech.Rate != rate {
|
||
t.Fatal("Rogue profile not saved", err)
|
||
}
|
||
override_read(t, a, override_screen(1, "[Username@host directory] $"))
|
||
if a.overrideCurrent != nil || a.overrides[1] != nil || a.settings.live.Presentation.Speech.Rate != base {
|
||
t.Fatal("shell did not restore default settings")
|
||
}
|
||
a.select_override("rogue")
|
||
override_read(t, a, override_screen(1, "rogue"))
|
||
if a.overrideCurrent == nil || a.settings.live.Presentation.Speech.Rate != rate {
|
||
t.Fatal("Rogue profile not reloaded")
|
||
}
|
||
}
|
||
|
||
func TestOverrideRestoresSynthesizerAndKeepsSaveBlockedUntilCompletion(t *testing.T) {
|
||
for _, failed := range []bool{false, true} {
|
||
a, voice, _ := selection_application(t, true)
|
||
a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return &preparedVoice{}, nil }
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
a.select_override("ninjam")
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
baseline := a.settings.saveDraft
|
||
if response := request_selection(t, a, "SET speech#voice=Temporary;speech#module=fixture"); response != "" {
|
||
t.Fatal(response)
|
||
}
|
||
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") }
|
||
}
|
||
a.select_override("default")
|
||
if !a.speechPending || !a.saves_unavailable() {
|
||
t.Fatal("restoration did not retain save guard")
|
||
}
|
||
a.handle_command(command.NextQuickValue)
|
||
if voice.messages[len(voice.messages)-1] != "Settings are busy" {
|
||
t.Fatal("settings changed during restoration")
|
||
}
|
||
finish_selection(t, a)
|
||
if failed {
|
||
if !a.saves_unavailable() || !a.overrideRestoreFailed {
|
||
t.Fatal("failed restoration released save guard")
|
||
}
|
||
override_read(t, a, override_screen(1, "shell output remains readable"))
|
||
if a.overrideCurrent != nil || !strings.Contains(voice.messages[len(voice.messages)-1], "shell output remains readable") {
|
||
t.Fatal("failed speech restoration suppressed ordinary reading", voice.messages)
|
||
}
|
||
a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return &preparedVoice{}, nil }
|
||
a.select_override("default")
|
||
finish_selection(t, a)
|
||
}
|
||
if a.saves_unavailable() || !same_speech_selection(a.settings.live.Presentation.Speech, baseline.Presentation.Speech) {
|
||
t.Fatal("speech selection not restored")
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestExitWaitsForPendingSpeechChangeAndRestoresBeforeShellOutput(t *testing.T) {
|
||
a, _, _ := selection_application(t, true)
|
||
a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return &preparedVoice{}, nil }
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
a.select_override("ninjam")
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
if response := request_selection(t, a, "SET speech#voice=Temporary;speech#module=fixture"); response != "" {
|
||
t.Fatal(response)
|
||
}
|
||
override_read(t, a, override_screen(1, "[Username@host directory] $"))
|
||
if !a.speechPending || a.overrides[1] == nil {
|
||
t.Fatal("exit did not wait for restoration")
|
||
}
|
||
finish_selection(t, a)
|
||
override_read(t, a, override_screen(1, "[Username@host directory] $"))
|
||
if a.overrides[1] != nil || a.overrideCurrent != nil || a.engine.Tracking_mode() != "cursor" {
|
||
t.Fatal("async exit missed")
|
||
}
|
||
}
|
||
|
||
func TestCodexShellExitWithoutBridgeClosure(t *testing.T) {
|
||
for _, pendingSpeech := range []bool{false, true} {
|
||
t.Run(fmt.Sprint(pendingSpeech), func(t *testing.T) {
|
||
a, voice, _ := selection_application(t, true)
|
||
a.prepareSpeech = func(config.Speech) (speech.Transport, error) { return &preparedVoice{}, nil }
|
||
composer := override_screen(1, "› hello")
|
||
composer.X = 7
|
||
override_read(t, a, composer)
|
||
a.select_override("codex")
|
||
override_read(t, a, composer)
|
||
baseline := a.overrideCurrent.underlying.settings.live
|
||
if pendingSpeech {
|
||
if response := request_selection(t, a, "SET speech#voice=Temporary;speech#module=fixture"); response != "" {
|
||
t.Fatal(response)
|
||
}
|
||
}
|
||
shell := override_screen(1, "[Username@host directory] $")
|
||
copy(shell.Line(20), []rune("› previous composer"))
|
||
shell.X = len([]rune(shell.Text(23))) + 1
|
||
override_read(t, a, shell)
|
||
if pendingSpeech {
|
||
finish_selection(t, a)
|
||
override_read(t, a, shell)
|
||
}
|
||
if a.overrideCurrent != nil || a.overrides[1] != nil || !reflect.DeepEqual(a.settings.live, baseline) {
|
||
t.Fatal("shell retained Codex settings without a bridge close event")
|
||
}
|
||
next := shell
|
||
next.X--
|
||
start := len(voice.messages)
|
||
a.engine.Note_input(true, false, false)
|
||
override_read(t, a, next)
|
||
if len(voice.messages) == start {
|
||
t.Fatal("shell arrow navigation remained silent")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func override_read(t *testing.T, a *application, snapshot screen.Snapshot) {
|
||
t.Helper()
|
||
a.source.(*fakeSource).snapshot = 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) {
|
||
a, voice := persistence_application(t, true)
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
a.open_applications()
|
||
if !a.ui.active || !strings.Contains(voice.messages[len(voice.messages)-1], "Default") {
|
||
t.Fatal(voice.messages)
|
||
}
|
||
for _, step := range []struct{ key, want string }{
|
||
{"down", "NetworkManager"},
|
||
{"down", "Ninjam"},
|
||
{"down", "Rogue"},
|
||
{"down", "Codex"},
|
||
{"down", "Default"},
|
||
{"up", "Codex"},
|
||
} {
|
||
a.ui.handle(interaction.Key{Name: step.key})
|
||
if got := voice.messages[len(voice.messages)-1]; got != step.want {
|
||
t.Fatalf("%s announcement = %q, want %q", step.key, got, step.want)
|
||
}
|
||
}
|
||
if len(a.overrides) != 0 {
|
||
t.Fatal("navigation activated override")
|
||
}
|
||
a.ui.handle(interaction.Key{Name: "escape"})
|
||
if len(a.overrides) != 0 || a.ui.active {
|
||
t.Fatal("cancel activated override")
|
||
}
|
||
a.open_applications()
|
||
a.ui.handle(interaction.Key{Name: "down"})
|
||
a.ui.handle(interaction.Key{Name: "down"})
|
||
a.ui.handle(interaction.Key{Name: "enter"})
|
||
if a.ui.active || a.overrides[1] == nil || a.overrides[1].id != "ninjam" {
|
||
t.Fatal("activation failed")
|
||
}
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
if a.engine.Tracking_mode() != "highlight" {
|
||
t.Fatal("settings not applied")
|
||
}
|
||
a.open_applications()
|
||
if a.ui.session.Values()["applications.application"].Selected != 2 {
|
||
t.Fatal("active item not selected")
|
||
}
|
||
a.ui.handle(interaction.Key{Name: "tab"})
|
||
a.ui.handle(interaction.Key{Name: "tab"}) // Skip Copy alias to reach Cancel.
|
||
a.ui.handle(interaction.Key{Name: "enter"})
|
||
if a.ui.active || a.overrides[1] == nil {
|
||
t.Fatal("Cancel button changed selection")
|
||
}
|
||
a.open_applications()
|
||
a.ui.handle(interaction.Key{Name: "down"}) // Rogue
|
||
a.ui.handle(interaction.Key{Name: "down"}) // Codex
|
||
a.ui.handle(interaction.Key{Name: "down"}) // list wraps to Default
|
||
a.ui.handle(interaction.Key{Name: "enter"})
|
||
if a.saves_unavailable() || a.engine.Tracking_mode() != "cursor" {
|
||
t.Fatal("default failed")
|
||
}
|
||
}
|
||
|
||
func TestApplicationsMenuPrefixSearch(t *testing.T) {
|
||
a, voice := persistence_application(t, true)
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
a.open_applications()
|
||
for _, step := range []struct{ text, want string }{{"n", "NetworkManager"}, {"i", "Ninjam"}} {
|
||
a.ui.handle(interaction.Key{Text: step.text})
|
||
if got := voice.messages[len(voice.messages)-1]; got != step.want {
|
||
t.Fatalf("typed %q: got %q, want %q", step.text, got, step.want)
|
||
}
|
||
if len(a.overrides) != 0 || !a.ui.active {
|
||
t.Fatal("search activated an override or closed the menu")
|
||
}
|
||
}
|
||
a.ui.handle(interaction.Key{Name: "enter"})
|
||
if a.ui.active || a.overrides[1] == nil || a.overrides[1].id != "ninjam" {
|
||
t.Fatal("Enter did not activate the search result")
|
||
}
|
||
}
|
||
|
||
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)
|
||
a.handle_command(command.ToggleAttributes)
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
baseSettings, baseQuick, baseRuntime := a.settings, a.quick.revision, a.engine.Capture_session()
|
||
before, err := os.ReadFile(a.options.ConfigPath)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
a.select_override("ninjam")
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
for _, id := range []command.ID{command.NextQuickValue, command.ProgressMonitor, command.ToggleTracking, command.AutoRead, command.ToggleAttributes} {
|
||
a.handle_command(id)
|
||
}
|
||
if response := a.apply_remote_settings("SET speech#rate=0.77;keyboard#word_echo=True"); response != "" {
|
||
t.Fatal(response)
|
||
}
|
||
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()
|
||
finish_persistence(t, a, false)
|
||
if !strings.Contains(voice.messages[len(voice.messages)-1], "Ninjam application settings saved") {
|
||
t.Fatal(voice.messages)
|
||
}
|
||
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
|
||
}
|
||
a.ui.handle(interaction.Key{Name: "tab"})
|
||
}
|
||
a.ui.handle(interaction.Key{Name: "enter"})
|
||
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) {
|
||
t.Fatal("pre-override session was not restored")
|
||
}
|
||
after, _ := os.ReadFile(a.options.ConfigPath)
|
||
if !slices.Equal(before, after) {
|
||
t.Fatal("configuration changed")
|
||
}
|
||
a.save_settings()
|
||
finish_persistence(t, a, false)
|
||
saved, _, err := config.Load(a.options.ConfigPath, nil)
|
||
if err != nil || saved.Presentation.Speech.Rate != baseSettings.live.Presentation.Speech.Rate || saved.Keyboard.WordEcho {
|
||
t.Fatal("override leaked after clearing", saved, err)
|
||
}
|
||
}
|
||
|
||
func TestOverridesSuspendPerTerminalAndClearOnlyOnFreshShell(t *testing.T) {
|
||
a, _ := persistence_application(t, false)
|
||
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)
|
||
appRate := a.settings.live.Presentation.Speech.Rate
|
||
override_read(t, a, override_screen(2, "other terminal"))
|
||
if a.overrideCurrent != nil || a.engine.Tracking_mode() != "cursor" || a.settings.live.Presentation.Speech.Rate == appRate {
|
||
t.Fatal("override leaked across VT")
|
||
}
|
||
override_read(t, a, override_screen(2, "networkmanager"))
|
||
a.select_override("networkmanager")
|
||
override_read(t, a, override_screen(2, "networkmanager"))
|
||
if a.overrideCurrent == nil || a.overrideCurrent.id != "networkmanager" {
|
||
t.Fatal("second terminal activation")
|
||
}
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
if a.overrideCurrent == nil || a.overrideCurrent.id != "ninjam" || a.settings.live.Presentation.Speech.Rate != appRate {
|
||
t.Fatal("resume lost temporary state")
|
||
}
|
||
for _, text := range []string{"Password:", "Confirm? [y/n]", "unrecognized screen", "<Username> $"} {
|
||
override_read(t, a, override_screen(1, text))
|
||
if a.overrides[1] == nil || a.overrideCurrent != nil || a.engine.Tracking_mode() != "cursor" {
|
||
t.Fatal("bad suspension", text)
|
||
}
|
||
}
|
||
s := override_screen(1, "[Username@host directory] $")
|
||
s.Generation = 2 // Returning from an alternate buffer must still detect exit.
|
||
override_read(t, a, s)
|
||
if a.overrides[1] != nil || a.overrides[2] == nil {
|
||
t.Fatal("exit affected wrong terminal")
|
||
}
|
||
a.select_override("default")
|
||
if a.overrides[2] == nil {
|
||
t.Fatal("Default cleared another terminal")
|
||
}
|
||
}
|
||
|
||
func TestOverrideRestoresUserRegionsAndPreservesRawReview(t *testing.T) {
|
||
a, voice := persistence_application(t, true)
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
a.engine.Command(command.CurrentLine)
|
||
a.engine.Set_mark()
|
||
a.engine.Command(command.SetLiveRegion)
|
||
before := a.engine.Command(command.ReadLiveRegion1).Text
|
||
a.select_override("ninjam")
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
a.handle_command(command.ReadLiveRegion1)
|
||
if voice.messages[len(voice.messages)-1] != "No chat message received" {
|
||
t.Fatal(voice.messages)
|
||
}
|
||
a.handle_command(command.ReadScreen)
|
||
if !strings.Contains(voice.messages[len(voice.messages)-1], "[ ]mute") {
|
||
t.Fatal("raw screen replaced", voice.messages)
|
||
}
|
||
a.select_override("default")
|
||
if after := a.engine.Command(command.ReadLiveRegion1).Text; after != before {
|
||
t.Fatal("user region lost", before, after)
|
||
}
|
||
}
|
||
|
||
func TestApplicationExitInvalidatesSettingsFormAndItsPreviews(t *testing.T) {
|
||
a, _ := persistence_application(t, true)
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
baseline := a.settings
|
||
a.select_override("ninjam")
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
a.open_settings()
|
||
receive_ui(t, a.ui)
|
||
generation := a.ui.generation
|
||
reading := a.settings.live.Reading
|
||
reading.Spellcheck = !reading.Spellcheck
|
||
a.preview_spelling(reading)
|
||
override_read(t, a, override_screen(1, "[Username@host directory] $"))
|
||
if a.ui.active || !reflect.DeepEqual(a.settings, baseline) {
|
||
t.Fatal("old form or preview survived exit")
|
||
}
|
||
a.ui.update(uiUpdate{generation: generation, apply: func() { t.Fatal("stale form callback applied") }})
|
||
a.save_settings()
|
||
finish_persistence(t, a, false)
|
||
saved, _, err := config.Load(a.options.ConfigPath, nil)
|
||
if err != nil || saved.Reading.Mode != baseline.saveDraft.Reading.Mode || saved.Reading.Spellcheck != baseline.saveDraft.Reading.Spellcheck {
|
||
t.Fatal("old application form contaminated subsequent save", err)
|
||
}
|
||
}
|
||
|
||
func TestActivationRejectsPendingOperationsAndRestartStartsWithoutOverride(t *testing.T) {
|
||
a, _ := persistence_application(t, true)
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
a.saver.busy = true
|
||
a.select_override("ninjam")
|
||
if len(a.overrides) != 0 {
|
||
t.Fatal("activated during save")
|
||
}
|
||
a.saver.busy, a.speechPending = false, true
|
||
a.select_override("ninjam")
|
||
if len(a.overrides) != 0 {
|
||
t.Fatal("activated during speech selection")
|
||
}
|
||
a.speechPending = false
|
||
a.select_override("ninjam")
|
||
override_read(t, a, override_screen(1, "ninjam"))
|
||
saved, _, err := config.Load(a.options.ConfigPath, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fresh, err := new_application(a.ctx, &fakeSource{}, &fakeKeys{poll: func(bool, func(input.Decision)) error { return nil }}, &fakeVoice{}, RunOptions{Settings: saved})
|
||
if err != nil || fresh.saves_unavailable() || fresh.engine.Tracking_mode() != "cursor" {
|
||
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")
|
||
}
|
||
}
|