408 lines
11 KiB
Go
408 lines
11 KiB
Go
|
|
package agentic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
)
|
||
|
|
|
||
|
|
// --- MemoryStore tests ---
|
||
|
|
|
||
|
|
func TestMemoryStore_SetGetAllowance_Good(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
a := &AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
DailyTokenLimit: 100000,
|
||
|
|
DailyJobLimit: 10,
|
||
|
|
ConcurrentJobs: 2,
|
||
|
|
MaxJobDuration: 30 * time.Minute,
|
||
|
|
ModelAllowlist: []string{"claude-sonnet-4-5-20250929"},
|
||
|
|
}
|
||
|
|
|
||
|
|
err := store.SetAllowance(a)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
got, err := store.GetAllowance("agent-1")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.Equal(t, a.AgentID, got.AgentID)
|
||
|
|
assert.Equal(t, a.DailyTokenLimit, got.DailyTokenLimit)
|
||
|
|
assert.Equal(t, a.DailyJobLimit, got.DailyJobLimit)
|
||
|
|
assert.Equal(t, a.ConcurrentJobs, got.ConcurrentJobs)
|
||
|
|
assert.Equal(t, a.ModelAllowlist, got.ModelAllowlist)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMemoryStore_GetAllowance_Bad_NotFound(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
_, err := store.GetAllowance("nonexistent")
|
||
|
|
require.Error(t, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMemoryStore_IncrementUsage_Good(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
|
||
|
|
err := store.IncrementUsage("agent-1", 5000, 1)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
usage, err := store.GetUsage("agent-1")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.Equal(t, int64(5000), usage.TokensUsed)
|
||
|
|
assert.Equal(t, 1, usage.JobsStarted)
|
||
|
|
assert.Equal(t, 1, usage.ActiveJobs)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMemoryStore_DecrementActiveJobs_Good(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
|
||
|
|
_ = store.IncrementUsage("agent-1", 0, 2)
|
||
|
|
_ = store.DecrementActiveJobs("agent-1")
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
assert.Equal(t, 1, usage.ActiveJobs)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMemoryStore_DecrementActiveJobs_Good_FloorAtZero(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
|
||
|
|
_ = store.DecrementActiveJobs("agent-1") // no-op, no usage record
|
||
|
|
_ = store.IncrementUsage("agent-1", 0, 0)
|
||
|
|
_ = store.DecrementActiveJobs("agent-1") // should stay at 0
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
assert.Equal(t, 0, usage.ActiveJobs)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMemoryStore_ReturnTokens_Good(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
|
||
|
|
_ = store.IncrementUsage("agent-1", 10000, 0)
|
||
|
|
err := store.ReturnTokens("agent-1", 5000)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
assert.Equal(t, int64(5000), usage.TokensUsed)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMemoryStore_ReturnTokens_Good_FloorAtZero(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
|
||
|
|
_ = store.IncrementUsage("agent-1", 1000, 0)
|
||
|
|
_ = store.ReturnTokens("agent-1", 5000) // more than used
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
assert.Equal(t, int64(0), usage.TokensUsed)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMemoryStore_ResetUsage_Good(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
|
||
|
|
_ = store.IncrementUsage("agent-1", 50000, 5)
|
||
|
|
err := store.ResetUsage("agent-1")
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
assert.Equal(t, int64(0), usage.TokensUsed)
|
||
|
|
assert.Equal(t, 0, usage.JobsStarted)
|
||
|
|
assert.Equal(t, 0, usage.ActiveJobs)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMemoryStore_ModelUsage_Good(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
|
||
|
|
_ = store.IncrementModelUsage("claude-sonnet", 10000)
|
||
|
|
_ = store.IncrementModelUsage("claude-sonnet", 5000)
|
||
|
|
|
||
|
|
usage, err := store.GetModelUsage("claude-sonnet")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.Equal(t, int64(15000), usage)
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- AllowanceService.Check tests ---
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Good(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.SetAllowance(&AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
DailyTokenLimit: 100000,
|
||
|
|
DailyJobLimit: 10,
|
||
|
|
ConcurrentJobs: 2,
|
||
|
|
})
|
||
|
|
|
||
|
|
result, err := svc.Check("agent-1", "")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.True(t, result.Allowed)
|
||
|
|
assert.Equal(t, AllowanceOK, result.Status)
|
||
|
|
assert.Equal(t, int64(100000), result.RemainingTokens)
|
||
|
|
assert.Equal(t, 10, result.RemainingJobs)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Good_Warning(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.SetAllowance(&AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
DailyTokenLimit: 100000,
|
||
|
|
})
|
||
|
|
_ = store.IncrementUsage("agent-1", 85000, 0)
|
||
|
|
|
||
|
|
result, err := svc.Check("agent-1", "")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.True(t, result.Allowed)
|
||
|
|
assert.Equal(t, AllowanceWarning, result.Status)
|
||
|
|
assert.Equal(t, int64(15000), result.RemainingTokens)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Bad_TokenLimitExceeded(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.SetAllowance(&AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
DailyTokenLimit: 100000,
|
||
|
|
})
|
||
|
|
_ = store.IncrementUsage("agent-1", 100001, 0)
|
||
|
|
|
||
|
|
result, err := svc.Check("agent-1", "")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.False(t, result.Allowed)
|
||
|
|
assert.Equal(t, AllowanceExceeded, result.Status)
|
||
|
|
assert.Contains(t, result.Reason, "daily token limit")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Bad_JobLimitExceeded(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.SetAllowance(&AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
DailyJobLimit: 5,
|
||
|
|
})
|
||
|
|
_ = store.IncrementUsage("agent-1", 0, 5)
|
||
|
|
|
||
|
|
result, err := svc.Check("agent-1", "")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.False(t, result.Allowed)
|
||
|
|
assert.Contains(t, result.Reason, "daily job limit")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Bad_ConcurrentLimitReached(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.SetAllowance(&AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
ConcurrentJobs: 1,
|
||
|
|
})
|
||
|
|
_ = store.IncrementUsage("agent-1", 0, 1) // 1 active job
|
||
|
|
|
||
|
|
result, err := svc.Check("agent-1", "")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.False(t, result.Allowed)
|
||
|
|
assert.Contains(t, result.Reason, "concurrent job limit")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Bad_ModelNotInAllowlist(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.SetAllowance(&AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
ModelAllowlist: []string{"claude-sonnet-4-5-20250929"},
|
||
|
|
})
|
||
|
|
|
||
|
|
result, err := svc.Check("agent-1", "claude-opus-4-6")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.False(t, result.Allowed)
|
||
|
|
assert.Contains(t, result.Reason, "model not in allowlist")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Good_ModelInAllowlist(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.SetAllowance(&AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
ModelAllowlist: []string{"claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001"},
|
||
|
|
})
|
||
|
|
|
||
|
|
result, err := svc.Check("agent-1", "claude-sonnet-4-5-20250929")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.True(t, result.Allowed)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Good_EmptyModelSkipsCheck(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.SetAllowance(&AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
ModelAllowlist: []string{"claude-sonnet-4-5-20250929"},
|
||
|
|
})
|
||
|
|
|
||
|
|
result, err := svc.Check("agent-1", "")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.True(t, result.Allowed)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Bad_GlobalModelBudgetExceeded(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.SetAllowance(&AgentAllowance{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
})
|
||
|
|
store.SetModelQuota(&ModelQuota{
|
||
|
|
Model: "claude-opus-4-6",
|
||
|
|
DailyTokenBudget: 500000,
|
||
|
|
})
|
||
|
|
_ = store.IncrementModelUsage("claude-opus-4-6", 500001)
|
||
|
|
|
||
|
|
result, err := svc.Check("agent-1", "claude-opus-4-6")
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.False(t, result.Allowed)
|
||
|
|
assert.Contains(t, result.Reason, "global model token budget")
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceCheck_Bad_NoAllowance(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_, err := svc.Check("unknown-agent", "")
|
||
|
|
require.Error(t, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- AllowanceService.RecordUsage tests ---
|
||
|
|
|
||
|
|
func TestAllowanceServiceRecordUsage_Good_JobStarted(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
err := svc.RecordUsage(UsageReport{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
JobID: "job-1",
|
||
|
|
Event: QuotaEventJobStarted,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
assert.Equal(t, 1, usage.JobsStarted)
|
||
|
|
assert.Equal(t, 1, usage.ActiveJobs)
|
||
|
|
assert.Equal(t, int64(0), usage.TokensUsed)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceRecordUsage_Good_JobCompleted(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
// Start a job first
|
||
|
|
_ = svc.RecordUsage(UsageReport{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
JobID: "job-1",
|
||
|
|
Event: QuotaEventJobStarted,
|
||
|
|
})
|
||
|
|
|
||
|
|
err := svc.RecordUsage(UsageReport{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
JobID: "job-1",
|
||
|
|
Model: "claude-sonnet",
|
||
|
|
TokensIn: 1000,
|
||
|
|
TokensOut: 500,
|
||
|
|
Event: QuotaEventJobCompleted,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
assert.Equal(t, int64(1500), usage.TokensUsed)
|
||
|
|
assert.Equal(t, 0, usage.ActiveJobs)
|
||
|
|
|
||
|
|
modelUsage, _ := store.GetModelUsage("claude-sonnet")
|
||
|
|
assert.Equal(t, int64(1500), modelUsage)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceRecordUsage_Good_JobFailed_ReturnsHalf(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = svc.RecordUsage(UsageReport{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
JobID: "job-1",
|
||
|
|
Event: QuotaEventJobStarted,
|
||
|
|
})
|
||
|
|
|
||
|
|
err := svc.RecordUsage(UsageReport{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
JobID: "job-1",
|
||
|
|
Model: "claude-sonnet",
|
||
|
|
TokensIn: 1000,
|
||
|
|
TokensOut: 1000,
|
||
|
|
Event: QuotaEventJobFailed,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
// 2000 tokens used, 1000 returned (50%) = 1000 net
|
||
|
|
assert.Equal(t, int64(1000), usage.TokensUsed)
|
||
|
|
assert.Equal(t, 0, usage.ActiveJobs)
|
||
|
|
|
||
|
|
// Model sees net usage (2000 - 1000 = 1000)
|
||
|
|
modelUsage, _ := store.GetModelUsage("claude-sonnet")
|
||
|
|
assert.Equal(t, int64(1000), modelUsage)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllowanceServiceRecordUsage_Good_JobCancelled_ReturnsAll(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.IncrementUsage("agent-1", 5000, 1) // simulate pre-existing usage
|
||
|
|
|
||
|
|
err := svc.RecordUsage(UsageReport{
|
||
|
|
AgentID: "agent-1",
|
||
|
|
JobID: "job-1",
|
||
|
|
TokensIn: 500,
|
||
|
|
TokensOut: 500,
|
||
|
|
Event: QuotaEventJobCancelled,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
// 5000 pre-existing - 1000 returned = 4000
|
||
|
|
assert.Equal(t, int64(4000), usage.TokensUsed)
|
||
|
|
assert.Equal(t, 0, usage.ActiveJobs)
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- AllowanceService.ResetAgent tests ---
|
||
|
|
|
||
|
|
func TestAllowanceServiceResetAgent_Good(t *testing.T) {
|
||
|
|
store := NewMemoryStore()
|
||
|
|
svc := NewAllowanceService(store)
|
||
|
|
|
||
|
|
_ = store.IncrementUsage("agent-1", 50000, 5)
|
||
|
|
|
||
|
|
err := svc.ResetAgent("agent-1")
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
usage, _ := store.GetUsage("agent-1")
|
||
|
|
assert.Equal(t, int64(0), usage.TokensUsed)
|
||
|
|
assert.Equal(t, 0, usage.JobsStarted)
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- startOfDay helper test ---
|
||
|
|
|
||
|
|
func TestStartOfDay_Good(t *testing.T) {
|
||
|
|
input := time.Date(2026, 2, 10, 15, 30, 45, 0, time.UTC)
|
||
|
|
expected := time.Date(2026, 2, 10, 0, 0, 0, 0, time.UTC)
|
||
|
|
assert.Equal(t, expected, startOfDay(input))
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- AllowanceStatus tests ---
|
||
|
|
|
||
|
|
func TestAllowanceStatus_Good_Values(t *testing.T) {
|
||
|
|
assert.Equal(t, AllowanceStatus("ok"), AllowanceOK)
|
||
|
|
assert.Equal(t, AllowanceStatus("warning"), AllowanceWarning)
|
||
|
|
assert.Equal(t, AllowanceStatus("exceeded"), AllowanceExceeded)
|
||
|
|
}
|