Files
goclaw/internal/tools/tts_agent_ctx_test.go
Duc Nguyen e3dad3c60c fix(tts): unify voice resolution — dashboard settings affect tts tool (#1175)
The dashboard /tts page writes to system_configs[tts.<provider>.voice],
but the LLM-invoked tts tool was only checking args > agent OtherConfig
> builtin_tool_tenant_configs[tts].default_voice_id. Two different
storage locations → the user's chosen voice was ignored, Edge defaulted
to en-US-AriaNeural even when "HoaiMy" (vi-VN-HoaiMyNeural) was set
correctly in the dashboard.

Add system_configs as a 4th-level fallback so the dashboard becomes
the single source of truth.

- TtsTool gains SetSystemConfigStore(s) setter
- resolveVoiceAndModel takes providerName + looks up
  tts.<provider>.voice/model when no higher-precedence source set
- effectiveProvider resolved BEFORE voice/model so the right key is hit
- Wired at gateway boot in cmd/gateway.go (after pgStores ready)
- 4 unit tests covering: fallback, arg precedence, no-store, empty provider

Verified against production trace 019e6036-44bb-703b-85fa-dee34f7ab2c0
where the tts tool was called with provider=edge, no voice arg, and
defaulted to English instead of the configured Vietnamese voice.
2026-07-18 00:22:48 +07:00

112 lines
3.3 KiB
Go

package tools
import (
"context"
"encoding/json"
"testing"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// ctxWithAgentAudio returns a context carrying an AgentAudioSnapshot whose
// OtherConfig has the given tts_voice_id / tts_model_id keys (either may be
// empty — empty means "not set by agent config").
func ctxWithAgentAudio(t *testing.T, voiceID, modelID string) context.Context {
t.Helper()
cfg := map[string]string{}
if voiceID != "" {
cfg["tts_voice_id"] = voiceID
}
if modelID != "" {
cfg["tts_model_id"] = modelID
}
raw, err := json.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
return store.WithAgentAudio(context.Background(), store.AgentAudioSnapshot{
AgentID: uuid.New(),
OtherConfig: raw,
})
}
func TestResolveVoiceAndModel_ArgsWinOverAgent(t *testing.T) {
tool := NewTtsTool(makeTTSManager("elevenlabs"))
ctx := ctxWithAgentAudio(t, "AGENT_V", "AGENT_M")
v, m, _ := tool.resolveVoiceAndModel(ctx, "edge", "ARG_V", "ARG_M")
if v != "ARG_V" {
t.Errorf("voice: got %q, want ARG_V (args must win)", v)
}
if m != "ARG_M" {
t.Errorf("model: got %q, want ARG_M (args must win)", m)
}
}
func TestResolveVoiceAndModel_AgentWinsOverTenantWhenArgsEmpty(t *testing.T) {
tool := NewTtsTool(makeTTSManager("elevenlabs"))
ctx := ctxWithAgentAudio(t, "AGENT_V", "AGENT_M")
ctx = WithBuiltinToolSettings(ctx, BuiltinToolSettings{
"tts": rawJSON(t, map[string]string{"default_voice_id": "TENANT_V", "default_model": "TENANT_M"}),
})
v, m, _ := tool.resolveVoiceAndModel(ctx, "edge", "", "")
if v != "AGENT_V" {
t.Errorf("voice: got %q, want AGENT_V (agent > tenant)", v)
}
if m != "AGENT_M" {
t.Errorf("model: got %q, want AGENT_M (agent > tenant)", m)
}
}
func TestResolveVoiceAndModel_TenantFallbackWhenAgentSilent(t *testing.T) {
tool := NewTtsTool(makeTTSManager("elevenlabs"))
// No agent audio ctx, only tenant settings.
ctx := WithBuiltinToolSettings(context.Background(), BuiltinToolSettings{
"tts": rawJSON(t, map[string]string{"default_voice_id": "TENANT_V", "default_model": "TENANT_M"}),
})
v, m, _ := tool.resolveVoiceAndModel(ctx, "edge", "", "")
if v != "TENANT_V" {
t.Errorf("voice: got %q, want TENANT_V", v)
}
if m != "TENANT_M" {
t.Errorf("model: got %q, want TENANT_M", m)
}
}
func TestResolveVoiceAndModel_EmptyAllMeansDefault(t *testing.T) {
tool := NewTtsTool(makeTTSManager("elevenlabs"))
v, m, _ := tool.resolveVoiceAndModel(context.Background(), "edge", "", "")
if v != "" {
t.Errorf("voice: got %q, want empty (no sources → provider default)", v)
}
if m != "" {
t.Errorf("model: got %q, want empty (no sources → provider default)", m)
}
}
func TestResolveVoiceAndModel_PartialAgentConfig(t *testing.T) {
tool := NewTtsTool(makeTTSManager("elevenlabs"))
// Agent only provides voice; model must fall to tenant.
ctx := ctxWithAgentAudio(t, "AGENT_V", "")
ctx = WithBuiltinToolSettings(ctx, BuiltinToolSettings{
"tts": rawJSON(t, map[string]string{"default_model": "TENANT_M"}),
})
v, m, _ := tool.resolveVoiceAndModel(ctx, "edge", "", "")
if v != "AGENT_V" {
t.Errorf("voice: got %q, want AGENT_V", v)
}
if m != "TENANT_M" {
t.Errorf("model: got %q, want TENANT_M (agent silent → tenant)", m)
}
}
func rawJSON(t *testing.T, v any) json.RawMessage {
t.Helper()
b, err := json.Marshal(v)
if err != nil {
t.Fatal(err)
}
return b
}