317 lines
6.8 KiB
Go
317 lines
6.8 KiB
Go
package cache
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestCacheKeyString(t *testing.T) {
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "abc123",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
|
||
|
|
keyStr := key.String()
|
||
|
|
assert.Contains(t, keyStr, "implementer")
|
||
|
|
assert.Contains(t, keyStr, "T1.1")
|
||
|
|
assert.Contains(t, keyStr, "abc123")
|
||
|
|
assert.Contains(t, keyStr, "claude-opus")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComputeHash(t *testing.T) {
|
||
|
|
data := map[string]interface{}{
|
||
|
|
"task": "T1.1",
|
||
|
|
"code": "package main",
|
||
|
|
}
|
||
|
|
|
||
|
|
hash1, err := ComputeHash(data)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotEmpty(t, hash1)
|
||
|
|
|
||
|
|
hash2, err := ComputeHash(data)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, hash1, hash2)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSetAndGet(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 100, 0)
|
||
|
|
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "abc123",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
|
||
|
|
result := map[string]interface{}{
|
||
|
|
"output": "implementation code",
|
||
|
|
"files": []string{"file1.go", "file2.go"},
|
||
|
|
}
|
||
|
|
|
||
|
|
err := cache.Set(key, result)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
|
||
|
|
retrieved, found, err := cache.Get(key)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.True(t, found)
|
||
|
|
assert.Equal(t, "implementation code", retrieved["output"])
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCacheMiss(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 100, 0)
|
||
|
|
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "abc123",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
|
||
|
|
retrieved, found, err := cache.Get(key)
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.False(t, found)
|
||
|
|
assert.Nil(t, retrieved)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInvalidate(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 100, 0)
|
||
|
|
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "abc123",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
|
||
|
|
cache.Set(key, map[string]interface{}{"output": "code"})
|
||
|
|
assert.Equal(t, 1, cache.GetSize())
|
||
|
|
|
||
|
|
cache.Invalidate(key)
|
||
|
|
assert.Equal(t, 0, cache.GetSize())
|
||
|
|
|
||
|
|
_, found, _ := cache.Get(key)
|
||
|
|
assert.False(t, found)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestClear(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 100, 0)
|
||
|
|
|
||
|
|
for i := 0; i < 10; i++ {
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: string(rune(48 + i)),
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
cache.Set(key, map[string]interface{}{"output": "code"})
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.Equal(t, 10, cache.GetSize())
|
||
|
|
|
||
|
|
cache.Clear()
|
||
|
|
assert.Equal(t, 0, cache.GetSize())
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetStats(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 100, 0)
|
||
|
|
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "abc123",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
|
||
|
|
cache.Set(key, map[string]interface{}{"output": "code"})
|
||
|
|
cache.Get(key) // Hit
|
||
|
|
|
||
|
|
stats := cache.GetStats()
|
||
|
|
assert.Equal(t, 1, stats["size"])
|
||
|
|
assert.Equal(t, 100, stats["max_size"])
|
||
|
|
assert.Equal(t, 1, stats["total_hits"])
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTTLExpiration(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 100, 100*time.Millisecond)
|
||
|
|
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "abc123",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
|
||
|
|
cache.Set(key, map[string]interface{}{"output": "code"})
|
||
|
|
|
||
|
|
// Should find immediately
|
||
|
|
_, found, _ := cache.Get(key)
|
||
|
|
assert.True(t, found)
|
||
|
|
|
||
|
|
// Wait for TTL to expire
|
||
|
|
time.Sleep(150 * time.Millisecond)
|
||
|
|
|
||
|
|
// Should not find after TTL
|
||
|
|
_, found, _ = cache.Get(key)
|
||
|
|
assert.False(t, found)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMaxSizeEviction(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 3, 0)
|
||
|
|
|
||
|
|
// Add 3 entries
|
||
|
|
for i := 0; i < 3; i++ {
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: string(rune(48 + i)),
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
cache.Set(key, map[string]interface{}{"output": "code"})
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.Equal(t, 3, cache.GetSize())
|
||
|
|
|
||
|
|
// Add 4th entry (should evict oldest)
|
||
|
|
key4 := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "3",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
cache.Set(key4, map[string]interface{}{"output": "code"})
|
||
|
|
|
||
|
|
// Size should still be 3
|
||
|
|
assert.Equal(t, 3, cache.GetSize())
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInvalidateByActivity(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 100, 0)
|
||
|
|
|
||
|
|
// Add implementer entries
|
||
|
|
for i := 0; i < 2; i++ {
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: string(rune(48 + i)),
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
cache.Set(key, map[string]interface{}{"output": "code"})
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add judge entries
|
||
|
|
for i := 0; i < 2; i++ {
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "judge",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: string(rune(48 + i)),
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
cache.Set(key, map[string]interface{}{"output": "verdict"})
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.Equal(t, 4, cache.GetSize())
|
||
|
|
|
||
|
|
// Invalidate implementer entries
|
||
|
|
cache.InvalidateByActivity("implementer")
|
||
|
|
|
||
|
|
assert.Equal(t, 2, cache.GetSize())
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestInvalidateByTask(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 100, 0)
|
||
|
|
|
||
|
|
// Add entries for T1.1
|
||
|
|
for i := 0; i < 2; i++ {
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: string(rune(48 + i)),
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
cache.Set(key, map[string]interface{}{"output": "code"})
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add entries for T1.2
|
||
|
|
for i := 0; i < 2; i++ {
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.2",
|
||
|
|
InputHash: string(rune(48 + i)),
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
cache.Set(key, map[string]interface{}{"output": "code"})
|
||
|
|
}
|
||
|
|
|
||
|
|
assert.Equal(t, 4, cache.GetSize())
|
||
|
|
|
||
|
|
// Invalidate T1.1 entries
|
||
|
|
cache.InvalidateByTask("T1.1")
|
||
|
|
|
||
|
|
assert.Equal(t, 2, cache.GetSize())
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetHitRate(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache := NewResultCache(tmpDir, 100, 0)
|
||
|
|
|
||
|
|
key1 := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "1",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
|
||
|
|
key2 := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "2",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
|
||
|
|
cache.Set(key1, map[string]interface{}{"output": "code"})
|
||
|
|
cache.Set(key2, map[string]interface{}{"output": "code"})
|
||
|
|
|
||
|
|
cache.Get(key1)
|
||
|
|
cache.Get(key1)
|
||
|
|
cache.Get(key2)
|
||
|
|
|
||
|
|
hitRate, count := cache.GetHitRate()
|
||
|
|
assert.Equal(t, 2, count)
|
||
|
|
assert.GreaterOrEqual(t, hitRate, 1.0)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPersistence(t *testing.T) {
|
||
|
|
tmpDir := t.TempDir()
|
||
|
|
cache1 := NewResultCache(tmpDir, 100, 0)
|
||
|
|
|
||
|
|
key := &CacheKey{
|
||
|
|
ActivityType: "implementer",
|
||
|
|
TaskID: "T1.1",
|
||
|
|
InputHash: "abc123",
|
||
|
|
ModelID: "claude-opus",
|
||
|
|
}
|
||
|
|
|
||
|
|
cache1.Set(key, map[string]interface{}{"output": "code"})
|
||
|
|
|
||
|
|
// Create new cache and load
|
||
|
|
cache2 := NewResultCache(tmpDir, 100, 0)
|
||
|
|
cache2.Load()
|
||
|
|
|
||
|
|
retrieved, found, _ := cache2.Get(key)
|
||
|
|
assert.True(t, found)
|
||
|
|
assert.Equal(t, "code", retrieved["output"])
|
||
|
|
}
|