// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package speech import ( "context" "reflect" "testing" "time" ) func TestSpeechPreludeFinishesBeforeSpeechAndPreservesFIFO(t *testing.T) { fake := new_recording_transport() service := New(fake) defer service.Close() release := make(chan struct{}) service.Say_after("notification", func(ctx context.Context) { fake.record("sound:start") select { case <-release: case <-ctx.Done(): } fake.record("sound:drained") }) service.Say("following text") wait_event(t, fake.changed, "sound:start") fake.mu.Lock() events := append([]string(nil), fake.events...) fake.mu.Unlock() if !reflect.DeepEqual(events, []string{"sound:start"}) { t.Fatal(events) } close(release) wait_event(t, fake.changed, "sent:following text") fake.mu.Lock() defer fake.mu.Unlock() want := []string{"sound:start", "sound:drained", "say:notification", "sent:notification", "say:following text", "sent:following text"} if !reflect.DeepEqual(fake.events, want) { t.Fatal(fake.events) } } func TestSpeechPreludeInterruptMuteAndCloseDiscardDelayedSpeech(t *testing.T) { for _, action := range []string{"interrupt", "mute", "close"} { t.Run(action, func(t *testing.T) { fake := new_recording_transport() service := New(fake) defer service.Close() entered, stopped := make(chan struct{}), make(chan struct{}) service.Say_after("discarded notification", func(ctx context.Context) { close(entered) <-ctx.Done() close(stopped) }) <-entered switch action { case "interrupt": service.Interrupt() case "mute": service.Toggle_mute() case "close": service.Close() } select { case <-stopped: case <-time.After(time.Second): t.Fatal("sound wait was not canceled") } if action == "mute" { service.Toggle_mute() } if action != "close" { service.Say("fresh review") wait_event(t, fake.changed, "sent:fresh review") } fake.mu.Lock() defer fake.mu.Unlock() for _, event := range fake.events { if event == "say:discarded notification" { t.Fatal("late notification speech escaped cancellation") } } }) } }