package store import ( "context" "errors" "testing" "time" "gitea.stevedudenhoeffer.com/steve/executus/budget" ) // TestSQLiteBudgetConformance runs the budget battery over the SQLite store and // asserts the same rolling-window contract the in-memory store must satisfy. func TestSQLiteBudgetConformance(t *testing.T) { ctx := context.Background() db, err := Open(":memory:") if err != nil { t.Fatal(err) } defer db.Close() st := db.Budget() if err := st.Initialize(ctx); err != nil { t.Fatal(err) } now := time.Now().UTC() b := budget.NewDBBudget(st, func() float64 { return 100 }, nil, func() time.Time { return now }) 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) } b.Commit(ctx, "u", 50) // 110 total if err := b.Check(ctx, "u"); !errors.Is(err, budget.ErrBudgetExceeded) { t.Fatalf("110/100 should be ErrBudgetExceeded, got %v", err) } // Direct Get reflects the persisted row. row, err := st.Get(ctx, "u") if err != nil || row == nil { t.Fatalf("Get: %v %+v", err, row) } if row.SecondsUsed != 110 || row.RunsCount != 2 { t.Errorf("row = %+v, want seconds=110 runs=2", row) } // Window rolls over after 7 days. now = now.Add(8 * 24 * time.Hour) b.Commit(ctx, "u", 1) if err := b.Check(ctx, "u"); err != nil { t.Fatalf("after rollover should pass: %v", err) } row, _ = st.Get(ctx, "u") if row.SecondsUsed != 1 || row.RunsCount != 1 { t.Errorf("post-rollover row = %+v, want seconds=1 runs=1", row) } // Unknown user -> (nil, nil). if r, err := st.Get(ctx, "nobody"); err != nil || r != nil { t.Errorf("Get(unknown) = %+v %v, want nil,nil", r, err) } }