amaniquery-agent / internal /cache /cache_test.go
Deployment Script
amaniquery-bot
Deploy AmaniQuery core stack to HF Space
f770e40
Raw History Blame Contribute Delete
9.77 kB
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)))
}
}