9116abcae2
Second Tier-2 battery, plugging into run.Ports.Budget: - budget.go: skillexec's BudgetTracker / NoOpBudget / DBBudget moved clean (stdlib only). Check/Commit match run.Budget exactly (compile-time proof in run.go: NoOpBudget and *DBBudget are run.Budget). - storage.go: the BudgetStorage seam + SkillBudget domain, split out of mort's GORM file (the GORM impl stays in mort). - memory.go: NewMemory() — zero-dependency in-process BudgetStorage with the 7-day rolling-window rollover in Add. Tests: per-user cap enforced, window rolls over after 7 days, NoOp always allows. CI invariant: core imports ZERO from the budget battery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
45 lines
1.2 KiB
Go
45 lines
1.2 KiB
Go
package budget
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDBBudgetRollingWindow(t *testing.T) {
|
|
ctx := context.Background()
|
|
mem := NewMemory()
|
|
now := time.Now()
|
|
clock := func() time.Time { return now }
|
|
b := NewDBBudget(mem, func() float64 { return 100 }, nil, clock)
|
|
|
|
// Under cap: allowed.
|
|
if err := b.Check(ctx, "u"); err != nil {
|
|
t.Fatalf("fresh caller should pass: %v", err)
|
|
}
|
|
b.Commit(ctx, "u", 60)
|
|
if err := b.Check(ctx, "u"); err != nil {
|
|
t.Fatalf("60/100 should pass: %v", err)
|
|
}
|
|
// Over cap: rejected.
|
|
b.Commit(ctx, "u", 50) // 110 total
|
|
if err := b.Check(ctx, "u"); !errors.Is(err, ErrBudgetExceeded) {
|
|
t.Fatalf("110/100 should be ErrBudgetExceeded, got %v", err)
|
|
}
|
|
// Window rolls over after 7 days: allowed again.
|
|
now = now.Add(8 * 24 * time.Hour)
|
|
b.Commit(ctx, "u", 1) // triggers rollover inside Add
|
|
if err := b.Check(ctx, "u"); err != nil {
|
|
t.Fatalf("after window rollover should pass: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNoOpBudgetAlwaysAllows(t *testing.T) {
|
|
b := NewNoOpBudget()
|
|
if err := b.Check(context.Background(), "anyone"); err != nil {
|
|
t.Fatalf("NoOp must always allow: %v", err)
|
|
}
|
|
b.Commit(context.Background(), "anyone", 1e9) // no-op
|
|
}
|