// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "errors" "io" "sync" "syscall" "testing" "time" "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/input" "git.stormux.org/storm/fenrir/internal/screen" "git.stormux.org/storm/fenrir/internal/speech" ) type unavailableSpeech struct { entered chan struct{} once sync.Once fatal bool } func (f *unavailableSpeech) Say(string) error { return io.EOF } func (f *unavailableSpeech) Configure(speech.Parameters) error { return nil } func (f *unavailableSpeech) Set_pitch(int) error { return nil } func (f *unavailableSpeech) Cancel() error { return nil } func (f *unavailableSpeech) Close() {} func (f *unavailableSpeech) Reopen(ctx context.Context) (speech.Transport, error) { f.once.Do(func() { close(f.entered) }) if f.fatal { return nil, syscall.EACCES } <-ctx.Done() return nil, ctx.Err() } func TestAudioRecoveryKeepsKeyboardCommandsAndReleasesOnFailure(t *testing.T) { for _, fatal := range []bool{false, true} { broken := &unavailableSpeech{entered: make(chan struct{}), fatal: fatal} voice := speech.New(broken) keys := &fakeKeys{events: make(chan error, 1)} polls := 0 keys.poll = func(_ bool, handle func(input.Decision)) error { polls++ if polls > 1 { handle(input.Decision{Forward: true, Interrupt: true, Keypress: true}) handle(input.Decision{Command: command.Quit}) } return nil } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) result := make(chan error, 1) go func() { result <- run(ctx, &fakeSource{snapshot: screen.Snapshot{Terminal: 1, Active: true, Rows: 1, Cols: 5, Cells: []rune("ready")}}, keys, voice, nil) }() select { case <-broken.entered: if !fatal { keys.events <- nil } case <-ctx.Done(): t.Fatal("fixture did not begin speech recovery") } select { case err := <-result: if fatal && !errors.Is(err, syscall.EACCES) || !fatal && (err != nil || polls < 2) || !keys.closed { t.Fatal("audio failure trapped input", err, polls, keys.closed) } case <-ctx.Done(): t.Fatal("reader could not quit or release keyboards during audio recovery") } cancel() voice.Close() } }