Files
Cyrene_v2/internal/runtime/multimodal/voice/chain_test.go
T

187 lines
4.6 KiB
Go

package voice
import (
"context"
"errors"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
)
// stubSTT is a mock STT returning a fixed result.
type stubSTT struct {
text string
err error
}
func (s *stubSTT) STT(_ context.Context, _ Audio) (string, error) { return s.text, s.err }
// stubTTS is a mock TTS returning a fixed result.
type stubTTS struct {
audio Audio
err error
}
func (s *stubTTS) TTS(_ context.Context, _ string) (Audio, error) { return s.audio, s.err }
// stubProvider is a mock Provider carrying optional directions.
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()
// With a cloud key and default edge-tts TTS, the chain starts at cloud and
// ends at 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])
}
// Without a key the cloud rung is skipped.
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")
}
}