Files
go-sip/internal/callflow/keyword_closing_test.go
T

111 lines
3.9 KiB
Go

package callflow
import (
"context"
"errors"
"testing"
"time"
"git.ipao.vip/rogee/go-sip/internal/ai"
)
type closingSendSession struct {
*scriptedTurnSession
sends int
closing func(context.Context) error
}
func (s *closingSendSession) SendPCM16(ctx context.Context, pcm []byte, rate int) error {
s.sends++
if s.sends == 2 && s.closing != nil {
if err := s.closing(ctx); err != nil {
return err
}
}
return s.scriptedTurnSession.SendPCM16(ctx, pcm, rate)
}
func TestKeywordClosingFailuresRemainVisibleAndHangupOnce(t *testing.T) {
synthesisErr := errors.New("injected closing synthesis failure")
playbackErr := errors.New("injected closing playback failure")
hangupErr := errors.New("injected unknown hangup outcome")
for _, tc := range []struct {
name string
synthErr, sendErr, finishErr error
empty bool
}{
{name: "synthesis", synthErr: synthesisErr},
{name: "playback", sendErr: playbackErr},
{name: "hangup", finishErr: hangupErr},
{name: "both-playback-and-hangup", sendErr: playbackErr, finishErr: hangupErr},
{name: "empty-closing", empty: true},
} {
t.Run(tc.name, func(t *testing.T) {
session := &closingSendSession{scriptedTurnSession: &scriptedTurnSession{turns: [][]byte{make([]byte, 6400)}}, closing: func(context.Context) error { return tc.sendErr }}
hangups := 0
turn := ai.TurnResult{Transcript: "不需要", Reply: "好的,再见。", EndedByKeyword: true, AudioPCM16: make([]byte, 6400)}
if tc.empty {
turn.AudioPCM16 = nil
}
pipeline := &approvedFlowPipeline{turn: turn, turnErr: tc.synthErr, finish: func(context.Context) error { hangups++; return tc.finishErr }}
result, err := ExecuteApproved(context.Background(), session, ai.ModeFullAI, pipeline, CaptureConfig{FirstSpeechTimeout: time.Second, MaxDuration: 2 * time.Millisecond, MaxTurns: 3})
if err == nil || hangups != 1 || pipeline.turnCalls != 1 || len(result.Turns) != 1 || !result.Turn.EndedByKeyword || result.Turn.Transcript != "不需要" {
t.Fatalf("closing error/facts lost or new turn started: err=%v hangups=%d turns=%d result=%+v", err, hangups, pipeline.turnCalls, result.Turn)
}
for _, want := range []error{tc.synthErr, tc.sendErr, tc.finishErr} {
if want != nil && !errors.Is(err, want) {
t.Fatalf("cause hidden: want=%v got=%v", want, err)
}
}
if (tc.synthErr != nil || tc.sendErr != nil || tc.empty) && len(result.OutboundTurns) != 1 {
t.Fatal("unsent closing reported as played")
}
if tc.synthErr != nil && session.sends != 1 {
t.Fatal("partial failed synthesis was played")
}
if session.sends > 2 {
t.Fatal("closing replayed")
}
})
}
}
func TestKeywordHangupWaitsForPlaybackReturn(t *testing.T) {
started, release, hungup := make(chan struct{}), make(chan struct{}), make(chan struct{})
session := &closingSendSession{scriptedTurnSession: &scriptedTurnSession{turns: [][]byte{make([]byte, 6400)}}, closing: func(context.Context) error {
close(started)
<-release
return nil
}}
pipeline := &approvedFlowPipeline{turn: ai.TurnResult{Transcript: "再见", Reply: "再见。", EndedByKeyword: true, AudioPCM16: make([]byte, 6400)}, finish: func(context.Context) error { close(hungup); return nil }}
done := make(chan error, 1)
go func() {
_, err := ExecuteApproved(context.Background(), session, ai.ModeFullAI, pipeline, CaptureConfig{FirstSpeechTimeout: time.Second, MaxDuration: 2 * time.Millisecond, MaxTurns: 3})
done <- err
}()
select {
case <-started:
case <-time.After(5 * time.Second):
close(release)
t.Fatal("closing playback never started")
}
select {
case <-hungup:
close(release)
t.Fatal("hung up while closing playback was still in progress")
default:
}
close(release)
if err := <-done; err != nil {
t.Fatal(err)
}
select {
case <-hungup:
default:
t.Fatal("playback completed without hangup")
}
if session.sends != 2 || pipeline.turnCalls != 1 {
t.Fatal("new turn/replay after closing")
}
}