Files
fenrir/internal/app/application_overrides_test.go

599 lines
23 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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")
}
}