package pipeline import ( "context" "testing" "time" "ai-operator/internal/ai" "ai-operator/internal/config" "ai-operator/internal/media" ) func TestTextChunker(t *testing.T) { c := NewTextChunker(10, 80, 20, time.Millisecond, true) chunks := c.Add("Первое предложение. Второе", false) if len(chunks) != 1 || chunks[0] != "Первое предложение." { t.Fatalf("chunks=%v", chunks) } flush := c.Flush() if len(flush) != 1 || flush[0] != "Второе" { t.Fatalf("flush=%v", flush) } } func TestNaturalizer(t *testing.T) { cfg := config.NaturalnessConfig{Enabled: true, AudioTagsEnabled: true, AllowNonverbalTags: true, MaxAudioTagsPerResponse: 2} got := NaturalizeForVoice("Здравствуйте, меня зовут Жанна. Чем могу помочь?", "ru", cfg) if got == "" || got == "Здравствуйте, меня зовут Жанна. Чем могу помочь?" { t.Fatalf("not naturalized: %q", got) } if stripped := RemoveAudioTags(got); stripped == "" || stripped == got { t.Fatalf("tags not stripped: %q", stripped) } cfg.AllowCough = false if got := NaturalizeForVoice("Первичное подключение газа бесплатно.", "ru", cfg); containsRune(got, "cough") { t.Fatalf("cough added unexpectedly: %q", got) } } func TestStreamingProviderFakeEndToEnd(t *testing.T) { cfg := config.Config{ STT: config.STTConfig{Model: "scribe_realtime_v2", SampleRate: 16000, InputFormat: "pcm_16000"}, LLM: config.LLMConfig{StreamChunkMinChars: 10, StreamChunkMaxChars: 160, FirstChunkTimeoutMS: 1, MaxOutputTokens: 100, Temperature: 0.2}, Eleven: config.ElevenLabsConfig{VoiceIDRU: "voice", TTSModelID: "eleven_flash_v2_5", TTSOutputFormat: "pcm_16000", TTSSampleRate: 16000}, Natural: config.NaturalnessConfig{Enabled: false}, Pipeline: config.PipelineConfig{InitialGreeting: false, BargeIn: true, MaxTurnSeconds: 2, TTSStartAfterChars: 20, TTSStartAfterPunctuation: true}, } factory := &FakeTTSFactory{} p := NewStreamingProviderWithDeps(cfg, nil, NewFakeSTT(), FakeLLM{}, factory.New) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := p.StartSession(ctx, ai.VoiceSessionConfig{CallID: "c1", SystemPrompt: "prompt"}); err != nil { t.Fatal(err) } if err := p.SendAudio(ctx, media.AudioChunk{Data: []byte{0, 0}}); err != nil { t.Fatal(err) } tool := false audio := false for !audio { select { case ev := <-p.Events(): if ev.Type == ai.VoiceEventToolCall { tool = true _ = p.SendToolResult(ctx, ai.ToolResult{CallID: ev.CallID, ToolCallID: ev.ToolCall.ID, Result: map[string]any{"answer_text": "Первичное подключение бесплатно."}}) } if ev.Type == ai.VoiceEventAssistantAudioDelta { audio = true } case <-ctx.Done(): t.Fatal("timeout") } } if !tool || !audio || len(factory.Texts) == 0 { t.Fatalf("tool=%t audio=%t texts=%v", tool, audio, factory.Texts) } } func containsRune(s, sub string) bool { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return true } } return false }