Shinwayy / internal /cache /response_cache_test.go
Shinway Deploy
track binary files with git-lfs
db492de
Raw
History Blame Contribute Delete
5.63 kB
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) // should evict "a"
_, 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") // miss
c.Get(ctx, "miss2") // miss
resp := &CachedResponse{StatusCode: 200, Body: []byte("x")}
c.Set(ctx, "hit", resp, time.Hour)
c.Get(ctx, "hit") // 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")
}
// Exceeded max response size
if ShouldCache(cfg, "gemini-pro", false, 1024*1024) {
t.Fatal("should not cache oversized response")
}
// Disabled config
disabledCfg := cfg
disabledCfg.Enabled = false
if ShouldCache(disabledCfg, "gemini-pro", false, 1024) {
t.Fatal("should not cache when disabled")
}
// Excluded model
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)
}
}