Files
goclaw/internal/http/builtin_tools_cache_invalidate_test.go
T
viettranx d77a3664db fix(cache): tenant-aware invalidation for builtin tools and skills
Tenant config changes for builtin tools and skills silently failed to
invalidate cached agent Loops, leaving tenants stuck on stale tool/skill
sets until the 10-minute TTL expired or an unrelated event wiped the
cache. Master-level skill CRUD had the same gap in the opposite
direction.

- Add CacheInvalidatePayload.TenantID so events can scope to one tenant
- Add Router.InvalidateTenant(tenantID) with prefix match on
  "tenantID:agentKey" cache keys; uuid.Nil is a no-op
- Rework emitCacheInvalidate helpers in builtin_tools + skills handlers
  to carry tenant scope; add defense-in-depth uuid.Nil guards in four
  tenant-config handlers
- Update TopicCacheBuiltinTools + TopicCacheSkills subscribers to branch
  on payload.TenantID (tenant event wipes that tenant, global event
  keeps the existing InvalidateAll path)
- Wire emitCacheInvalidate into master skill CRUD paths (update, delete,
  toggle, upload, install-deps, rescan-deps, import) that previously
  only called BumpVersion
- Document the system-owner bypass in requireTenantAdmin so handlers
  keep guarding uuid.Nil themselves
2026-04-11 23:22:17 +07:00

81 lines
2.2 KiB
Go

package http
import (
"testing"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/bus"
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
)
// TestBuiltinToolsHandler_EmitCacheInvalidate_CarriesTenantID verifies the
// emit helper populates TenantID in the broadcast payload so downstream
// subscribers can branch on tenant-scoped vs global invalidation.
func TestBuiltinToolsHandler_EmitCacheInvalidate_CarriesTenantID(t *testing.T) {
mb := bus.New()
defer mb.Close()
h := &BuiltinToolsHandler{msgBus: mb}
var captured bus.CacheInvalidatePayload
var gotEvent bool
mb.Subscribe("test-capture", func(e bus.Event) {
if e.Name != protocol.EventCacheInvalidate {
return
}
p, ok := e.Payload.(bus.CacheInvalidatePayload)
if !ok {
return
}
captured = p
gotEvent = true
})
tid := uuid.New()
h.emitCacheInvalidate("tts", tid)
if !gotEvent {
t.Fatal("no cache invalidate event received")
}
if captured.Kind != bus.CacheKindBuiltinTools {
t.Errorf("kind = %q, want %q", captured.Kind, bus.CacheKindBuiltinTools)
}
if captured.Key != "tts" {
t.Errorf("key = %q, want %q", captured.Key, "tts")
}
if captured.TenantID != tid {
t.Errorf("tenantID = %v, want %v", captured.TenantID, tid)
}
}
// TestBuiltinToolsHandler_EmitCacheInvalidate_NilTenantMeansGlobal verifies
// passing uuid.Nil yields a payload whose subscribers read as "global".
func TestBuiltinToolsHandler_EmitCacheInvalidate_NilTenantMeansGlobal(t *testing.T) {
mb := bus.New()
defer mb.Close()
h := &BuiltinToolsHandler{msgBus: mb}
var captured bus.CacheInvalidatePayload
mb.Subscribe("test-capture", func(e bus.Event) {
if p, ok := e.Payload.(bus.CacheInvalidatePayload); ok {
captured = p
}
})
h.emitCacheInvalidate("tts", uuid.Nil)
if captured.TenantID != uuid.Nil {
t.Errorf("tenantID = %v, want uuid.Nil for global invalidation", captured.TenantID)
}
}
// TestBuiltinToolsHandler_EmitCacheInvalidate_NilMsgBusNoop verifies the
// helper is safe to call when msgBus is unset (e.g., in tests/desktop lite).
func TestBuiltinToolsHandler_EmitCacheInvalidate_NilMsgBusNoop(t *testing.T) {
h := &BuiltinToolsHandler{msgBus: nil}
// Must not panic.
h.emitCacheInvalidate("tts", uuid.New())
}