package voice import ( "context" "errors" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" ) // stubSTT 是一个返回固定结果的 mock STT。 type stubSTT struct { text string err error } func (s *stubSTT) STT(_ context.Context, _ Audio) (string, error) { return s.text, s.err } // stubTTS 是一个返回固定结果的 mock TTS。 type stubTTS struct { audio Audio err error } func (s *stubTTS) TTS(_ context.Context, _ string) (Audio, error) { return s.audio, s.err } // stubProvider 是携带可选方向的 mock Provider。 type stubProvider struct { name string stt STT tts TTS } func (s *stubProvider) Name() string { return s.name } func (s *stubProvider) STT() STT { return s.stt } func (s *stubProvider) TTS() TTS { return s.tts } func TestChain_STT_Degrades(t *testing.T) { t.Parallel() tests := []struct { name string steps []Provider want string wantErr bool }{ { name: "first provider succeeds", steps: []Provider{&stubProvider{name: "cloud", stt: &stubSTT{text: "hello"}}}, want: "hello", }, { name: "first fails second succeeds", steps: []Provider{ &stubProvider{name: "cloud", stt: &stubSTT{err: ErrNotConfigured}}, &stubProvider{name: "local", stt: &stubSTT{text: "degraded"}}, }, want: "degraded", }, { name: "nil direction skipped", steps: []Provider{ &stubProvider{name: "edge", stt: nil}, &stubProvider{name: "silent", stt: &stubSTT{text: "silence"}}, }, want: "silence", }, { name: "all fail no silent -> error", steps: []Provider{ &stubProvider{name: "cloud", stt: &stubSTT{err: ErrNotConfigured}}, }, wantErr: true, }, { name: "all fail silent last -> no error", steps: []Provider{ &stubProvider{name: "cloud", stt: &stubSTT{err: ErrNotConfigured}}, SilentProvider(), }, want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := NewChain(tt.steps...).STT(context.Background(), Audio{Format: "wav", Data: []byte("x")}) if tt.wantErr { if !errors.Is(err, ErrProviderFailed) { t.Fatalf("err = %v, want ErrProviderFailed", err) } return } if err != nil { t.Fatalf("STT error: %v", err) } if got != tt.want { t.Errorf("text = %q, want %q", got, tt.want) } }) } } func TestChain_TTS_Degrades(t *testing.T) { t.Parallel() tests := []struct { name string steps []Provider want Audio wantErr bool }{ { name: "first provider succeeds", steps: []Provider{&stubProvider{name: "cloud", tts: &stubTTS{audio: Audio{Format: "mp3", Data: []byte("a")}}}}, want: Audio{Format: "mp3", Data: []byte("a")}, }, { name: "first fails second succeeds", steps: []Provider{ &stubProvider{name: "cloud", tts: &stubTTS{err: ErrNotConfigured}}, &stubProvider{name: "edge-tts", tts: &stubTTS{audio: Audio{Format: "mp3", Data: []byte("b")}}}, }, want: Audio{Format: "mp3", Data: []byte("b")}, }, { name: "all fail no silent -> error", steps: []Provider{ &stubProvider{name: "cloud", tts: &stubTTS{err: ErrNotConfigured}}, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := NewChain(tt.steps...).TTS(context.Background(), "hi") if tt.wantErr { if !errors.Is(err, ErrProviderFailed) { t.Fatalf("err = %v, want ErrProviderFailed", err) } return } if err != nil { t.Fatalf("TTS error: %v", err) } if got.Format != tt.want.Format || string(got.Data) != string(tt.want.Data) { t.Errorf("audio = %+v, want %+v", got, tt.want) } }) } } func TestChain_Empty(t *testing.T) { t.Parallel() if _, err := NewChain().STT(context.Background(), Audio{}); !errors.Is(err, ErrProviderFailed) { t.Fatalf("err = %v, want ErrProviderFailed", err) } } func TestNew_ConfigAssembly(t *testing.T) { t.Parallel() // 配了云端 key 且 TTS 默认走 edge-tts 时,链从 cloud 开始、 // 到 silent 结束。 c := New(config.VoiceConfig{DashscopeAPIKey: "k"}) names := make([]string, 0, len(c.steps)) for _, p := range c.steps { names = append(names, p.Name()) } if len(names) < 2 || names[len(names)-1] != "silent" { t.Fatalf("steps = %v, want to end at silent", names) } if names[0] != "cloud" { t.Errorf("first step = %q, want cloud", names[0]) } // 没有 key 时跳过云端一级。 c2 := New(config.VoiceConfig{}) foundCloud := false for _, p := range c2.steps { if p.Name() == "cloud" { foundCloud = true } } if foundCloud { t.Error("cloud step present without an API key") } }