mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-25 12:22:44 +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.
210 lines
7.0 KiB
Go
210 lines
7.0 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/audio"
|
|
)
|
|
|
|
// sleepingTTSProvider is a test-only TTS provider that sleeps for a configurable
|
|
// duration before returning, used to exercise handler timeout paths.
|
|
type sleepingTTSProvider struct {
|
|
sleepMs int
|
|
}
|
|
|
|
func (s *sleepingTTSProvider) Name() string { return "sleep" }
|
|
|
|
func (s *sleepingTTSProvider) Synthesize(ctx context.Context, text string, opts audio.TTSOptions) (*audio.SynthResult, error) {
|
|
select {
|
|
case <-time.After(time.Duration(s.sleepMs) * time.Millisecond):
|
|
return &audio.SynthResult{Audio: []byte("ok"), MimeType: "audio/mpeg"}, nil
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
// stubSystemConfigStore returns configured values for "tts.timeout_ms" and ignores others.
|
|
type stubSystemConfigStore struct {
|
|
timeoutMsValue string // raw string returned for "tts.timeout_ms"
|
|
}
|
|
|
|
func (s *stubSystemConfigStore) Get(_ context.Context, key string) (string, error) {
|
|
if key == "tts.timeout_ms" {
|
|
return s.timeoutMsValue, nil
|
|
}
|
|
return "", nil
|
|
}
|
|
func (s *stubSystemConfigStore) Set(_ context.Context, _, _ string) error { return nil }
|
|
func (s *stubSystemConfigStore) Delete(_ context.Context, _ string) error { return nil }
|
|
func (s *stubSystemConfigStore) List(_ context.Context) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
|
|
// newTTSMuxWithStore builds a TTSHandler backed by mgr and systemConfigs, wires routes.
|
|
func newTTSMuxWithStore(mgr *audio.Manager, sc *stubSystemConfigStore) *http.ServeMux {
|
|
h := NewTTSHandler(mgr)
|
|
h.SetStores(sc, nil)
|
|
mux := http.NewServeMux()
|
|
h.RegisterRoutes(mux)
|
|
return mux
|
|
}
|
|
|
|
// synthRequestBody builds the POST /v1/tts/synthesize JSON body.
|
|
func synthRequestBody(t *testing.T, text string) *bytes.Buffer {
|
|
t.Helper()
|
|
b, _ := json.Marshal(map[string]string{"text": text})
|
|
return bytes.NewBuffer(b)
|
|
}
|
|
|
|
// --- Synthesize timeout tests ---
|
|
|
|
// TestSynthesize_UsesTenantTimeoutMs verifies the handler applies tts.timeout_ms from
|
|
// tenant config. Backend sleeps 1000ms with tenant timeout=500ms → expect 504.
|
|
// Backend sleeps 100ms with tenant timeout=500ms → expect 200.
|
|
func TestSynthesize_UsesTenantTimeoutMs(t *testing.T) {
|
|
setupTestToken(t, "") // dev mode — no auth required
|
|
|
|
sc := &stubSystemConfigStore{timeoutMsValue: "500"}
|
|
|
|
// Slow path: backend sleeps 1000ms, tenant timeout 500ms → 504.
|
|
provider := &sleepingTTSProvider{sleepMs: 1000}
|
|
mgr := audio.NewManager(audio.ManagerConfig{})
|
|
mgr.RegisterTTS(provider)
|
|
|
|
mux := newTTSMuxWithStore(mgr, sc)
|
|
|
|
req := httptest.NewRequest("POST", "/v1/tts/synthesize", synthRequestBody(t, "hello"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
mux.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusGatewayTimeout {
|
|
t.Errorf("want 504 (tenant timeout 500ms, backend sleeps 1000ms), got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// Fast path: backend sleeps 100ms, tenant timeout 500ms → 200.
|
|
provider2 := &sleepingTTSProvider{sleepMs: 100}
|
|
mgr2 := audio.NewManager(audio.ManagerConfig{})
|
|
mgr2.RegisterTTS(provider2)
|
|
|
|
mux2 := newTTSMuxWithStore(mgr2, sc)
|
|
|
|
req2 := httptest.NewRequest("POST", "/v1/tts/synthesize", synthRequestBody(t, "hello"))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
rr2 := httptest.NewRecorder()
|
|
mux2.ServeHTTP(rr2, req2)
|
|
|
|
if rr2.Code != http.StatusOK {
|
|
t.Errorf("want 200 (tenant timeout 500ms, backend sleeps 100ms), got %d: %s", rr2.Code, rr2.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestSynthesize_DefaultTimeoutWhenTenantUnset verifies that when tts.timeout_ms is
|
|
// unset, the handler uses defaultSynthesizeTimeoutMs (>=120000ms, not old 15s).
|
|
func TestSynthesize_DefaultTimeoutWhenTenantUnset(t *testing.T) {
|
|
setupTestToken(t, "") // dev mode
|
|
|
|
sc := &stubSystemConfigStore{timeoutMsValue: ""} // no tenant timeout
|
|
|
|
provider := &sleepingTTSProvider{sleepMs: 100}
|
|
mgr := audio.NewManager(audio.ManagerConfig{})
|
|
mgr.RegisterTTS(provider)
|
|
|
|
mux := newTTSMuxWithStore(mgr, sc)
|
|
|
|
req := httptest.NewRequest("POST", "/v1/tts/synthesize", synthRequestBody(t, "hello"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
mux.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("want 200 (default timeout unset, backend fast), got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// Assert the default constant is >=120000ms (not the old 15s).
|
|
if defaultSynthesizeTimeoutMs < 120000 {
|
|
t.Errorf("defaultSynthesizeTimeoutMs must be >=120000, got %d", defaultSynthesizeTimeoutMs)
|
|
}
|
|
}
|
|
|
|
// TestSynthesize_TenantTimeoutInvalidFallsBackToDefault verifies that an invalid
|
|
// (non-numeric) tts.timeout_ms falls back to the 120s default and allows fast backends.
|
|
func TestSynthesize_TenantTimeoutInvalidFallsBackToDefault(t *testing.T) {
|
|
setupTestToken(t, "") // dev mode
|
|
|
|
sc := &stubSystemConfigStore{timeoutMsValue: "abc"} // invalid
|
|
|
|
provider := &sleepingTTSProvider{sleepMs: 100}
|
|
mgr := audio.NewManager(audio.ManagerConfig{})
|
|
mgr.RegisterTTS(provider)
|
|
|
|
mux := newTTSMuxWithStore(mgr, sc)
|
|
|
|
req := httptest.NewRequest("POST", "/v1/tts/synthesize", synthRequestBody(t, "hello"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
mux.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("want 200 (invalid tenant value → default, backend fast), got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|
|
|
|
// --- Test-connection timeout resolution tests ---
|
|
|
|
// TestTestConnection_ReqTimeoutOverridesTenant verifies that a non-zero req.TimeoutMs
|
|
// overrides the tenant config value (req=500, tenant=5000 → effective=500).
|
|
func TestTestConnection_ReqTimeoutOverridesTenant(t *testing.T) {
|
|
sc := &stubSystemConfigStore{timeoutMsValue: "5000"} // tenant=5000ms
|
|
|
|
tenantMs := loadTenantTTSTimeoutMs(context.Background(), sc)
|
|
if tenantMs != 5000 {
|
|
t.Fatalf("precondition: tenant timeout should be 5000, got %d", tenantMs)
|
|
}
|
|
|
|
// Simulate handler priority: req.TimeoutMs > 0 → use req value.
|
|
reqTimeoutMs := 500
|
|
effectiveMs := reqTimeoutMs
|
|
if effectiveMs <= 0 {
|
|
effectiveMs = tenantMs
|
|
}
|
|
if effectiveMs <= 0 {
|
|
effectiveMs = defaultTestConnectionTimeoutMs
|
|
}
|
|
|
|
if effectiveMs != 500 {
|
|
t.Errorf("effectiveMs should be 500 (req override), got %d", effectiveMs)
|
|
}
|
|
}
|
|
|
|
// TestTestConnection_TenantFallbackWhenReqZero verifies that when req.TimeoutMs=0,
|
|
// the handler falls back to the saved tenant config value (tenant=800 → effective=800).
|
|
func TestTestConnection_TenantFallbackWhenReqZero(t *testing.T) {
|
|
sc := &stubSystemConfigStore{timeoutMsValue: "800"} // tenant=800ms
|
|
|
|
tenantMs := loadTenantTTSTimeoutMs(context.Background(), sc)
|
|
if tenantMs != 800 {
|
|
t.Fatalf("precondition: tenant timeout should be 800, got %d", tenantMs)
|
|
}
|
|
|
|
// Simulate handler priority: req.TimeoutMs=0 → fall back to tenant.
|
|
reqTimeoutMs := 0
|
|
effectiveMs := reqTimeoutMs
|
|
if effectiveMs <= 0 {
|
|
effectiveMs = tenantMs
|
|
}
|
|
if effectiveMs <= 0 {
|
|
effectiveMs = defaultTestConnectionTimeoutMs
|
|
}
|
|
|
|
if effectiveMs != 800 {
|
|
t.Errorf("effectiveMs should be 800 (tenant fallback), got %d", effectiveMs)
|
|
}
|
|
}
|