test: tests para rate limiter y registry con rate limiting

Tests unitarios para tools/ratelimit.go:
- Allow dentro del limite, denegacion al exceder
- Keys independientes (rooms distintas no interfieren)
- Expiracion de ventana temporal
- Cleanup de entries expiradas vs activas

Tests de integracion para Registry.ExecuteForRoom:
- Rate limiting activo bloquea tras exceder limite
- Sin rate limiter todas las llamadas pasan

Parte de issue 0019c (tarea 6.5).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 19:45:47 +00:00
parent 69efb6ab95
commit 3dcd890dc9
+161
View File
@@ -0,0 +1,161 @@
package tools
import (
"context"
"log/slog"
"testing"
"time"
)
func TestRateLimiter_AllowWithinLimit(t *testing.T) {
rl := NewRateLimiter(3, time.Minute)
for i := 0; i < 3; i++ {
if !rl.Allow("room1") {
t.Fatalf("call %d should be allowed", i+1)
}
}
}
func TestRateLimiter_DenyOverLimit(t *testing.T) {
rl := NewRateLimiter(3, time.Minute)
for i := 0; i < 3; i++ {
rl.Allow("room1")
}
if rl.Allow("room1") {
t.Fatal("4th call should be denied")
}
}
func TestRateLimiter_DifferentKeysIndependent(t *testing.T) {
rl := NewRateLimiter(2, time.Minute)
rl.Allow("room1")
rl.Allow("room1")
// room1 is full, but room2 should still be allowed
if rl.Allow("room1") {
t.Fatal("room1 3rd call should be denied")
}
if !rl.Allow("room2") {
t.Fatal("room2 should be allowed independently")
}
}
func TestRateLimiter_WindowExpiry(t *testing.T) {
// Use a very short window for testing
rl := NewRateLimiter(2, 50*time.Millisecond)
rl.Allow("room1")
rl.Allow("room1")
if rl.Allow("room1") {
t.Fatal("should be denied before window expires")
}
// Wait for window to expire
time.Sleep(60 * time.Millisecond)
if !rl.Allow("room1") {
t.Fatal("should be allowed after window expires")
}
}
func TestRateLimiter_Cleanup(t *testing.T) {
rl := NewRateLimiter(5, 50*time.Millisecond)
rl.Allow("room1")
rl.Allow("room2")
// Wait for entries to expire
time.Sleep(60 * time.Millisecond)
rl.Cleanup()
rl.mu.Lock()
count := len(rl.buckets)
rl.mu.Unlock()
if count != 0 {
t.Fatalf("expected 0 buckets after cleanup, got %d", count)
}
}
func TestRateLimiter_CleanupKeepsActive(t *testing.T) {
rl := NewRateLimiter(5, time.Minute)
rl.Allow("room1")
rl.Cleanup()
rl.mu.Lock()
count := len(rl.buckets)
rl.mu.Unlock()
if count != 1 {
t.Fatalf("expected 1 bucket after cleanup of active entries, got %d", count)
}
}
func TestRegistry_ExecuteForRoom_RateLimited(t *testing.T) {
logger := slog.Default()
reg := NewRegistry(logger)
// Register a simple echo tool
reg.Register(Tool{
Def: Def{Name: "echo", Description: "echo tool"},
Exec: func(_ context.Context, args map[string]any) Result {
return Result{Output: "ok"}
},
})
rl := NewRateLimiter(2, time.Minute)
reg.SetRateLimiter(rl)
ctx := context.Background()
// First two calls succeed
r1 := reg.ExecuteForRoom(ctx, "echo", "", "!room:test")
if r1.Err != nil {
t.Fatalf("call 1 should succeed: %v", r1.Err)
}
r2 := reg.ExecuteForRoom(ctx, "echo", "", "!room:test")
if r2.Err != nil {
t.Fatalf("call 2 should succeed: %v", r2.Err)
}
// Third call is rate limited
r3 := reg.ExecuteForRoom(ctx, "echo", "", "!room:test")
if r3.Err == nil {
t.Fatal("call 3 should be rate limited")
}
// Different room still works
r4 := reg.ExecuteForRoom(ctx, "echo", "", "!room:other")
if r4.Err != nil {
t.Fatalf("different room should succeed: %v", r4.Err)
}
}
func TestRegistry_ExecuteForRoom_NoLimiter(t *testing.T) {
logger := slog.Default()
reg := NewRegistry(logger)
reg.Register(Tool{
Def: Def{Name: "echo", Description: "echo tool"},
Exec: func(_ context.Context, args map[string]any) Result {
return Result{Output: "ok"}
},
})
// No rate limiter set — all calls should succeed
ctx := context.Background()
for i := 0; i < 20; i++ {
r := reg.ExecuteForRoom(ctx, "echo", "", "!room:test")
if r.Err != nil {
t.Fatalf("call %d should succeed without limiter: %v", i+1, r.Err)
}
}
}