mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-06 04:23:13 +00:00
- Add per-tenant TTS configuration endpoints (GET/POST /v1/tts/config) with RoleAdmin auth instead of master scope - Implement TenantTTSResolver for channels to use tenant-specific TTS providers and auto mode settings - Add per-agent voice override in channel TTS auto-apply: - Extend OutboundMessage with AgentID and AgentOtherConfig - Inject AgentAudioSnapshot in dispatch.go from outbound message - MaybeApply reads tts_voice_id/tts_model_id from agent context - Fix events.go to use RunContext.TenantID directly (H3) - Fix dispatch.go error notification to use sendCtx (H1) - Update UI to use new /v1/tts/config endpoints
134 lines
5.2 KiB
Go
134 lines
5.2 KiB
Go
package methods
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/agent"
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// errSentinelMiss is a sentinel error used to verify DB fallback in cache-miss
|
|
// paths. The value is stable across tests so equality checks are reliable.
|
|
var errSentinelMiss = errors.New("sentinel: agent miss")
|
|
|
|
// errorAgentStore embeds store.AgentStore and overrides only GetByID and
|
|
// GetByKey to return a sentinel error. All other methods retain the nil embed,
|
|
// so any unexpected call path panics — loud failure is intentional.
|
|
type errorAgentStore struct {
|
|
store.AgentStore
|
|
err error
|
|
}
|
|
|
|
func (s *errorAgentStore) GetByID(context.Context, uuid.UUID) (*store.AgentData, error) {
|
|
return nil, s.err
|
|
}
|
|
|
|
func (s *errorAgentStore) GetByKey(context.Context, string) (*store.AgentData, error) {
|
|
return nil, s.err
|
|
}
|
|
|
|
// TestResolveAgentUUIDCached_NilRouterCallsResolveAgentUUID verifies that when
|
|
// router is nil the helper delegates directly to resolveAgentUUID — exercising
|
|
// the slow DB path. With a stub store returning a sentinel error, the helper
|
|
// must propagate that error unchanged.
|
|
func TestResolveAgentUUIDCached_NilRouterCallsResolveAgentUUID(t *testing.T) {
|
|
stub := &errorAgentStore{err: errSentinelMiss}
|
|
|
|
_, err := resolveAgentUUIDCached(context.Background(), nil, stub, uuid.New().String())
|
|
|
|
if !errors.Is(err, errSentinelMiss) {
|
|
t.Errorf("expected sentinel miss error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestResolveAgentUUIDCached_CacheMissFallsBack — when the router is set but
|
|
// has no cached entry for the given agent_key, the helper must fall back to
|
|
// the DB path. The stub store's sentinel error confirms we took the fallback.
|
|
func TestResolveAgentUUIDCached_CacheMissFallsBack(t *testing.T) {
|
|
r := agent.NewRouter()
|
|
stub := &errorAgentStore{err: errSentinelMiss}
|
|
|
|
_, err := resolveAgentUUIDCached(context.Background(), r, stub, "missing-agent-key")
|
|
|
|
if !errors.Is(err, errSentinelMiss) {
|
|
t.Errorf("expected sentinel miss error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestResolveAgentUUIDCached_UUIDInputTakesDBPath verifies that a caller
|
|
// passing the UUID form falls through to the DB path. Router cache entries
|
|
// are canonicalized to `tenantID:agentKey`, so the raw UUID input never hits
|
|
// the cache and the helper must delegate to the store stub (whose sentinel
|
|
// error surfaces here).
|
|
func TestResolveAgentUUIDCached_UUIDInputTakesDBPath(t *testing.T) {
|
|
r := agent.NewRouter()
|
|
stub := &errorAgentStore{err: errSentinelMiss}
|
|
|
|
_, err := resolveAgentUUIDCached(context.Background(), r, stub, uuid.New().String())
|
|
|
|
if !errors.Is(err, errSentinelMiss) {
|
|
t.Errorf("expected sentinel miss error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// cacheHitStubAgent implements agent.Agent + agentUUIDProvider so it can be
|
|
// registered in the router and the cache-aware helper can extract its UUID
|
|
// without hitting the store.
|
|
type cacheHitStubAgent struct {
|
|
id string
|
|
uid uuid.UUID
|
|
}
|
|
|
|
func (s *cacheHitStubAgent) ID() string { return s.id }
|
|
func (s *cacheHitStubAgent) UUID() uuid.UUID { return s.uid }
|
|
func (s *cacheHitStubAgent) OtherConfig() json.RawMessage { return nil }
|
|
func (s *cacheHitStubAgent) Run(context.Context, agent.RunRequest) (*agent.RunResult, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *cacheHitStubAgent) IsRunning() bool { return false }
|
|
func (s *cacheHitStubAgent) Model() string { return "test-model" }
|
|
func (s *cacheHitStubAgent) ProviderName() string { return "test" }
|
|
func (s *cacheHitStubAgent) Provider() providers.Provider { return nil }
|
|
|
|
// TestResolveAgentUUIDCached_CacheHitSkipsDBPath pins the fast path: when the
|
|
// caller passes an agent_key AND the Loop is cached in the router AND the
|
|
// cached Loop satisfies agentUUIDProvider, the helper must return the cached
|
|
// UUID without invoking the store. The store is an errorAgentStore that
|
|
// returns a sentinel error on any call — if the fast path misbehaves and
|
|
// falls through to the DB lookup, that sentinel surfaces as a test failure.
|
|
func TestResolveAgentUUIDCached_CacheHitSkipsDBPath(t *testing.T) {
|
|
r := agent.NewRouter()
|
|
expectedUUID := uuid.New()
|
|
agentKey := "cached-agent"
|
|
|
|
// Prime the router cache via a resolver that returns the stub. Router.Get
|
|
// canonicalizes under `tenantID:agentKey` so a subsequent cache-aware
|
|
// lookup with the same (ctx, agentKey) resolves via the fast path.
|
|
r.SetResolver(func(_ context.Context, _ string) (agent.Agent, error) {
|
|
return &cacheHitStubAgent{id: agentKey, uid: expectedUUID}, nil
|
|
})
|
|
tenantID := uuid.New()
|
|
ctx := store.WithTenantID(context.Background(), tenantID)
|
|
if _, err := r.Get(ctx, agentKey); err != nil {
|
|
t.Fatalf("prime Router.Get: %v", err)
|
|
}
|
|
|
|
// Any store call is a test failure. If the fast path works, the store is
|
|
// never touched and no error surfaces.
|
|
stub := &errorAgentStore{err: errSentinelMiss}
|
|
|
|
got, err := resolveAgentUUIDCached(ctx, r, stub, agentKey)
|
|
if err != nil {
|
|
t.Fatalf("fast path returned error %v — expected cache hit without DB lookup", err)
|
|
}
|
|
if got != expectedUUID {
|
|
t.Errorf("fast path returned UUID %v, want %v", got, expectedUUID)
|
|
}
|
|
}
|