Files
go-sip/internal/ai/approved_mock_test.go
T

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")
}
}