// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "testing" "time" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/ocr" ) func TestCopyFeedbackRequiresClipboardChangeAndNewCopyKey(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() changes := make(chan struct{}) tracker := &input.IntentTracker{} speech := &fakeSpeechQueue{spoken: make(chan string, 4), interrupt: make(chan struct{}, 4)} done := runCopyFeedback(ctx, changes, tracker, speech, nil, nil) checkSilent := func() { t.Helper() select { case got := <-speech.spoken: t.Fatalf("unexpected speech %q", got) case <-time.After(30 * time.Millisecond): } } change := func() { changes <- struct{}{} } change() checkSilent() tracker.Observe(input.NewKeyEvent(0, true, "Control_L")) tracker.Observe(input.NewKeyEvent(0, true, "c")) checkSilent() change() select { case got := <-speech.spoken: if got != "copied to clipboard" { t.Fatalf("copy speech = %q", got) } case <-time.After(time.Second): t.Fatal("copy was not announced") } change() checkSilent() tracker.Observe(input.NewKeyEvent(0, true, "c")) change() select { case got := <-speech.spoken: if got != "copied to clipboard" { t.Fatalf("second copy speech = %q", got) } case <-time.After(time.Second): t.Fatal("second copy was not announced") } tracker.Observe(input.NewKeyEvent(0, true, "v")) change() checkSilent() cancel() select { case <-done: case <-time.After(time.Second): t.Fatal("copy worker did not stop") } } func TestCopyFeedbackDoesNotRepeatOCRCopy(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() changes := make(chan struct{}) tracker := &input.IntentTracker{} speech := &fakeSpeechQueue{spoken: make(chan string, 2), interrupt: make(chan struct{}, 2)} session := &ocrSession{cursor: ocr.NewCursor(ocr.Result{Text: "OCR"}), cancel: func() {}} done := runCopyFeedback(ctx, changes, tracker, speech, nil, session) tracker.Observe(input.NewKeyEvent(0, true, "Control_L")) tracker.Observe(input.NewKeyEvent(0, true, "c")) changes <- struct{}{} select { case got := <-speech.spoken: t.Fatalf("duplicate OCR copy feedback %q", got) case <-time.After(30 * time.Millisecond): } session.mu.Lock() session.cursor, session.cancel = nil, nil session.mu.Unlock() changes <- struct{}{} select { case got := <-speech.spoken: t.Fatalf("late duplicate OCR copy feedback %q", got) case <-time.After(30 * time.Millisecond): } tracker.Observe(input.NewKeyEvent(0, true, "c")) changes <- struct{}{} select { case got := <-speech.spoken: if got != "copied to clipboard" { t.Fatal(got) } case <-time.After(time.Second): t.Fatal("ordinary copy feedback was suppressed") } cancel() select { case <-done: case <-time.After(time.Second): t.Fatal("copy worker did not stop") } }