mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-16 20:25:55 +00:00
- config: align struct tag formatting in TelemetryConfig for readability - audio tests: modernize test patterns - cache, http, hooks, providers: small style and cleanup passes - No functional changes
202 lines
5.0 KiB
Go
202 lines
5.0 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// TestResolveChain_CacheHit verifies that resolveChain returns cached chains.
|
|
// Scenario 7: back-to-back same-tenant calls return the same cached slice.
|
|
func TestResolveChain_CacheHit(t *testing.T) {
|
|
fake := newFakeSecretsStore()
|
|
tool := &WebSearchTool{
|
|
secrets: fake,
|
|
cache: newWebCache(defaultCacheMaxEntries, defaultCacheTTL),
|
|
chainCache: newTenantChainCache(),
|
|
}
|
|
|
|
tid := uuid.New()
|
|
ctx := context.Background()
|
|
ctx = store.WithTenantID(ctx, tid)
|
|
|
|
// Set a provider key
|
|
fake.Set(ctx, "tools.web.brave.api_key", "test-key-brave")
|
|
|
|
// First call → resolves and caches
|
|
chain1 := tool.resolveChain(ctx)
|
|
|
|
// Second call → should hit cache
|
|
chain2 := tool.resolveChain(ctx)
|
|
|
|
if len(chain1) == 0 || len(chain2) == 0 {
|
|
t.Fatalf("expected non-empty chains, got %d and %d", len(chain1), len(chain2))
|
|
}
|
|
|
|
// Both should have same provider order
|
|
if len(chain1) != len(chain2) {
|
|
t.Errorf("chain length mismatch: %d vs %d", len(chain1), len(chain2))
|
|
}
|
|
|
|
for i := range chain1 {
|
|
if chain1[i].Name() != chain2[i].Name() {
|
|
t.Errorf("provider %d: %s vs %s", i, chain1[i].Name(), chain2[i].Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestResolveChain_TenantIsolation verifies that two tenants get independent chains.
|
|
// Scenario 8: switching tenant context yields different chains.
|
|
func TestResolveChain_TenantIsolation(t *testing.T) {
|
|
fake := newFakeSecretsStore()
|
|
tool := &WebSearchTool{
|
|
secrets: fake,
|
|
cache: newWebCache(defaultCacheMaxEntries, defaultCacheTTL),
|
|
chainCache: newTenantChainCache(),
|
|
}
|
|
|
|
tenantA := uuid.New()
|
|
tenantB := uuid.New()
|
|
|
|
// Setup tenant A: Brave key only
|
|
ctxA := context.Background()
|
|
ctxA = store.WithTenantID(ctxA, tenantA)
|
|
fake.Set(ctxA, "tools.web.brave.api_key", "test-key-brave-A")
|
|
|
|
// Setup tenant B: Exa key only
|
|
ctxB := context.Background()
|
|
ctxB = store.WithTenantID(ctxB, tenantB)
|
|
fake.Set(ctxB, "tools.web.exa.api_key", "test-key-exa-B")
|
|
|
|
// Resolve chains for each tenant
|
|
chainA := tool.resolveChain(ctxA)
|
|
chainB := tool.resolveChain(ctxB)
|
|
|
|
// Tenant A should have Brave
|
|
foundBraveInA := false
|
|
for _, p := range chainA {
|
|
if p.Name() == "brave" {
|
|
foundBraveInA = true
|
|
break
|
|
}
|
|
}
|
|
if !foundBraveInA {
|
|
t.Error("tenant A chain missing Brave")
|
|
}
|
|
|
|
// Tenant B should have Exa
|
|
foundExaInB := false
|
|
for _, p := range chainB {
|
|
if p.Name() == "exa" {
|
|
foundExaInB = true
|
|
break
|
|
}
|
|
}
|
|
if !foundExaInB {
|
|
t.Error("tenant B chain missing Exa")
|
|
}
|
|
|
|
// Tenant A should NOT have Exa
|
|
foundExaInA := false
|
|
for _, p := range chainA {
|
|
if p.Name() == "exa" {
|
|
foundExaInA = true
|
|
break
|
|
}
|
|
}
|
|
if foundExaInA {
|
|
t.Error("tenant A chain should not have Exa")
|
|
}
|
|
|
|
// Tenant B should NOT have Brave
|
|
foundBraveInB := false
|
|
for _, p := range chainB {
|
|
if p.Name() == "brave" {
|
|
foundBraveInB = true
|
|
break
|
|
}
|
|
}
|
|
if foundBraveInB {
|
|
t.Error("tenant B chain should not have Brave")
|
|
}
|
|
}
|
|
|
|
// TestResolveChain_CacheInvalidation verifies cache invalidation refreshes the chain.
|
|
func TestResolveChain_CacheInvalidation(t *testing.T) {
|
|
fake := newFakeSecretsStore()
|
|
tool := &WebSearchTool{
|
|
secrets: fake,
|
|
cache: newWebCache(defaultCacheMaxEntries, defaultCacheTTL),
|
|
chainCache: newTenantChainCache(),
|
|
}
|
|
|
|
tid := uuid.New()
|
|
ctx := context.Background()
|
|
ctx = store.WithTenantID(ctx, tid)
|
|
|
|
// Initial state: only DDG
|
|
chain1 := tool.resolveChain(ctx)
|
|
if len(chain1) != 1 || chain1[0].Name() != "duckduckgo" {
|
|
t.Fatalf("initial chain should be DDG only, got %v", chainNames(chain1))
|
|
}
|
|
|
|
// Add a provider key and invalidate cache
|
|
fake.Set(ctx, "tools.web.brave.api_key", "test-key-brave-new")
|
|
tool.chainCache.Invalidate(tid)
|
|
|
|
// Next resolve should pick up the new key
|
|
chain2 := tool.resolveChain(ctx)
|
|
if len(chain2) != 2 {
|
|
t.Errorf("after invalidation, expected 2 providers, got %d", len(chain2))
|
|
}
|
|
|
|
foundBrave := false
|
|
for _, p := range chain2 {
|
|
if p.Name() == "brave" {
|
|
foundBrave = true
|
|
break
|
|
}
|
|
}
|
|
if !foundBrave {
|
|
t.Error("after invalidation, Brave should be in chain")
|
|
}
|
|
}
|
|
|
|
// TestResolveChain_TTLCacheExpiry verifies that expired cache entries trigger refresh.
|
|
func TestResolveChain_TTLCacheExpiry(t *testing.T) {
|
|
fake := newFakeSecretsStore()
|
|
tool := &WebSearchTool{
|
|
secrets: fake,
|
|
cache: newWebCache(defaultCacheMaxEntries, defaultCacheTTL),
|
|
chainCache: newTenantChainCache(),
|
|
}
|
|
|
|
tid := uuid.New()
|
|
ctx := context.Background()
|
|
ctx = store.WithTenantID(ctx, tid)
|
|
|
|
// Initial state: DDG only
|
|
chain1 := tool.resolveChain(ctx)
|
|
if len(chain1) != 1 {
|
|
t.Fatalf("initial chain should be 1 provider, got %d", len(chain1))
|
|
}
|
|
|
|
// Manually expire the cache entry
|
|
tool.chainCache.entries[tid] = tenantChainEntry{
|
|
chain: chain1,
|
|
expires: time.Now().Add(-1 * time.Second), // expired
|
|
}
|
|
|
|
// Add provider and resolve again — should bypass expired cache
|
|
fake.Set(ctx, "tools.web.brave.api_key", "test-key-brave-after-ttl")
|
|
chain2 := tool.resolveChain(ctx)
|
|
|
|
if len(chain2) != 2 {
|
|
t.Errorf("after TTL expiry, expected 2 providers, got %d", len(chain2))
|
|
}
|
|
}
|