package cache_test import ( "context" "strings" "testing" "time" "github.com/AmaniQuery/amaniquery/internal/cache" ) // TestLRUCache_SetGet verifies basic set/get operations func TestLRUCache_SetGet(t *testing.T) { lru := cache.NewLRUCache(100) // Test set and get lru.Set("key1", []byte("value1"), time.Hour) value, found := lru.Get("key1") if !found { t.Fatal("Expected to find key1") } if string(value) != "value1" { t.Errorf("Expected 'value1', got '%s'", string(value)) } // Test missing key _, found = lru.Get("nonexistent") if found { t.Error("Expected not to find nonexistent key") } } // TestLRUCache_Update verifies updating existing keys func TestLRUCache_Update(t *testing.T) { lru := cache.NewLRUCache(100) lru.Set("key1", []byte("original"), time.Hour) lru.Set("key1", []byte("updated"), time.Hour) value, found := lru.Get("key1") if !found { t.Fatal("Expected to find key1") } if string(value) != "updated" { t.Errorf("Expected 'updated', got '%s'", string(value)) } } // TestLRUCache_Eviction verifies LRU eviction when over capacity func TestLRUCache_Eviction(t *testing.T) { lru := cache.NewLRUCache(3) // Fill cache to capacity lru.Set("key1", []byte("value1"), time.Hour) lru.Set("key2", []byte("value2"), time.Hour) lru.Set("key3", []byte("value3"), time.Hour) // Access key1 to make it recently used lru.Get("key1") // Add new key, should evict key2 (least recently used) lru.Set("key4", []byte("value4"), time.Hour) // key2 should be evicted _, found := lru.Get("key2") if found { t.Error("Expected key2 to be evicted") } // key1 should still exist (was accessed recently) _, found = lru.Get("key1") if !found { t.Error("Expected key1 to still exist") } // key3 and key4 should exist _, found = lru.Get("key3") if !found { t.Error("Expected key3 to exist") } _, found = lru.Get("key4") if !found { t.Error("Expected key4 to exist") } } // TestLRUCache_Delete verifies deletion operations func TestLRUCache_Delete(t *testing.T) { lru := cache.NewLRUCache(100) lru.Set("key1", []byte("value1"), time.Hour) lru.Set("key2", []byte("value2"), time.Hour) lru.Delete("key1") _, found := lru.Get("key1") if found { t.Error("Expected key1 to be deleted") } // key2 should still exist _, found = lru.Get("key2") if !found { t.Error("Expected key2 to still exist") } } // TestLRUCache_DeleteNonexistent verifies deleting nonexistent keys doesn't panic func TestLRUCache_DeleteNonexistent(t *testing.T) { lru := cache.NewLRUCache(100) // Should not panic lru.Delete("nonexistent") } // TestCacheMissError tests the error type func TestCacheMissError(t *testing.T) { err := cache.ErrCacheMiss if err.Error() != "cache miss" { t.Errorf("Expected 'cache miss', got '%s'", err.Error()) } } // TestMultiTierCache_LocalOnly tests cache without Redis connection func TestMultiTierCache_LocalOnly(t *testing.T) { // Create cache with invalid Redis URL to ensure Redis is not used cfg := cache.Config{ RedisURL: "redis://invalid:6379", // Will fail connection LocalSize: 100, TTL: time.Hour, MaxRetries: 1, PoolSize: 1, } c, err := cache.New(cfg) if err != nil { t.Fatalf("Failed to create cache: %v", err) } defer c.Close() ctx := context.Background() // Set and get should work with local cache err = c.Set(ctx, "key1", []byte("value1"), 0) if err != nil { t.Fatalf("Set failed: %v", err) } value, err := c.Get(ctx, "key1") if err != nil { t.Fatalf("Get failed: %v", err) } if string(value) != "value1" { t.Errorf("Expected 'value1', got '%s'", string(value)) } } // TestMultiTierCache_CacheMiss tests cache miss behavior func TestMultiTierCache_CacheMiss(t *testing.T) { cfg := cache.Config{ RedisURL: "redis://invalid:6379", LocalSize: 100, TTL: time.Hour, MaxRetries: 1, PoolSize: 1, } c, err := cache.New(cfg) if err != nil { t.Fatalf("Failed to create cache: %v", err) } defer c.Close() ctx := context.Background() _, err = c.Get(ctx, "nonexistent") if err == nil { t.Error("Expected cache miss error") } } // TestMultiTierCache_Delete tests deletion func TestMultiTierCache_Delete(t *testing.T) { cfg := cache.Config{ RedisURL: "redis://invalid:6379", LocalSize: 100, TTL: time.Hour, MaxRetries: 1, PoolSize: 1, } c, err := cache.New(cfg) if err != nil { t.Fatalf("Failed to create cache: %v", err) } defer c.Close() ctx := context.Background() // Set then delete _ = c.Set(ctx, "key1", []byte("value1"), 0) err = c.Delete(ctx, "key1") if err != nil { t.Fatalf("Delete failed: %v", err) } // Should be gone _, err = c.Get(ctx, "key1") if err == nil { t.Error("Expected cache miss after delete") } } // TestMultiTierCache_JSON tests JSON operations func TestMultiTierCache_JSON(t *testing.T) { cfg := cache.Config{ RedisURL: "redis://invalid:6379", LocalSize: 100, TTL: time.Hour, MaxRetries: 1, PoolSize: 1, } c, err := cache.New(cfg) if err != nil { t.Fatalf("Failed to create cache: %v", err) } defer c.Close() ctx := context.Background() type testData struct { Name string `json:"name"` Value int `json:"value"` } original := testData{Name: "test", Value: 42} err = c.SetJSON(ctx, "json-key", original, 0) if err != nil { t.Fatalf("SetJSON failed: %v", err) } var result testData err = c.GetJSON(ctx, "json-key", &result) if err != nil { t.Fatalf("GetJSON failed: %v", err) } if result.Name != original.Name || result.Value != original.Value { t.Errorf("JSON mismatch: expected %+v, got %+v", original, result) } } // TestMultiTierCache_Metrics tests metrics tracking func TestMultiTierCache_Metrics(t *testing.T) { cfg := cache.Config{ RedisURL: "redis://invalid:6379", LocalSize: 100, TTL: time.Hour, MaxRetries: 1, PoolSize: 1, } c, err := cache.New(cfg) if err != nil { t.Fatalf("Failed to create cache: %v", err) } defer c.Close() ctx := context.Background() // Set and get to generate metrics _ = c.Set(ctx, "key1", []byte("value1"), 0) _, _ = c.Get(ctx, "key1") // Hit _, _ = c.Get(ctx, "key2") // Miss metrics := c.GetMetrics() if metrics.LocalHits != 1 { t.Errorf("Expected 1 local hit, got %d", metrics.LocalHits) } if metrics.LocalMisses != 1 { t.Errorf("Expected 1 local miss, got %d", metrics.LocalMisses) } } // TestLRUCache_TTLExpiry verifies entries are not served past their TTL // (regression guard for stale L1 data outliving the Redis expiry). func TestLRUCache_TTLExpiry(t *testing.T) { lru := cache.NewLRUCache(10) lru.Set("key1", []byte("value1"), 50*time.Millisecond) lru.Set("key2", []byte("value2"), time.Hour) // Immediately readable if _, found := lru.Get("key1"); !found { t.Fatal("Expected key1 to be present before expiry") } time.Sleep(60 * time.Millisecond) if _, found := lru.Get("key1"); found { t.Error("Expected key1 to have expired") } if _, found := lru.Get("key2"); !found { t.Error("Expected key2 (long TTL) to still be present") } } // TestLRUCache_DeletePrefix verifies prefix-based eviction removes only the // matching keys (regression guard for pattern/prefix invalidation). func TestLRUCache_DeletePrefix(t *testing.T) { lru := cache.NewLRUCache(10) lru.Set("L1:nbo:rag:a", []byte("1"), time.Hour) lru.Set("L1:nbo:rag:b", []byte("2"), time.Hour) lru.Set("L1:nbo:legal:c", []byte("3"), time.Hour) lru.DeletePrefix("L1:nbo:rag:") if _, found := lru.Get("L1:nbo:rag:a"); found { t.Error("Expected rag:a to be deleted") } if _, found := lru.Get("L1:nbo:rag:b"); found { t.Error("Expected rag:b to be deleted") } if _, found := lru.Get("L1:nbo:legal:c"); !found { t.Error("Expected non-matching key to survive") } } // TestMultiTierCache_DeleteByPrefixPattern verifies prefix invalidation matches // readable NormalizeKey keys (regression guard: hashed keys could never be // matched by prefix patterns, silently no-op'ing admin invalidations). func TestMultiTierCache_DeleteByPrefixPattern(t *testing.T) { cfg := cache.Config{ RedisURL: "redis://invalid:6379", LocalSize: 100, TTL: time.Hour, MaxRetries: 1, PoolSize: 1, KeyPrefix: "L1:nbo:", } c, err := cache.New(cfg) if err != nil { t.Fatalf("Failed to create cache: %v", err) } defer c.Close() ctx := context.Background() ragKey := c.NormalizeKey("rag", "who owns the land registry") legalKey := c.NormalizeKey("legal", "statute of limitations kenya") if !strings.HasPrefix(ragKey, "L1:nbo:rag:") { t.Fatalf("expected readable key prefix, got %q", ragKey) } if err := c.Set(ctx, ragKey, []byte("rag-answer"), time.Hour); err != nil { t.Fatalf("Set failed: %v", err) } if err := c.Set(ctx, legalKey, []byte("legal-answer"), time.Hour); err != nil { t.Fatalf("Set failed: %v", err) } if err := c.DeleteByPrefix(ctx, "rag:"); err != nil { t.Fatalf("DeleteByPrefix failed: %v", err) } if _, err := c.Get(ctx, ragKey); err == nil { t.Error("Expected rag key to be invalidated by prefix") } if _, err := c.Get(ctx, legalKey); err != nil { t.Error("Expected legal key to survive prefix invalidation") } } // BenchmarkLRUCache_Set benchmarks LRU set operations func BenchmarkLRUCache_Set(b *testing.B) { lru := cache.NewLRUCache(10000) b.ResetTimer() for i := 0; i < b.N; i++ { lru.Set("key"+string(rune(i%1000)), []byte("value"), time.Hour) } } // BenchmarkLRUCache_Get benchmarks LRU get operations func BenchmarkLRUCache_Get(b *testing.B) { lru := cache.NewLRUCache(10000) // Pre-populate for i := 0; i < 1000; i++ { lru.Set("key"+string(rune(i)), []byte("value"), time.Hour) } b.ResetTimer() for i := 0; i < b.N; i++ { lru.Get("key" + string(rune(i%1000))) } }