package main import ( "context" "net/http" "net/http/httptest" "os" "path/filepath" "testing" ) func TestNormalizeTTS(t *testing.T) { cfg := agentCfg{TTSModel: "cosyvoice-v3.5-plus", TTSVoice: "clone-id"} got, err := normalizeTTS(nil, cfg) if err != nil || got.Voice != "clone-id" || got.Rate != 1 || got.Pitch != 1 || got.Volume != 50 { t.Fatalf("defaults: got=%+v err=%v", got, err) } bad := ttsCfg{Model: "m", Voice: "v", Rate: 2.1, Pitch: 1, Volume: 50} if _, err := normalizeTTS(&bad, cfg); err == nil { t.Fatal("expected out-of-range rate to fail") } } func TestLoadDotEnv(t *testing.T) { const loaded = "VOICE_TEST_DOTENV_LOADED" old, existed := os.LookupEnv(loaded) os.Unsetenv(loaded) defer func() { if existed { os.Setenv(loaded, old) } else { os.Unsetenv(loaded) } }() t.Setenv("VOICE_TEST_DOTENV_EXISTING", "system") path := filepath.Join(t.TempDir(), ".env") if err := os.WriteFile(path, []byte("export "+loaded+"=\"hello world\"\nVOICE_TEST_DOTENV_EXISTING=file\n"), 0600); err != nil { t.Fatal(err) } if err := loadDotEnv(path); err != nil { t.Fatal(err) } if os.Getenv(loaded) != "hello world" || os.Getenv("VOICE_TEST_DOTENV_EXISTING") != "system" { t.Fatal("dotenv loading or environment precedence failed") } } func TestASRCloseLeavesEventsForReader(t *testing.T) { t.Run("bailian", func(t *testing.T) { a := newBailianASR("model", agentCfg{}) a.close() a.emit(asrEvent{Typ: "final", Text: "ok"}) if ev := <-a.events(); ev.Text != "ok" { t.Fatalf("event=%+v", ev) } a.closeUpstream() }) t.Run("volc", func(t *testing.T) { v := newVolcASR(agentCfg{}) v.close() v.emit(asrEvent{Typ: "final", Text: "ok"}) if ev := <-v.events(); ev.Text != "ok" { t.Fatalf("event=%+v", ev) } v.closeUpstream() }) } func TestLLMRequestErrorClosesStream(t *testing.T) { text, errs := llmChat(context.Background(), agentCfg{BailianBaseURL: "://"}, nil) if _, open := <-text; open { t.Fatal("text stream left open after request error") } if err := <-errs; err == nil { t.Fatal("expected request error") } } func TestInterruptTurn(t *testing.T) { s := &session{} ctx, cancel, _ := s.startTurn() defer cancel() s.interruptTurn() select { case <-ctx.Done(): default: t.Fatal("active turn was not canceled") } } func TestListClonedVoices(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer key" { t.Fatal("missing authorization") } w.Write([]byte(`{"output":{"voice_list":[{"voice_id":"clone-ok","status":"OK"},{"voice_id":"clone-building","status":"DEPLOYING"}]}}`)) })) defer server.Close() voices, err := listClonedVoices(context.Background(), agentCfg{BailianKey: "key", VoiceAPIURL: server.URL}) if err != nil || len(voices) != 1 || voices[0].ID != "clone-ok" { t.Fatalf("voices=%+v err=%v", voices, err) } }