package app import ( "bytes" "context" "encoding/json" "log/slog" "testing" "time" "git.stormux.org/storm/cthulhu/internal/clipboard" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" ) func TestAutomaticClipboardQueuesTextAndNormalInputCancels(t *testing.T) { live := &liveSettings{} settings := config.DefaultSettings() if settings.Reading.AutomaticallyReadClipboardChanges { t.Fatal("clipboard reading defaults on") } settings.Reading.AutomaticallyReadClipboardChanges = true live.Store(settings) worker := newSpeechWorker(slog.New(slog.DiscardHandler), nil) worker.running = true // Inspect the real FIFO without a delivery goroutine. changes := make(chan clipboard.TextChange, 6) changes <- clipboard.TextChange{Text: "First", Epoch: 1, Timestamp: 10} changes <- clipboard.TextChange{Text: "Second", Epoch: 1, Timestamp: 20} changes <- clipboard.TextChange{Text: "", Epoch: 1, Timestamp: 30} changes <- clipboard.TextChange{Text: "Third", Epoch: 1, Timestamp: 40} close(changes) var output bytes.Buffer serve_clipboard_changes(context.Background(), live, nil, worker, json.NewEncoder(&output), changes, nil) if len(worker.pending) != 3 { t.Fatalf("clipboard queue = %+v", worker.pending) } for i, want := range []string{"First", "Second", "Third"} { if worker.pending[i].Text != want || !worker.pending[i].Append { t.Fatalf("clipboard request %d = %+v", i, worker.pending[i]) } } if !worker.InterruptForInput(input.Event{Pressed: true, Name: "Control_L", Timestamp: 50}) || len(worker.pending) != 0 { t.Fatal("normal input did not clear clipboard queue") } worker.Enqueue(speechRequest{Text: "Late old clipboard", Cause: "clipboard", Append: true, ClipboardTimestamp: 40}) if len(worker.pending) != 0 { t.Fatal("old asynchronous clipboard reply replayed after interruption") } worker.Enqueue(speechRequest{Text: "New clipboard", Cause: "clipboard", Append: true, ClipboardTimestamp: 60}) if len(worker.pending) != 1 { t.Fatal("new clipboard change suppressed after interruption") } worker.Enqueue(speechRequest{Text: "Same millisecond copy", Cause: "clipboard", Append: true, ClipboardTimestamp: 50}) if len(worker.pending) != 2 { t.Fatal("copy result in same server millisecond as input was suppressed") } } func TestAutomaticClipboardRejectsDisabledAndStaleApplications(t *testing.T) { for _, tc := range []struct { name string enabled, pending bool }{ {"disabled", false, false}, {"switching", true, true}, {"stale epoch", true, false}, } { t.Run(tc.name, func(t *testing.T) { live := &liveSettings{} settings := config.DefaultSettings() settings.Reading.AutomaticallyReadClipboardChanges = tc.enabled live.Store(settings) if tc.pending { name := "Other Application" live.requestedApplication.Store(&name) } changes := make(chan clipboard.TextChange, 1) changes <- clipboard.TextChange{Text: "Do not speak", Epoch: 0} close(changes) speech := &fakeSpeechQueue{spoken: make(chan string, 1), interrupt: make(chan struct{}, 1)} var output bytes.Buffer serve_clipboard_changes(context.Background(), live, nil, speech, json.NewEncoder(&output), changes, nil) if len(speech.spoken) != 0 || len(speech.interrupt) != 0 { t.Fatal("disabled/stale clipboard was spoken") } }) } } func TestAutomaticClipboardDisabledStartsNoHelperAndStops(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) live := &liveSettings{clipboardChanges: make(chan struct{}, 1)} live.Store(config.DefaultSettings()) done := run_automatic_clipboard(ctx, slog.New(slog.DiscardHandler), live, nil, discardSpeech{}) cancel() select { case <-done: case <-time.After(time.Second): t.Fatal("disabled clipboard worker did not stop") } }