85 lines
2.9 KiB
Go
85 lines
2.9 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
)
|
|
|
|
type keyEchoCompletionSpeaker struct{ controlledSpeaker }
|
|
|
|
func (speaker *keyEchoCompletionSpeaker) SpeakEcho(ctx context.Context, text string) error {
|
|
return speaker.call(ctx, "queued:echo:"+text)
|
|
}
|
|
|
|
func (speaker *keyEchoCompletionSpeaker) SpeakVoiceAndWait(ctx context.Context, text, voice string, _ func()) error {
|
|
return speaker.call(ctx, "completed:"+voice+":"+text)
|
|
}
|
|
|
|
func TestSpeechWorkerKeyEchoCompletesBeforeCaretSpeech(t *testing.T) {
|
|
for _, custom := range []bool{false, true} {
|
|
t.Run(fmt.Sprint(custom), func(t *testing.T) {
|
|
speaker := &keyEchoCompletionSpeaker{controlledSpeaker{calls: make(chan speechCall, 8)}}
|
|
worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
|
if err := worker.Start(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { worker.Stop(context.Background()) })
|
|
worker.Enqueue(speechRequest{Text: "right", Role: "key", Cause: "key-echo", CustomEcho: custom})
|
|
voice := ""
|
|
if custom {
|
|
voice = "echo"
|
|
}
|
|
echo := expectSpeechCall(t, &speaker.controlledSpeaker, "completed:"+voice+":right")
|
|
worker.Enqueue(speechRequest{Text: "h", Role: "entry", Cause: "object:text-caret-moved", Character: true})
|
|
select {
|
|
case call := <-speaker.calls:
|
|
t.Fatalf("caret output overtook key echo completion: %s", call.text)
|
|
default:
|
|
}
|
|
close(echo.done)
|
|
close(expectSpeechCall(t, &speaker.controlledSpeaker, "h").done)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSpeechWorkerKeyEchoWaitStillAllowsInputInterruption(t *testing.T) {
|
|
speaker := &keyEchoCompletionSpeaker{controlledSpeaker{calls: make(chan speechCall, 8)}}
|
|
worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
|
if err := worker.Start(t.Context()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { worker.Stop(context.Background()) })
|
|
worker.Enqueue(speechRequest{Text: "right", Role: "key", Cause: "key-echo", CustomEcho: true})
|
|
echo := expectSpeechCall(t, &speaker.controlledSpeaker, "completed:echo:right")
|
|
worker.Enqueue(speechRequest{Text: "stale character", Cause: "object:text-caret-moved"})
|
|
if !worker.InterruptForInput(input.Event{Pressed: true, Name: "Left", Timestamp: 100}) {
|
|
t.Fatal("new input did not interrupt key echo")
|
|
}
|
|
close(expectSpeechCall(t, &speaker.controlledSpeaker, "cancel").done)
|
|
close(echo.done) // Backend reports cancellation of the old echo.
|
|
worker.Enqueue(speechRequest{Text: "left", Role: "key", Cause: "key-echo", CustomEcho: true})
|
|
for {
|
|
select {
|
|
case call := <-speaker.calls:
|
|
close(call.done)
|
|
if call.text == "completed:echo:left" {
|
|
return
|
|
}
|
|
if call.text != "cancel" {
|
|
t.Fatalf("stale speech survived new input: %s", call.text)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("new key echo did not resume after interruption")
|
|
}
|
|
}
|
|
}
|