| package cache |
|
|
| import ( |
| "context" |
| "testing" |
| "time" |
|
|
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| ) |
|
|
| func defaultCacheConfig() config.ResponseCacheConfig { |
| return config.ResponseCacheConfig{ |
| Enabled: true, |
| TTLSeconds: 300, |
| MaxMemoryEntries: 100, |
| MaxResponseSizeKB: 512, |
| ExcludeStreaming: true, |
| RedisKeyPrefix: "test", |
| } |
| } |
|
|
| func TestMemoryResponseCache_SetGet(t *testing.T) { |
| cfg := defaultCacheConfig() |
| c := newMemoryResponseCache(cfg) |
| defer c.Close() |
|
|
| ctx := context.Background() |
| resp := &CachedResponse{ |
| StatusCode: 200, |
| Body: []byte(`{"result":"ok"}`), |
| Model: "test-model", |
| } |
|
|
| if err := c.Set(ctx, "key1", resp, 5*time.Minute); err != nil { |
| t.Fatalf("Set failed: %v", err) |
| } |
|
|
| got, ok := c.Get(ctx, "key1") |
| if !ok { |
| t.Fatal("expected cache hit") |
| } |
| if got.StatusCode != 200 { |
| t.Fatalf("expected status 200, got %d", got.StatusCode) |
| } |
| if string(got.Body) != `{"result":"ok"}` { |
| t.Fatalf("unexpected body: %s", got.Body) |
| } |
| } |
|
|
| func TestMemoryResponseCache_Miss(t *testing.T) { |
| cfg := defaultCacheConfig() |
| c := newMemoryResponseCache(cfg) |
| defer c.Close() |
|
|
| _, ok := c.Get(context.Background(), "nonexistent") |
| if ok { |
| t.Fatal("expected cache miss") |
| } |
| } |
|
|
| func TestMemoryResponseCache_Expiry(t *testing.T) { |
| cfg := defaultCacheConfig() |
| c := newMemoryResponseCache(cfg) |
| defer c.Close() |
|
|
| ctx := context.Background() |
| resp := &CachedResponse{StatusCode: 200, Body: []byte("test")} |
| if err := c.Set(ctx, "expiring", resp, 1*time.Millisecond); err != nil { |
| t.Fatalf("Set failed: %v", err) |
| } |
|
|
| time.Sleep(5 * time.Millisecond) |
| _, ok := c.Get(ctx, "expiring") |
| if ok { |
| t.Fatal("expected cache miss after TTL expiry") |
| } |
| } |
|
|
| func TestMemoryResponseCache_Eviction(t *testing.T) { |
| cfg := defaultCacheConfig() |
| cfg.MaxMemoryEntries = 2 |
| c := newMemoryResponseCache(cfg) |
| defer c.Close() |
|
|
| ctx := context.Background() |
| resp := &CachedResponse{StatusCode: 200, Body: []byte("data")} |
| c.Set(ctx, "a", resp, time.Hour) |
| c.Set(ctx, "b", resp, time.Hour) |
| c.Set(ctx, "c", resp, time.Hour) |
|
|
| _, ok := c.Get(ctx, "a") |
| if ok { |
| t.Fatal("expected 'a' to be evicted") |
| } |
| _, ok = c.Get(ctx, "c") |
| if !ok { |
| t.Fatal("expected 'c' to be present") |
| } |
| } |
|
|
| func TestMemoryResponseCache_Stats(t *testing.T) { |
| cfg := defaultCacheConfig() |
| c := newMemoryResponseCache(cfg) |
| defer c.Close() |
|
|
| ctx := context.Background() |
| c.Get(ctx, "miss1") |
| c.Get(ctx, "miss2") |
|
|
| resp := &CachedResponse{StatusCode: 200, Body: []byte("x")} |
| c.Set(ctx, "hit", resp, time.Hour) |
| c.Get(ctx, "hit") |
|
|
| stats := c.Stats() |
| if stats.Hits != 1 { |
| t.Fatalf("expected 1 hit, got %d", stats.Hits) |
| } |
| if stats.Misses != 2 { |
| t.Fatalf("expected 2 misses, got %d", stats.Misses) |
| } |
| if stats.Size != 1 { |
| t.Fatalf("expected size 1, got %d", stats.Size) |
| } |
| } |
|
|
| func TestMemoryResponseCache_Delete(t *testing.T) { |
| cfg := defaultCacheConfig() |
| c := newMemoryResponseCache(cfg) |
| defer c.Close() |
|
|
| ctx := context.Background() |
| resp := &CachedResponse{StatusCode: 200, Body: []byte("x")} |
| c.Set(ctx, "k1", resp, time.Hour) |
| c.Set(ctx, "k2", resp, time.Hour) |
|
|
| c.Delete(ctx, "*") |
|
|
| _, ok1 := c.Get(ctx, "k1") |
| _, ok2 := c.Get(ctx, "k2") |
| if ok1 || ok2 { |
| t.Fatal("expected all entries deleted") |
| } |
| } |
|
|
| func TestResponseCacheKey(t *testing.T) { |
| k1 := ResponseCacheKey("model-a", []byte("body1")) |
| k2 := ResponseCacheKey("model-a", []byte("body1")) |
| k3 := ResponseCacheKey("model-b", []byte("body1")) |
|
|
| if k1 != k2 { |
| t.Fatal("same inputs should produce same key") |
| } |
| if k1 == k3 { |
| t.Fatal("different models should produce different keys") |
| } |
| } |
|
|
| func TestShouldCache(t *testing.T) { |
| cfg := defaultCacheConfig() |
|
|
| if !ShouldCache(cfg, "gemini-pro", false, 1024) { |
| t.Fatal("should cache normal response") |
| } |
|
|
| if ShouldCache(cfg, "gemini-pro", true, 1024) { |
| t.Fatal("should not cache streaming when exclude-streaming=true") |
| } |
|
|
| |
| if ShouldCache(cfg, "gemini-pro", false, 1024*1024) { |
| t.Fatal("should not cache oversized response") |
| } |
|
|
| |
| disabledCfg := cfg |
| disabledCfg.Enabled = false |
| if ShouldCache(disabledCfg, "gemini-pro", false, 1024) { |
| t.Fatal("should not cache when disabled") |
| } |
|
|
| |
| cfg.ExcludeModels = []string{"*-preview"} |
| if ShouldCache(cfg, "gemini-pro-preview", false, 1024) { |
| t.Fatal("should not cache excluded model") |
| } |
| } |
|
|
| func TestNewResponseCache_MemoryFallback(t *testing.T) { |
| cfg := defaultCacheConfig() |
| c := NewResponseCache(cfg, nil) |
| defer c.Close() |
|
|
| ctx := context.Background() |
| resp := &CachedResponse{StatusCode: 200, Body: []byte("test")} |
| if err := c.Set(ctx, "k", resp, time.Minute); err != nil { |
| t.Fatalf("Set failed: %v", err) |
| } |
| got, ok := c.Get(ctx, "k") |
| if !ok || got.StatusCode != 200 { |
| t.Fatal("expected cache hit with status 200") |
| } |
| } |
|
|
| func BenchmarkMemoryResponseCache_SetGet(b *testing.B) { |
| cfg := defaultCacheConfig() |
| cfg.MaxMemoryEntries = 100000 |
| c := newMemoryResponseCache(cfg) |
| defer c.Close() |
|
|
| ctx := context.Background() |
| resp := &CachedResponse{StatusCode: 200, Body: []byte(`{"choices":[{"message":{"content":"hello"}}]}`)} |
|
|
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| key := ResponseCacheKey("bench-model", []byte(`{"model":"bench-model","messages":[{"role":"user","content":"hi"}]}`)) |
| c.Set(ctx, key, resp, time.Hour) |
| c.Get(ctx, key) |
| } |
| } |
|
|
| func BenchmarkResponseCacheKey(b *testing.B) { |
| body := []byte(`{"model":"gemini-2.5-pro","messages":[{"role":"user","content":"explain quantum computing"}]}`) |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| ResponseCacheKey("gemini-2.5-pro", body) |
| } |
| } |
|
|