mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-11 02:10:51 +00:00
0d3230b2bf
Add optional Redis cache support via `go build -tags redis`, following the same paired-stub pattern as OTel and Tailscale. The Cache[V] interface is unchanged; Redis and in-memory implementations are injected at startup without altering usage logic. - Add RedisCache[V] implementation with JSON serialization, fail-open on errors - Add gateway_redis.go / gateway_redis_noop.go paired wiring files - Refactor GroupWriterCache and ContextFileInterceptor to accept injected caches - Add GOCLAW_REDIS_DSN env var, docker-compose.redis.yml overlay - Update Dockerfile and GitHub Actions with ENABLE_REDIS build arg - Add Redis variant to CI matrix (5 variants: latest, otel, tsnet, redis, full)
143 lines
3.1 KiB
Go
143 lines
3.1 KiB
Go
//go:build redis
|
|
|
|
package cache
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
func testRedisClient(t *testing.T) *redis.Client {
|
|
t.Helper()
|
|
dsn := os.Getenv("REDIS_TEST_DSN")
|
|
if dsn == "" {
|
|
dsn = "redis://localhost:6379/15" // use DB 15 for tests
|
|
}
|
|
client, err := NewRedisClient(dsn)
|
|
if err != nil {
|
|
t.Skipf("Redis not available: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
client.FlushDB(context.Background())
|
|
client.Close()
|
|
})
|
|
return client
|
|
}
|
|
|
|
func TestRedisCache_GetSet(t *testing.T) {
|
|
client := testRedisClient(t)
|
|
c := NewRedisCache[string](client, "test:getset")
|
|
ctx := context.Background()
|
|
|
|
// miss
|
|
_, ok := c.Get(ctx, "k1")
|
|
if ok {
|
|
t.Fatal("expected miss")
|
|
}
|
|
|
|
// hit
|
|
c.Set(ctx, "k1", "hello", time.Minute)
|
|
v, ok := c.Get(ctx, "k1")
|
|
if !ok || v != "hello" {
|
|
t.Fatalf("expected hello, got %q ok=%v", v, ok)
|
|
}
|
|
}
|
|
|
|
func TestRedisCache_TTLExpiry(t *testing.T) {
|
|
client := testRedisClient(t)
|
|
c := NewRedisCache[int](client, "test:ttl")
|
|
ctx := context.Background()
|
|
|
|
c.Set(ctx, "k1", 42, 100*time.Millisecond)
|
|
v, ok := c.Get(ctx, "k1")
|
|
if !ok || v != 42 {
|
|
t.Fatalf("expected 42, got %d ok=%v", v, ok)
|
|
}
|
|
|
|
time.Sleep(150 * time.Millisecond)
|
|
_, ok = c.Get(ctx, "k1")
|
|
if ok {
|
|
t.Fatal("expected miss after TTL")
|
|
}
|
|
}
|
|
|
|
func TestRedisCache_Delete(t *testing.T) {
|
|
client := testRedisClient(t)
|
|
c := NewRedisCache[string](client, "test:del")
|
|
ctx := context.Background()
|
|
|
|
c.Set(ctx, "k1", "v1", time.Minute)
|
|
c.Delete(ctx, "k1")
|
|
|
|
_, ok := c.Get(ctx, "k1")
|
|
if ok {
|
|
t.Fatal("expected miss after delete")
|
|
}
|
|
}
|
|
|
|
func TestRedisCache_DeleteByPrefix(t *testing.T) {
|
|
client := testRedisClient(t)
|
|
c := NewRedisCache[string](client, "test:prefix")
|
|
ctx := context.Background()
|
|
|
|
c.Set(ctx, "agent:1:a", "v1", time.Minute)
|
|
c.Set(ctx, "agent:1:b", "v2", time.Minute)
|
|
c.Set(ctx, "agent:2:a", "v3", time.Minute)
|
|
|
|
c.DeleteByPrefix(ctx, "agent:1:")
|
|
|
|
if _, ok := c.Get(ctx, "agent:1:a"); ok {
|
|
t.Fatal("agent:1:a should be deleted")
|
|
}
|
|
if _, ok := c.Get(ctx, "agent:1:b"); ok {
|
|
t.Fatal("agent:1:b should be deleted")
|
|
}
|
|
if _, ok := c.Get(ctx, "agent:2:a"); !ok {
|
|
t.Fatal("agent:2:a should still exist")
|
|
}
|
|
}
|
|
|
|
func TestRedisCache_Clear(t *testing.T) {
|
|
client := testRedisClient(t)
|
|
c := NewRedisCache[int](client, "test:clear")
|
|
ctx := context.Background()
|
|
|
|
c.Set(ctx, "a", 1, time.Minute)
|
|
c.Set(ctx, "b", 2, time.Minute)
|
|
c.Clear(ctx)
|
|
|
|
if _, ok := c.Get(ctx, "a"); ok {
|
|
t.Fatal("expected miss after clear")
|
|
}
|
|
if _, ok := c.Get(ctx, "b"); ok {
|
|
t.Fatal("expected miss after clear")
|
|
}
|
|
}
|
|
|
|
// TestRedisCache_StructRoundtrip verifies JSON serialization of complex types.
|
|
type testStruct struct {
|
|
Name string `json:"name"`
|
|
Age int `json:"age"`
|
|
}
|
|
|
|
func TestRedisCache_StructRoundtrip(t *testing.T) {
|
|
client := testRedisClient(t)
|
|
c := NewRedisCache[[]testStruct](client, "test:struct")
|
|
ctx := context.Background()
|
|
|
|
data := []testStruct{{Name: "Alice", Age: 30}, {Name: "Bob", Age: 25}}
|
|
c.Set(ctx, "users", data, time.Minute)
|
|
|
|
got, ok := c.Get(ctx, "users")
|
|
if !ok {
|
|
t.Fatal("expected hit")
|
|
}
|
|
if len(got) != 2 || got[0].Name != "Alice" || got[1].Age != 25 {
|
|
t.Fatalf("roundtrip mismatch: %+v", got)
|
|
}
|
|
}
|