mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 14:15:00 +00:00
The agent config UI (tools-profile-section) and i18n already expose a per-agent "Rate Limit (per hour)" field saved into tools_config, but the backend ignored it — the tool rate limiter only ever used the global tools.rate_limit_per_hour. Wire the field through: ToolPolicySpec gains RateLimitPerHour; the agent loop threads it into the tool-execution context; the registry passes it to the limiter, which uses it in place of the global max when > 0 (0 inherits the global).
154 lines
3.4 KiB
Go
154 lines
3.4 KiB
Go
package tools
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewToolRateLimiter_Zero(t *testing.T) {
|
|
rl := NewToolRateLimiter(0)
|
|
if rl != nil {
|
|
t.Errorf("expected nil for maxPerHour=0, got %v", rl)
|
|
}
|
|
}
|
|
|
|
func TestNewToolRateLimiter_Negative(t *testing.T) {
|
|
rl := NewToolRateLimiter(-5)
|
|
if rl != nil {
|
|
t.Errorf("expected nil for maxPerHour=-5, got %v", rl)
|
|
}
|
|
}
|
|
|
|
func TestToolRateLimiter_AllowUnderLimit(t *testing.T) {
|
|
rl := NewToolRateLimiter(5)
|
|
for i := range 5 {
|
|
if err := rl.Allow("user1"); err != nil {
|
|
t.Errorf("action %d should be allowed: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToolRateLimiter_BlockOverLimit(t *testing.T) {
|
|
rl := NewToolRateLimiter(3)
|
|
|
|
for i := range 3 {
|
|
if err := rl.Allow("user1"); err != nil {
|
|
t.Fatalf("action %d should be allowed: %v", i, err)
|
|
}
|
|
}
|
|
|
|
err := rl.Allow("user1")
|
|
if err == nil {
|
|
t.Error("4th action should be blocked")
|
|
}
|
|
}
|
|
|
|
func TestToolRateLimiter_SeparateKeys(t *testing.T) {
|
|
rl := NewToolRateLimiter(2)
|
|
|
|
// Fill user1
|
|
rl.Allow("user1")
|
|
rl.Allow("user1")
|
|
|
|
// user1 is blocked
|
|
if err := rl.Allow("user1"); err == nil {
|
|
t.Error("user1 should be blocked")
|
|
}
|
|
|
|
// user2 is independent
|
|
if err := rl.Allow("user2"); err != nil {
|
|
t.Errorf("user2 should be allowed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestToolRateLimiter_AllowWithLimit_Override(t *testing.T) {
|
|
rl := NewToolRateLimiter(100) // global default 100
|
|
|
|
// A per-agent override of 2 caps this key at 2, regardless of the global 100.
|
|
if err := rl.AllowWithLimit("agentA", 2); err != nil {
|
|
t.Fatalf("call 1 should be allowed: %v", err)
|
|
}
|
|
if err := rl.AllowWithLimit("agentA", 2); err != nil {
|
|
t.Fatalf("call 2 should be allowed: %v", err)
|
|
}
|
|
if err := rl.AllowWithLimit("agentA", 2); err == nil {
|
|
t.Error("call 3 should be blocked by the override of 2")
|
|
}
|
|
|
|
// maxOverride <= 0 falls back to the configured global (100), on its own key.
|
|
for i := range 3 {
|
|
if err := rl.AllowWithLimit("agentB", 0); err != nil {
|
|
t.Fatalf("agentB call %d should use global 100: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToolRateLimiter_WindowExpiry(t *testing.T) {
|
|
rl := &ToolRateLimiter{
|
|
windows: make(map[string][]time.Time),
|
|
maxPerHr: 2,
|
|
window: 100 * time.Millisecond, // short window for testing
|
|
}
|
|
|
|
// Fill the window
|
|
rl.Allow("key1")
|
|
rl.Allow("key1")
|
|
|
|
if err := rl.Allow("key1"); err == nil {
|
|
t.Error("should be blocked at limit")
|
|
}
|
|
|
|
// Wait for window to expire
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
if err := rl.Allow("key1"); err != nil {
|
|
t.Errorf("should be allowed after window expiry: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestToolRateLimiter_Cleanup(t *testing.T) {
|
|
rl := &ToolRateLimiter{
|
|
windows: make(map[string][]time.Time),
|
|
maxPerHr: 10,
|
|
window: 50 * time.Millisecond,
|
|
}
|
|
|
|
rl.Allow("key1")
|
|
rl.Allow("key2")
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
rl.Cleanup()
|
|
|
|
rl.mu.Lock()
|
|
count := len(rl.windows)
|
|
rl.mu.Unlock()
|
|
|
|
if count != 0 {
|
|
t.Errorf("cleanup should remove all expired entries, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestToolRateLimiter_CleanupPartial(t *testing.T) {
|
|
rl := &ToolRateLimiter{
|
|
windows: make(map[string][]time.Time),
|
|
maxPerHr: 10,
|
|
window: 200 * time.Millisecond,
|
|
}
|
|
|
|
rl.Allow("key1") // will expire
|
|
time.Sleep(100 * time.Millisecond)
|
|
rl.Allow("key1") // still fresh
|
|
|
|
// Only the first entry should be pruned, not the whole key
|
|
time.Sleep(150 * time.Millisecond)
|
|
rl.Cleanup()
|
|
|
|
rl.mu.Lock()
|
|
entries := len(rl.windows["key1"])
|
|
rl.mu.Unlock()
|
|
|
|
if entries != 1 {
|
|
t.Errorf("expected 1 remaining entry, got %d", entries)
|
|
}
|
|
}
|