summaryrefslogtreecommitdiff
path: root/internal/storage/db_test.go
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-26 20:27:36 +0000
committerClaude <noreply@anthropic.com>2026-05-26 20:27:36 +0000
commit39f74dbbc7adca0e2058112408bb99694deefcaf (patch)
treefb657954c1eba928567403b0e320b976e23f734a /internal/storage/db_test.go
parent561915c5182c3fb39cd6a8b6613c489b35b7c1bf (diff)
feat(budget,storage,config): per-provider spend accounting substrate (Phase 6)
Adds the budget accountant foundation: - storage: a per-execution `agent` column (additive migration, populated by the dispatcher) and SpendByProviderSince(since), summing cost_usd per provider in a window. Accurate attribution survives a task running on different providers across retries. - internal/budget: Accountant over a SpendSource + Limits, exposing Headroom (remaining/fraction per provider), Allow (would estCost breach the cap), and All (one query, sorted). Unconfigured/local providers are unlimited. - config: a [budget] section (window + provider_5h_usd map). No default cap — gating is opt-in by configuring limits. Fully unit-tested; dispatcher gating and the API/UI surface follow. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
Diffstat (limited to 'internal/storage/db_test.go')
-rw-r--r--internal/storage/db_test.go38
1 files changed, 38 insertions, 0 deletions
diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go
index 0e67e02..4d744a4 100644
--- a/internal/storage/db_test.go
+++ b/internal/storage/db_test.go
@@ -309,6 +309,44 @@ func TestListTasks_WithLimit(t *testing.T) {
}
}
+func TestSpendByProviderSince(t *testing.T) {
+ db := testDB(t)
+ now := time.Now().UTC()
+
+ tk := &task.Task{
+ ID: "spend-task", Name: "S", Agent: task.AgentConfig{Type: "claude", Instructions: "x"},
+ Priority: task.PriorityNormal, Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"},
+ Tags: []string{}, DependsOn: []string{}, State: task.StatePending, CreatedAt: now, UpdatedAt: now,
+ }
+ if err := db.CreateTask(tk); err != nil {
+ t.Fatal(err)
+ }
+
+ // Two recent claude execs, one recent gemini, one old claude (outside window).
+ execs := []*Execution{
+ {ID: "s1", TaskID: "spend-task", StartTime: now.Add(-1 * time.Hour), Status: "COMPLETED", CostUSD: 1.5, Agent: "claude"},
+ {ID: "s2", TaskID: "spend-task", StartTime: now.Add(-2 * time.Hour), Status: "COMPLETED", CostUSD: 2.0, Agent: "claude"},
+ {ID: "s3", TaskID: "spend-task", StartTime: now.Add(-30 * time.Minute), Status: "COMPLETED", CostUSD: 0.75, Agent: "gemini"},
+ {ID: "s4", TaskID: "spend-task", StartTime: now.Add(-10 * time.Hour), Status: "COMPLETED", CostUSD: 99, Agent: "claude"},
+ }
+ for _, e := range execs {
+ if err := db.CreateExecution(e); err != nil {
+ t.Fatalf("create exec %s: %v", e.ID, err)
+ }
+ }
+
+ spends, err := db.SpendByProviderSince(now.Add(-5 * time.Hour))
+ if err != nil {
+ t.Fatalf("SpendByProviderSince: %v", err)
+ }
+ if got := spends["claude"]; got != 3.5 {
+ t.Errorf("claude spend: want 3.5 (old $99 excluded), got %v", got)
+ }
+ if got := spends["gemini"]; got != 0.75 {
+ t.Errorf("gemini spend: want 0.75, got %v", got)
+ }
+}
+
func TestCreateExecution_AndGet(t *testing.T) {
db := testDB(t)
now := time.Now().UTC().Truncate(time.Second)