// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package speech import ( "bufio" "context" "errors" "fmt" "net" "testing" "time" ) func TestContinuousSpeechWaitsForEndAndSurvivesCancellation(t *testing.T) { for _, interrupted := range []bool{false, true} { t.Run(fmt.Sprint(interrupted), func(t *testing.T) { conn, server := net.Pipe() control, controlServer := net.Pipe() client := connectedClient(conn) client.cancelConn, client.cancelReader, client.clientID = control, bufio.NewReader(control), 42 t.Cleanup(func() { server.Close(); controlServer.Close(); client.Close(context.Background()) }) ctx, cancel := context.WithCancel(context.Background()) defer cancel() began := make(chan struct{}, 1) finished := make(chan error, 1) go func() { finished <- client.SpeakAndWait(ctx, "First line", func() { began <- struct{}{} }) }() server.SetDeadline(time.Now().Add(2 * time.Second)) reader := bufio.NewReader(server) if err := exchangeSSIP(reader, server, "SET SELF NOTIFICATION ALL on\r\n", "200 OK\r\n"); err != nil { t.Fatal(err) } for _, want := range []string{"SPEAK\r\n", "First line\r\n", ".\r\n"} { got, err := reader.ReadString('\n') if err != nil || got != want { t.Fatalf("speech block %q, %v; want %q", got, err, want) } } fmt.Fprint(server, "230 OK\r\n225-1\r\n225 QUEUED\r\n") select { case <-began: t.Fatal("queue acknowledgement advanced reading position") case <-finished: t.Fatal("queue acknowledgement completed speech") default: } fmt.Fprint(server, "701-1\r\n701-42\r\n701 BEGIN\r\n") select { case <-began: case <-time.After(time.Second): t.Fatal("BEGIN did not update reading position") } if interrupted { cancel() controlServer.SetDeadline(time.Now().Add(time.Second)) if err := exchangeSSIP(bufio.NewReader(controlServer), controlServer, "CANCEL 42\r\n", "200 OK\r\n"); err != nil { t.Fatal(err) } fmt.Fprint(server, "703-1\r\n703-42\r\n703 CANCELED\r\n") } else { fmt.Fprint(server, "702-1\r\n702-42\r\n702 END\r\n") } if err := exchangeSSIP(reader, server, "SET SELF NOTIFICATION ALL off\r\n", "200 OK\r\n"); err != nil { t.Fatal(err) } if err := <-finished; interrupted && !errors.Is(err, context.Canceled) || !interrupted && err != nil { t.Fatalf("completion = %v", err) } // The cancellation notification must not become the next SPEAK reply. go func() { finished <- client.Speak(context.Background(), "After") }() for range 3 { if _, err := reader.ReadString('\n'); err != nil { t.Fatal(err) } } fmt.Fprint(server, "230 OK\r\n225-2\r\n225 QUEUED\r\n") if err := <-finished; err != nil { t.Fatalf("speech after read all: %v", err) } }) } }