Files
AskaEth b0fbb8bc0d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/multimodal(21 文件,纯替换)
- 只翻注释/文档字符串,代码零变化:go/scanner 词法流比对 21 文件 token+字面量全等,增删行数 1:1(467/467)
- 刻意保留非散文英文:JSON Schema description、工具 Description() 返回值、adapter prompt、API 路径与命令示例、环境变量名
- 逐行确认代码前缀未变,未触碰其他模块
2026-10-04 12:28:16 +08:00

187 lines
4.6 KiB
Go

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