111 lines
3.9 KiB
Go
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")
|
|
}
|
|
}
|