100 lines
5.9 KiB
Go
100 lines
5.9 KiB
Go
package ai
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
func TestApprovedMockASROnlyDoesNotInventRefusalOrAssistantAudio(t *testing.T) {
|
|
var hangups int
|
|
pipeline, err := NewApprovedMockPipeline(Binding{Mode: "asr_only", Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{
|
|
Turns: []ApprovedMockTurn{{Transcript: "我拒绝但是没有配置关键词"}},
|
|
}, func(context.Context) error { hangups++; return nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if opening, err := pipeline.Open(context.Background()); err != nil || len(opening) != 0 {
|
|
t.Fatalf("ASR-only mock generated an opening: size=%d err=%v", len(opening), err)
|
|
}
|
|
turn, err := pipeline.RunTurn(context.Background(), bytes.Repeat([]byte{1, 0}, 1600))
|
|
if err != nil || turn.Transcript != "我拒绝但是没有配置关键词" || turn.Reply != "" || len(turn.AudioPCM16) != 0 || turn.EndedByKeyword || hangups != 0 {
|
|
t.Fatalf("mock invented unapproved full-AI or refusal facts: turn=%+v hangups=%d err=%v", turn, hangups, err)
|
|
}
|
|
if _, err := pipeline.RunTurn(context.Background(), []byte{1, 0}); err == nil {
|
|
t.Fatal("mock replayed an already used ASR result")
|
|
}
|
|
}
|
|
|
|
func TestApprovedMockAbsentOptionalTurnLimitUsesSharedOneTurnBoundary(t *testing.T) {
|
|
pipeline, err := NewApprovedMockPipeline(Binding{Mode: "asr_only"}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "隔离识别"}}}, func(context.Context) error { return nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if turn, err := pipeline.RunTurn(context.Background(), bytes.Repeat([]byte{1, 0}, 1600)); err != nil || turn.Transcript != "隔离识别" {
|
|
t.Fatalf("absent optional turn limit incorrectly rejected one approved turn: turn=%+v err=%v", turn, err)
|
|
}
|
|
}
|
|
|
|
func TestApprovedMockFullAIFreezesScriptAndReturnsKeywordClosing(t *testing.T) {
|
|
opening := bytes.Repeat([]byte{1, 0}, 320)
|
|
reply := bytes.Repeat([]byte{2, 0}, 320)
|
|
script := ApprovedMockScript{OpeningPCM16: opening, Turns: []ApprovedMockTurn{
|
|
{Transcript: "正常模拟用户语音", Reply: "模拟助手答复", ReplyPCM16: reply},
|
|
{Transcript: "请不要联系", Reply: "好的,再见。", ReplyPCM16: reply},
|
|
}}
|
|
var hangups int
|
|
bound := Binding{Mode: "full_ai", Opening: "批准的开场", HangupKeywords: []HangupKeyword{{Name: "结束", Triggers: []string{"不要联系"}, ClosingRemark: "好的,再见。"}}, LLM: &LLMConfig{}, TTS: &TTSConfig{}, Conversation: ConversationConfig{MaxTurns: 2}}
|
|
pipeline, err := NewApprovedMockPipeline(bound, script, func(context.Context) error { hangups++; return nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opening[0], reply[0], script.Turns[0].Transcript = 9, 9, "修改后的转写"
|
|
played, err := pipeline.Open(context.Background())
|
|
if err != nil || len(played) != 640 || played[0] != 1 {
|
|
t.Fatalf("opening was not frozen from the approved mock fixture: size=%d err=%v", len(played), err)
|
|
}
|
|
if _, err := pipeline.Open(context.Background()); err == nil {
|
|
t.Fatal("ambiguous opening was replayed")
|
|
}
|
|
first, err := pipeline.RunTurn(context.Background(), bytes.Repeat([]byte{1, 0}, 1600))
|
|
if err != nil || first.Transcript != "正常模拟用户语音" || first.Reply != "模拟助手答复" || len(first.AudioPCM16) != 640 || first.AudioPCM16[0] != 2 {
|
|
t.Fatalf("full-AI mock changed approved fixture bytes: turn=%+v err=%v", first, err)
|
|
}
|
|
last, err := pipeline.RunTurn(context.Background(), bytes.Repeat([]byte{1, 0}, 1600))
|
|
if err != nil || !last.EndedByKeyword || last.Transcript != "请不要联系" || last.Reply != "好的,再见。" || len(last.AudioPCM16) != 640 || last.AudioPCM16[0] != 2 || hangups != 0 {
|
|
t.Fatalf("keyword closing was not frozen or hung up early: turn=%+v hangups=%d err=%v", last, hangups, err)
|
|
}
|
|
if err := pipeline.FinishKeyword(context.Background()); err != nil || hangups != 1 {
|
|
t.Fatalf("closing hangup failed: %v count=%d", err, hangups)
|
|
}
|
|
if _, err := pipeline.RunTurn(context.Background(), bytes.Repeat([]byte{1, 0}, 1600)); err == nil {
|
|
t.Fatal("mock continued after closing")
|
|
}
|
|
}
|
|
|
|
func TestApprovedMockRejectsMissingAIOrUnscriptedMedia(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
bound Binding
|
|
script ApprovedMockScript
|
|
}{
|
|
{"missing_script", Binding{Mode: "asr_only", Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{}},
|
|
{"asr_with_llm", Binding{Mode: "asr_only", LLM: &LLMConfig{}, Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}},
|
|
{"full_without_llm", Binding{Mode: "full_ai", TTS: &TTSConfig{}, Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}},
|
|
{"unavailable_opening_audio", Binding{Mode: "full_ai", LLM: &LLMConfig{}, TTS: &TTSConfig{}, Opening: "批准开场", Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试", Reply: "模拟答复", ReplyPCM16: []byte{1, 0}}}}},
|
|
{"unsolicited_odd_opening_audio", Binding{Mode: "full_ai", LLM: &LLMConfig{}, TTS: &TTSConfig{}, Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{OpeningPCM16: []byte{1}, Turns: []ApprovedMockTurn{{Transcript: "测试", Reply: "模拟答复", ReplyPCM16: []byte{1, 0}}}}},
|
|
{"extra_unapproved_turn", Binding{Mode: "asr_only", Conversation: ConversationConfig{MaxTurns: 1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "第一轮"}, {Transcript: "第二轮"}}}},
|
|
{"negative_turn_limit", Binding{Mode: "asr_only", Conversation: ConversationConfig{MaxTurns: -1}}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := NewApprovedMockPipeline(tc.bound, tc.script, func(context.Context) error { return nil }); err == nil {
|
|
t.Fatal("unapproved Mock settings were accepted")
|
|
}
|
|
})
|
|
}
|
|
if _, err := NewApprovedMockPipeline(Binding{Mode: "asr_only"}, ApprovedMockScript{Turns: []ApprovedMockTurn{{Transcript: "测试"}}}, nil); err == nil {
|
|
t.Fatal("mock without a real hangup callback was admitted")
|
|
}
|
|
}
|