Files

125 lines
4.3 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"reflect"
"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/presentation"
"git.stormux.org/storm/fenrir/internal/screen"
)
func prompt_snapshot(lines ...string) screen.Snapshot {
result := screen.Snapshot{Terminal: 1, Active: true, Rows: len(lines), Cols: 80, Y: len(lines) - 1}
for _, line := range lines {
cells := make([]rune, result.Cols)
for index := range cells {
cells[index] = ' '
}
copy(cells, []rune(line))
result.Cells = append(result.Cells, cells...)
}
return result
}
func TestPromptRestorationDiscardsBatchAndPreservesModes(t *testing.T) {
for _, mode := range []string{"sound", "speech", "missing sound", "automatic off", "muted", "menu"} {
for _, line := range []string{"$", "Password:", "Worked for 19s • 7:46 PM", "Worked for 3m 35s • 8:05 PM", "CUSTOM READY"} {
t.Run(mode+"/"+line, func(t *testing.T) {
settings := config.Defaults()
settings.Presentation.Sound.Enabled = mode != "speech"
settings.Reading.Automatic = mode != "automatic off"
settings.Prompt.ExactMatches = []string{"CUSTOM READY"}
source := &fakeSource{snapshot: prompt_snapshot("old", "working"), events: make(chan error, 1)}
keys := &fakeKeys{events: make(chan error, 1)}
voice, sounds := &fakeVoice{}, &fakeSounds{}
if mode == "missing sound" {
sounds.missing = map[presentation.Cue]bool{presentation.SpeechOn: true}
}
reads, polls := 0, 0
source.afterRead = func() {
reads++
if reads > 1 {
keys.events <- nil
}
}
keys.poll = func(_ bool, handle func(input.Decision)) error {
polls++
switch polls {
case 1:
handle(input.Decision{Command: command.TempDisableSpeech, Interrupt: true, Keypress: true})
if mode == "muted" {
handle(input.Decision{Command: command.Mute})
}
if mode == "menu" {
// Menu loading is asynchronous. Start the prompt trial
// only after its opening announcement has been delivered.
voice.onSay = func(text string) {
if !strings.HasPrefix(text, "Fenrir Menu.") {
return
}
voice.onSay = nil
voice.messages, sounds.cues = nil, nil
source.snapshot = prompt_snapshot("discard this backlog", "Worked for 19s • 7:4")
source.events <- nil
}
handle(input.Decision{Command: command.FenrirMenu})
return nil
}
voice.messages, sounds.cues = nil, nil
source.snapshot = prompt_snapshot("discard this backlog", "Worked for 19s • 7:4")
source.events <- nil
case 2:
if len(voice.messages) != 0 || len(sounds.cues) != 0 {
t.Fatal("incomplete line restored speech", voice.messages, sounds.cues)
}
source.snapshot = prompt_snapshot("also discard this batch", line)
source.events <- nil
case 3:
var wantSpeech []string
var wantCues []presentation.Cue
if mode != "muted" && mode != "menu" {
if mode == "speech" || mode == "missing sound" {
wantSpeech = append(wantSpeech, "Speech restored")
} else {
wantCues = []presentation.Cue{presentation.SpeechOn}
}
if mode != "automatic off" {
wantSpeech = append(wantSpeech, line)
}
}
if !reflect.DeepEqual(voice.messages, wantSpeech) || !reflect.DeepEqual(sounds.cues, wantCues) {
t.Fatal(voice.messages, sounds.cues, "want", wantSpeech, wantCues)
}
voice.messages, sounds.cues = nil, nil
source.events <- nil // unchanged trigger must not repeat
case 4:
if len(voice.messages) != 0 || len(sounds.cues) != 0 {
t.Fatal("repeated restoration", voice.messages, sounds.cues)
}
if mode == "menu" {
handle(input.Decision{UI: &interaction.Key{Name: "escape"}})
}
handle(input.Decision{Command: command.Quit})
}
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := run(ctx, source, keys, voice, nil, RunOptions{Settings: settings, Sounds: sounds}); err != nil || polls != 4 {
t.Fatal(err, polls)
}
})
}
}
}