mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-26 02:19:51 +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
211 lines
5.1 KiB
Go
211 lines
5.1 KiB
Go
package tools
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TestTenantChainCache_TTLExpiry verifies that cached entries expire after TTL.
|
|
// Uses injected clock to avoid time.Sleep.
|
|
func TestTenantChainCache_TTLExpiry(t *testing.T) {
|
|
c := newTenantChainCache()
|
|
|
|
// Replace the cache's time function with a fake clock.
|
|
fakeNow := time.Unix(1000, 0)
|
|
c.now = func() time.Time { return fakeNow }
|
|
|
|
tid := uuid.New()
|
|
chain := []SearchProvider{&fakeSearchProvider{"brave"}}
|
|
|
|
// Set chain at t=1000
|
|
c.Set(tid, chain)
|
|
|
|
// Should be present at t=1000
|
|
got, ok := c.Get(tid)
|
|
if !ok || len(got) != 1 {
|
|
t.Error("expected cache hit at t=0")
|
|
}
|
|
|
|
// Move time forward 30 seconds (still within 60s TTL)
|
|
fakeNow = time.Unix(1030, 0)
|
|
got, ok = c.Get(tid)
|
|
if !ok || len(got) != 1 {
|
|
t.Error("expected cache hit at t+30s")
|
|
}
|
|
|
|
// Move time forward to 61 seconds (exceeds TTL)
|
|
fakeNow = time.Unix(1061, 0)
|
|
_, ok = c.Get(tid)
|
|
if ok {
|
|
t.Error("expected cache miss after TTL expiry")
|
|
}
|
|
}
|
|
|
|
// TestTenantChainCache_Invalidate verifies single-tenant invalidation.
|
|
func TestTenantChainCache_Invalidate(t *testing.T) {
|
|
c := newTenantChainCache()
|
|
c.now = func() time.Time { return time.Now() }
|
|
|
|
tid1, tid2 := uuid.New(), uuid.New()
|
|
chain1 := []SearchProvider{&fakeSearchProvider{"brave"}}
|
|
chain2 := []SearchProvider{&fakeSearchProvider{"exa"}}
|
|
|
|
c.Set(tid1, chain1)
|
|
c.Set(tid2, chain2)
|
|
|
|
// Verify both cached
|
|
if _, ok := c.Get(tid1); !ok {
|
|
t.Error("tid1 should be cached")
|
|
}
|
|
if _, ok := c.Get(tid2); !ok {
|
|
t.Error("tid2 should be cached")
|
|
}
|
|
|
|
// Invalidate only tid1
|
|
c.Invalidate(tid1)
|
|
|
|
// tid1 should be gone, tid2 should remain
|
|
if _, ok := c.Get(tid1); ok {
|
|
t.Error("tid1 should be evicted")
|
|
}
|
|
if _, ok := c.Get(tid2); !ok {
|
|
t.Error("tid2 should still be cached")
|
|
}
|
|
}
|
|
|
|
// TestTenantChainCache_InvalidateAll verifies full-cache invalidation.
|
|
func TestTenantChainCache_InvalidateAll(t *testing.T) {
|
|
c := newTenantChainCache()
|
|
c.now = func() time.Time { return time.Now() }
|
|
|
|
tid1, tid2, tid3 := uuid.New(), uuid.New(), uuid.New()
|
|
for _, tid := range []uuid.UUID{tid1, tid2, tid3} {
|
|
c.Set(tid, []SearchProvider{&fakeSearchProvider{"test"}})
|
|
}
|
|
|
|
// Verify all cached
|
|
for _, tid := range []uuid.UUID{tid1, tid2, tid3} {
|
|
if _, ok := c.Get(tid); !ok {
|
|
t.Errorf("expected %v cached", tid)
|
|
}
|
|
}
|
|
|
|
// Invalidate all
|
|
c.InvalidateAll()
|
|
|
|
// All should be gone
|
|
for _, tid := range []uuid.UUID{tid1, tid2, tid3} {
|
|
if _, ok := c.Get(tid); ok {
|
|
t.Errorf("expected %v evicted", tid)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestTenantChainCache_ConcurrentReaders verifies race-safe reads.
|
|
func TestTenantChainCache_ConcurrentReaders(t *testing.T) {
|
|
c := newTenantChainCache()
|
|
c.now = func() time.Time { return time.Now() }
|
|
|
|
tid := uuid.New()
|
|
chain := []SearchProvider{&fakeSearchProvider{"brave"}, &fakeSearchProvider{"exa"}}
|
|
c.Set(tid, chain)
|
|
|
|
// Spawn 10 concurrent readers
|
|
var wg sync.WaitGroup
|
|
for range 10 {
|
|
wg.Go(func() {
|
|
for range 100 {
|
|
got, ok := c.Get(tid)
|
|
if !ok || len(got) != 2 {
|
|
t.Errorf("concurrent read failed")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestTenantChainCache_ConcurrentMutations verifies race-safe writes.
|
|
func TestTenantChainCache_ConcurrentMutations(t *testing.T) {
|
|
c := newTenantChainCache()
|
|
c.now = func() time.Time { return time.Now() }
|
|
|
|
// Spawn concurrent writers for different tenants
|
|
var wg sync.WaitGroup
|
|
for i := range 5 {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
tid := uuid.New()
|
|
for j := range 10 {
|
|
c.Set(tid, []SearchProvider{&fakeSearchProvider{"brave"}})
|
|
c.Get(tid)
|
|
if j%3 == 0 {
|
|
c.Invalidate(tid)
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestTenantChainCache_SameSliceOnBackToBackCalls verifies idempotent reads.
|
|
func TestTenantChainCache_SameSliceOnBackToBackCalls(t *testing.T) {
|
|
c := newTenantChainCache()
|
|
c.now = func() time.Time { return time.Now() }
|
|
|
|
tid := uuid.New()
|
|
originalChain := []SearchProvider{&fakeSearchProvider{"brave"}}
|
|
c.Set(tid, originalChain)
|
|
|
|
// Back-to-back calls should return identical slice
|
|
chain1, ok1 := c.Get(tid)
|
|
chain2, ok2 := c.Get(tid)
|
|
|
|
if !ok1 || !ok2 {
|
|
t.Fatal("cache hits failed")
|
|
}
|
|
|
|
if len(chain1) != len(chain2) || len(chain1) != 1 {
|
|
t.Errorf("chain lengths differ: %d vs %d", len(chain1), len(chain2))
|
|
}
|
|
|
|
if chain1[0].Name() != chain2[0].Name() {
|
|
t.Errorf("provider names differ: %s vs %s", chain1[0].Name(), chain2[0].Name())
|
|
}
|
|
}
|
|
|
|
// TestTenantChainCache_InvalidateAfterRead verifies cache refresh on invalidation.
|
|
func TestTenantChainCache_InvalidateAfterRead(t *testing.T) {
|
|
c := newTenantChainCache()
|
|
c.now = func() time.Time { return time.Now() }
|
|
|
|
tid := uuid.New()
|
|
|
|
// Set chain v1
|
|
c.Set(tid, []SearchProvider{&fakeSearchProvider{"brave"}})
|
|
got1, ok1 := c.Get(tid)
|
|
if !ok1 {
|
|
t.Fatal("initial read failed")
|
|
}
|
|
if got1[0].Name() != "brave" {
|
|
t.Errorf("expected brave, got %s", got1[0].Name())
|
|
}
|
|
|
|
// Invalidate and set new chain v2
|
|
c.Invalidate(tid)
|
|
c.Set(tid, []SearchProvider{&fakeSearchProvider{"exa"}})
|
|
got2, ok2 := c.Get(tid)
|
|
if !ok2 {
|
|
t.Fatal("read after invalidate failed")
|
|
}
|
|
if got2[0].Name() != "exa" {
|
|
t.Errorf("expected exa, got %s", got2[0].Name())
|
|
}
|
|
}
|