mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-09 18:25:43 +00:00
- HTTP synthesize + test-connection now read tenant tts.timeout_ms (default 120s, was hardcoded 15s/10s). Gemini client default also bumped 30s→120s so both layers align when tenant config unset. - Inline prefix "Speak naturally: " prepended to single-voice text; multi-speaker transcripts pass through unchanged. - ErrTextOnlyResponse sentinel for 400 "text generation" bodies; single-voice retries once with stronger prefix. Narrowed needle list avoids false positives on unrelated 400s. - SynthesizeWithFallbackAdapted now returns errors.Join so sentinel survives fallback chain; HTTP 422 mapping + locale-translated ForLLM in agent tool (EN/VI/ZH catalogs). - Default Gemini model bumped to gemini-3.1-flash-tts-preview.
66 lines
2.2 KiB
Go
66 lines
2.2 KiB
Go
package audio_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/audio"
|
|
"github.com/nextlevelbuilder/goclaw/internal/audio/gemini"
|
|
)
|
|
|
|
// mockSentinelTTS returns a configurable error from Synthesize.
|
|
type mockSentinelTTS struct {
|
|
providerName string
|
|
err error
|
|
}
|
|
|
|
func (m *mockSentinelTTS) Name() string { return m.providerName }
|
|
func (m *mockSentinelTTS) Synthesize(_ context.Context, _ string, _ audio.TTSOptions) (*audio.SynthResult, error) {
|
|
return nil, m.err
|
|
}
|
|
|
|
// TestSynthesizeWithFallbackAdapted_PreservesTextOnlySentinel verifies that
|
|
// ErrTextOnlyResponse survives through SynthesizeWithFallbackAdapted so that
|
|
// errors.Is(err, gemini.ErrTextOnlyResponse) returns true at the call site.
|
|
func TestSynthesizeWithFallbackAdapted_PreservesTextOnlySentinel(t *testing.T) {
|
|
t.Run("primary_only_returns_sentinel", func(t *testing.T) {
|
|
// Single provider: primary returns ErrTextOnlyResponse. No fallback.
|
|
mgr := audio.NewManager(audio.ManagerConfig{Primary: "gemini"})
|
|
mgr.RegisterTTS(&mockSentinelTTS{
|
|
providerName: "gemini",
|
|
err: gemini.ErrTextOnlyResponse,
|
|
})
|
|
|
|
_, err := mgr.SynthesizeWithFallbackAdapted(context.Background(), "hello", audio.TTSOptions{}, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if !errors.Is(err, gemini.ErrTextOnlyResponse) {
|
|
t.Errorf("errors.Is(err, ErrTextOnlyResponse) = false; err = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("primary_sentinel_plus_fallback_other_error", func(t *testing.T) {
|
|
// Primary returns ErrTextOnlyResponse; fallback returns a different error.
|
|
// Sentinel must survive errors.Join.
|
|
mgr := audio.NewManager(audio.ManagerConfig{Primary: "gemini"})
|
|
mgr.RegisterTTS(&mockSentinelTTS{
|
|
providerName: "gemini",
|
|
err: gemini.ErrTextOnlyResponse,
|
|
})
|
|
mgr.RegisterTTS(&mockSentinelTTS{
|
|
providerName: "openai",
|
|
err: errors.New("openai: connection refused"),
|
|
})
|
|
|
|
_, err := mgr.SynthesizeWithFallbackAdapted(context.Background(), "hello", audio.TTSOptions{}, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if !errors.Is(err, gemini.ErrTextOnlyResponse) {
|
|
t.Errorf("errors.Is(err, ErrTextOnlyResponse) = false after errors.Join; err = %v", err)
|
|
}
|
|
})
|
|
}
|