go-agentic/registry_redis_test.go
Snider 6970da5c49 refactor: apply go fix modernizers for Go 1.26
Automated fixes: interface{} → any, range-over-int, t.Context(),
wg.Go(), strings.SplitSeq, strings.Builder, slices.Contains,
maps helpers, min/max builtins.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-22 21:00:17 +00:00

327 lines
8.6 KiB
Go

package agentic
import (
"context"
"fmt"
"sort"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newTestRedisRegistry creates a RedisRegistry with a unique prefix for test isolation.
// Skips the test if Redis is unreachable.
func newTestRedisRegistry(t *testing.T) *RedisRegistry {
t.Helper()
prefix := fmt.Sprintf("test_reg_%d", time.Now().UnixNano())
reg, err := NewRedisRegistry(testRedisAddr,
WithRegistryRedisPrefix(prefix),
WithRegistryTTL(5*time.Minute),
)
if err != nil {
t.Skipf("Redis unavailable at %s: %v", testRedisAddr, err)
}
t.Cleanup(func() {
ctx := context.Background()
_ = reg.FlushPrefix(ctx)
_ = reg.Close()
})
return reg
}
// --- Register tests ---
func TestRedisRegistry_Register_Good(t *testing.T) {
reg := newTestRedisRegistry(t)
err := reg.Register(AgentInfo{
ID: "agent-1",
Name: "Test Agent",
Capabilities: []string{"go", "testing"},
Status: AgentAvailable,
MaxLoad: 5,
})
require.NoError(t, err)
got, err := reg.Get("agent-1")
require.NoError(t, err)
assert.Equal(t, "agent-1", got.ID)
assert.Equal(t, "Test Agent", got.Name)
assert.Equal(t, []string{"go", "testing"}, got.Capabilities)
assert.Equal(t, AgentAvailable, got.Status)
assert.Equal(t, 5, got.MaxLoad)
}
func TestRedisRegistry_Register_Good_Overwrite(t *testing.T) {
reg := newTestRedisRegistry(t)
_ = reg.Register(AgentInfo{ID: "agent-1", Name: "Original", MaxLoad: 3})
err := reg.Register(AgentInfo{ID: "agent-1", Name: "Updated", MaxLoad: 10})
require.NoError(t, err)
got, err := reg.Get("agent-1")
require.NoError(t, err)
assert.Equal(t, "Updated", got.Name)
assert.Equal(t, 10, got.MaxLoad)
}
func TestRedisRegistry_Register_Bad_EmptyID(t *testing.T) {
reg := newTestRedisRegistry(t)
err := reg.Register(AgentInfo{ID: "", Name: "No ID"})
require.Error(t, err)
assert.Contains(t, err.Error(), "agent ID is required")
}
// --- Deregister tests ---
func TestRedisRegistry_Deregister_Good(t *testing.T) {
reg := newTestRedisRegistry(t)
_ = reg.Register(AgentInfo{ID: "agent-1", Name: "To Remove"})
err := reg.Deregister("agent-1")
require.NoError(t, err)
_, err = reg.Get("agent-1")
require.Error(t, err)
}
func TestRedisRegistry_Deregister_Bad_NotFound(t *testing.T) {
reg := newTestRedisRegistry(t)
err := reg.Deregister("nonexistent")
require.Error(t, err)
assert.Contains(t, err.Error(), "agent not found")
}
// --- Get tests ---
func TestRedisRegistry_Get_Good(t *testing.T) {
reg := newTestRedisRegistry(t)
now := time.Now().UTC().Truncate(time.Millisecond)
_ = reg.Register(AgentInfo{
ID: "agent-1",
Name: "Getter",
Status: AgentBusy,
CurrentLoad: 2,
MaxLoad: 5,
LastHeartbeat: now,
})
got, err := reg.Get("agent-1")
require.NoError(t, err)
assert.Equal(t, AgentBusy, got.Status)
assert.Equal(t, 2, got.CurrentLoad)
assert.WithinDuration(t, now, got.LastHeartbeat, time.Millisecond)
}
func TestRedisRegistry_Get_Bad_NotFound(t *testing.T) {
reg := newTestRedisRegistry(t)
_, err := reg.Get("nonexistent")
require.Error(t, err)
assert.Contains(t, err.Error(), "agent not found")
}
func TestRedisRegistry_Get_Good_ReturnsCopy(t *testing.T) {
reg := newTestRedisRegistry(t)
_ = reg.Register(AgentInfo{ID: "agent-1", Name: "Original", CurrentLoad: 1})
got, _ := reg.Get("agent-1")
got.CurrentLoad = 99
got.Name = "Tampered"
// Re-read — should be unchanged (deserialized from Redis).
again, _ := reg.Get("agent-1")
assert.Equal(t, "Original", again.Name)
assert.Equal(t, 1, again.CurrentLoad)
}
// --- List tests ---
func TestRedisRegistry_List_Good_Empty(t *testing.T) {
reg := newTestRedisRegistry(t)
agents := reg.List()
assert.Empty(t, agents)
}
func TestRedisRegistry_List_Good_Multiple(t *testing.T) {
reg := newTestRedisRegistry(t)
_ = reg.Register(AgentInfo{ID: "a", Name: "Alpha"})
_ = reg.Register(AgentInfo{ID: "b", Name: "Beta"})
_ = reg.Register(AgentInfo{ID: "c", Name: "Charlie"})
agents := reg.List()
assert.Len(t, agents, 3)
// Sort by ID for deterministic assertion.
sort.Slice(agents, func(i, j int) bool { return agents[i].ID < agents[j].ID })
assert.Equal(t, "a", agents[0].ID)
assert.Equal(t, "b", agents[1].ID)
assert.Equal(t, "c", agents[2].ID)
}
// --- Heartbeat tests ---
func TestRedisRegistry_Heartbeat_Good(t *testing.T) {
reg := newTestRedisRegistry(t)
past := time.Now().UTC().Add(-5 * time.Minute)
_ = reg.Register(AgentInfo{
ID: "agent-1",
Status: AgentAvailable,
LastHeartbeat: past,
})
err := reg.Heartbeat("agent-1")
require.NoError(t, err)
got, _ := reg.Get("agent-1")
assert.True(t, got.LastHeartbeat.After(past))
assert.Equal(t, AgentAvailable, got.Status)
}
func TestRedisRegistry_Heartbeat_Good_RecoverFromOffline(t *testing.T) {
reg := newTestRedisRegistry(t)
_ = reg.Register(AgentInfo{
ID: "agent-1",
Status: AgentOffline,
})
err := reg.Heartbeat("agent-1")
require.NoError(t, err)
got, _ := reg.Get("agent-1")
assert.Equal(t, AgentAvailable, got.Status)
}
func TestRedisRegistry_Heartbeat_Good_BusyStaysBusy(t *testing.T) {
reg := newTestRedisRegistry(t)
_ = reg.Register(AgentInfo{
ID: "agent-1",
Status: AgentBusy,
})
err := reg.Heartbeat("agent-1")
require.NoError(t, err)
got, _ := reg.Get("agent-1")
assert.Equal(t, AgentBusy, got.Status)
}
func TestRedisRegistry_Heartbeat_Bad_NotFound(t *testing.T) {
reg := newTestRedisRegistry(t)
err := reg.Heartbeat("nonexistent")
require.Error(t, err)
assert.Contains(t, err.Error(), "agent not found")
}
// --- Reap tests ---
func TestRedisRegistry_Reap_Good_StaleAgent(t *testing.T) {
reg := newTestRedisRegistry(t)
stale := time.Now().UTC().Add(-10 * time.Minute)
fresh := time.Now().UTC()
_ = reg.Register(AgentInfo{ID: "stale-1", Status: AgentAvailable, LastHeartbeat: stale})
_ = reg.Register(AgentInfo{ID: "fresh-1", Status: AgentAvailable, LastHeartbeat: fresh})
reaped := reg.Reap(5 * time.Minute)
assert.Len(t, reaped, 1)
assert.Contains(t, reaped, "stale-1")
got, _ := reg.Get("stale-1")
assert.Equal(t, AgentOffline, got.Status)
got, _ = reg.Get("fresh-1")
assert.Equal(t, AgentAvailable, got.Status)
}
func TestRedisRegistry_Reap_Good_AlreadyOfflineSkipped(t *testing.T) {
reg := newTestRedisRegistry(t)
stale := time.Now().UTC().Add(-10 * time.Minute)
_ = reg.Register(AgentInfo{ID: "already-off", Status: AgentOffline, LastHeartbeat: stale})
reaped := reg.Reap(5 * time.Minute)
assert.Empty(t, reaped)
}
func TestRedisRegistry_Reap_Good_NoStaleAgents(t *testing.T) {
reg := newTestRedisRegistry(t)
now := time.Now().UTC()
_ = reg.Register(AgentInfo{ID: "a", Status: AgentAvailable, LastHeartbeat: now})
_ = reg.Register(AgentInfo{ID: "b", Status: AgentBusy, LastHeartbeat: now})
reaped := reg.Reap(5 * time.Minute)
assert.Empty(t, reaped)
}
func TestRedisRegistry_Reap_Good_BusyAgentReaped(t *testing.T) {
reg := newTestRedisRegistry(t)
stale := time.Now().UTC().Add(-10 * time.Minute)
_ = reg.Register(AgentInfo{ID: "busy-stale", Status: AgentBusy, LastHeartbeat: stale})
reaped := reg.Reap(5 * time.Minute)
assert.Len(t, reaped, 1)
assert.Contains(t, reaped, "busy-stale")
got, _ := reg.Get("busy-stale")
assert.Equal(t, AgentOffline, got.Status)
}
// --- Concurrent access ---
func TestRedisRegistry_Concurrent_Good(t *testing.T) {
reg := newTestRedisRegistry(t)
var wg sync.WaitGroup
for i := range 20 {
wg.Add(1)
go func(n int) {
defer wg.Done()
id := "agent-" + string(rune('a'+n%5))
_ = reg.Register(AgentInfo{
ID: id,
Name: "Concurrent",
Status: AgentAvailable,
LastHeartbeat: time.Now().UTC(),
})
_, _ = reg.Get(id)
_ = reg.Heartbeat(id)
_ = reg.List()
_ = reg.Reap(1 * time.Minute)
}(i)
}
wg.Wait()
// No race conditions — test passes under -race.
agents := reg.List()
assert.True(t, len(agents) > 0)
}
// --- Constructor error case ---
func TestNewRedisRegistry_Bad_Unreachable(t *testing.T) {
_, err := NewRedisRegistry("127.0.0.1:1") // almost certainly unreachable
require.Error(t, err)
apiErr, ok := err.(*APIError)
require.True(t, ok, "expected *APIError")
assert.Equal(t, 500, apiErr.Code)
assert.Contains(t, err.Error(), "failed to connect to Redis")
}
// --- Config-based factory with redis backend ---
func TestNewAgentRegistryFromConfig_Good_Redis(t *testing.T) {
cfg := RegistryConfig{
RegistryBackend: "redis",
RegistryRedisAddr: testRedisAddr,
}
reg, err := NewAgentRegistryFromConfig(cfg)
if err != nil {
t.Skipf("Redis unavailable at %s: %v", testRedisAddr, err)
}
rr, ok := reg.(*RedisRegistry)
assert.True(t, ok, "expected RedisRegistry")
_ = rr.Close()
}