package app import ( "context" "log/slog" "testing" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" ) func TestProcessInputEventsUsesLiveInterruptionSettings(t *testing.T) { live := &liveSettings{} settings := config.DefaultSettings() settings.EnableKeyEcho = true settings.EnableActionKeys = true for _, mode := range []string{input.InterruptAllKeys, input.InterruptCommonKeys, input.InterruptCustomKeys} { settings.SpeechInterruption = input.InterruptionSettings{Mode: mode, Keys: []string{"Tab"}} live.Store(settings) speech := &fakeSpeechQueue{spoken: make(chan string, 10), interrupt: make(chan struct{}, 10)} listener := &fakeInputListener{events: make(chan input.Event, 8)} for _, name := range []string{"a", "Tab", "Control_L", "Return"} { listener.events <- input.NewKeyEvent(42, true, name) listener.events <- input.NewKeyEvent(42, false, name) } close(listener.events) processInputEvents(context.Background(), slog.New(slog.DiscardHandler), speech, listener, input.EchoSettings{}, live, nil) want := map[string]int{input.InterruptAllKeys: 4, input.InterruptCommonKeys: 2, input.InterruptCustomKeys: 1}[mode] if len(speech.interrupt) != want { t.Fatalf("mode %s interrupted %d times, want %d", mode, len(speech.interrupt), want) } // Selecting interruption keys must not suppress ordinary key echo. if len(speech.spoken) < 2 { t.Fatalf("mode %s suppressed Tab/Enter echo", mode) } } } func TestAppliedInterruptionKeysPreserveQueuedSpeechAndSayAll(t *testing.T) { logger := slog.New(slog.DiscardHandler) speech := newSpeechWorker(logger, nil) speech.running = true live := &liveSettings{} settings := config.DefaultSettings() settings.EnableKeyEcho = false live.Store(settings) preferences := &preferencesWorker{settings: settings, live: live, speechWorker: speech} settings.SpeechInterruption.Mode = input.InterruptCommonKeys if err := preferences.applyRuntime(context.Background(), settings); err != nil { t.Fatal(err) } deliver := func(name string) { listener := &fakeInputListener{events: make(chan input.Event, 1)} listener.events <- input.NewKeyEvent(42, true, name) close(listener.events) processInputEvents(context.Background(), logger, speech, listener, input.EchoSettings{}, live, nil) } readContext := speech.BeginReadAll(context.Background(), 0) speech.Enqueue(speechRequest{Text: "First announcement"}) speech.Enqueue(speechRequest{Text: "Second announcement"}) generation := speech.generation deliver("Up") deliver("a") if readContext.Err() != nil || len(speech.pending) != 2 || speech.generation != generation { t.Fatal("unselected keys cancelled Say All or dropped queued speech") } deliver("Control_R") if readContext.Err() != context.Canceled || len(speech.pending) != 0 { t.Fatal("common interruption key did not cancel Say All and pending output") } settings.SpeechInterruption = input.InterruptionSettings{Mode: input.InterruptCustomKeys, Keys: []string{"F8"}} if err := preferences.applyRuntime(context.Background(), settings); err != nil { t.Fatal(err) } speech.Enqueue(speechRequest{Text: "Custom mode announcement"}) deliver("Control_R") if len(speech.pending) != 1 { t.Fatal("previous interruption mode remained active after Apply") } deliver("F8") if len(speech.pending) != 0 { t.Fatal("custom interruption key did not cancel queued output") } speech.Enqueue(speechRequest{Text: "Old output"}) speech.ProtectCommand(100) if len(speech.pending) != 0 { t.Fatal("reader command failed to cancel old output in custom mode") } }