// 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") } } }