mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-03 05:20:01 +00:00
Three related fixes to agent router identity handling: 1. Canonicalize on resolve — Router.Get now stores entries under the canonical tenantID:agent_key after a successful resolver call, regardless of whether the caller passed a UUID or agent_key. Prevents fragmentation where the same logical agent would occupy two cache slots. Callers that pass the UUID form are now un-cached and resolve on every call; all production callers pass agent_key today. 2. Exact-segment match — matchAgentCacheKey helper replaces HasSuffix in Router.Remove and Router.InvalidateAgent. Prevents substring collisions like "tenantX:sub-foo" being wiped when invalidating "foo". Rejects empty agentKey to guard against wildcard wipes. 3. Tenant-scoped IsRunning (C6) — Router.IsRunning now accepts ctx and uses agentCacheKey for lookup. Pre-fix, the bare r.agents[agentID] lookup always returned false in tenant-scoped deployments, causing agents.list to report every live agent as idle. Updates the sole caller at gateway/methods/agents.go:134. Phase 2 of agent identity hardening (TD-3).
77 lines
2.5 KiB
Go
77 lines
2.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// TestInvalidateAgent_MatchesAgentKeyNotUUID documents the pre-Phase-2 state
|
|
// where cache entries could be fragmented between UUID-keyed and agentKey-keyed
|
|
// forms. This test manually pokes r.agents[] to simulate the fragmented state
|
|
// — it does NOT exercise Router.Get, which now canonicalizes on resolve (see
|
|
// TestRouterGet_UUIDInputStoresCanonicalKey for the fixed path).
|
|
//
|
|
// Kept as a regression guard on the exact-segment match semantics: invalidating
|
|
// by agentKey must clear the agentKey entry but leave a UUID-shaped entry alone
|
|
// (the UUID's last segment is not the agentKey).
|
|
func TestInvalidateAgent_MatchesAgentKeyNotUUID(t *testing.T) {
|
|
r := NewRouter()
|
|
agentKey := "my-agent"
|
|
agentUUID := uuid.New().String()
|
|
tenantID := uuid.New()
|
|
|
|
// Simulate chat path: cache entry keyed by agentKey
|
|
ctx := store.WithTenantID(context.Background(), tenantID)
|
|
chatKey := agentCacheKey(ctx, agentKey)
|
|
r.agents[chatKey] = &agentEntry{}
|
|
|
|
// Simulate old heartbeat path: cache entry keyed by UUID (the bug)
|
|
uuidKey := agentCacheKey(ctx, agentUUID)
|
|
r.agents[uuidKey] = &agentEntry{}
|
|
|
|
// InvalidateAgent with agentKey should clear agentKey entry
|
|
r.InvalidateAgent(agentKey)
|
|
|
|
if _, ok := r.agents[chatKey]; ok {
|
|
t.Error("InvalidateAgent(agentKey) should have cleared the agentKey-based cache entry")
|
|
}
|
|
|
|
// UUID entry is NOT cleared — this is why the bug existed
|
|
if _, ok := r.agents[uuidKey]; !ok {
|
|
t.Error("InvalidateAgent(agentKey) should NOT match UUID-based entries (documenting the bug)")
|
|
}
|
|
|
|
// InvalidateAgent with UUID clears the UUID entry (belt-and-suspenders fix)
|
|
r.InvalidateAgent(agentUUID)
|
|
if _, ok := r.agents[uuidKey]; ok {
|
|
t.Error("InvalidateAgent(UUID) should have cleared the UUID-based cache entry")
|
|
}
|
|
}
|
|
|
|
// TestInvalidateAgent_TenantScoped verifies that InvalidateAgent clears entries
|
|
// across all tenants for the same agentKey (suffix match).
|
|
func TestInvalidateAgent_TenantScoped(t *testing.T) {
|
|
r := NewRouter()
|
|
agentKey := "default"
|
|
tenantA := uuid.New()
|
|
tenantB := uuid.New()
|
|
|
|
ctxA := store.WithTenantID(context.Background(), tenantA)
|
|
ctxB := store.WithTenantID(context.Background(), tenantB)
|
|
|
|
keyA := agentCacheKey(ctxA, agentKey)
|
|
keyB := agentCacheKey(ctxB, agentKey)
|
|
r.agents[keyA] = &agentEntry{}
|
|
r.agents[keyB] = &agentEntry{}
|
|
|
|
r.InvalidateAgent(agentKey)
|
|
|
|
if len(r.agents) != 0 {
|
|
t.Errorf("InvalidateAgent should clear all tenant-scoped entries, got %d remaining", len(r.agents))
|
|
}
|
|
}
|