Ctrl K
ComunidadGuías…

internal/budget/cost_basis_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package budget

import (
	"context"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"

	"github.com/enterpilot/gomodel/config"
	"github.com/enterpilot/gomodel/internal/storage"
	"github.com/enterpilot/gomodel/internal/storage/mongotest"
	"github.com/enterpilot/gomodel/internal/storage/sqlx"
	"github.com/enterpilot/gomodel/internal/storage/sqlx/sqlxtest"
	"github.com/enterpilot/gomodel/internal/usage"
)

func TestNormalizeCostBasis(t *testing.T) {
	tests := []struct {
		raw     string
		want    CostBasis
		wantErr bool
	}{
		{raw: "", want: CostBasisProvider},
		{raw: "provider", want: CostBasisProvider},
		{raw: " Billed ", want: CostBasisBilled},
		{raw: "billed", want: CostBasisBilled},
		{raw: "invoice", wantErr: true},
	}
	for _, tt := range tests {
		t.Run(tt.raw, func(t *testing.T) {
			got, err := NormalizeCostBasis(tt.raw)
			if tt.wantErr {
				require.ErrorContains(t, err, "cost_basis must be one of provider, billed")
				return
			}
			require.NoError(t, err)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestNormalizeBudgetDefaultsCostBasisToProvider(t *testing.T) {
	got, err := NormalizeBudget(Budget{Subject: "/team", PeriodSeconds: PeriodDailySeconds, Amount: 1})
	require.NoError(t, err)
	assert.Equal(t, CostBasisProvider, got.CostBasis)

	_, err = NormalizeBudget(Budget{Subject: "/team", PeriodSeconds: PeriodDailySeconds, Amount: 1, CostBasis: "gross"})
	require.Error(t, err)
}

func TestServiceRejectsBilledBudgetWritesWhenDisabled(t *testing.T) {
	ctx := context.Background()
	billed := Budget{
		Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled,
		PeriodSeconds: PeriodMonthlySeconds, Amount: 100,
	}
	store := &fakeStore{}
	service, err := NewService(ctx, store, WithBilledBudgets(false))
	require.NoError(t, err)
	err = service.UpsertBudgets(ctx, []Budget{billed})
	require.ErrorIs(t, err, ErrBilledBudgetsUnavailable)
	err = service.ReplaceConfigBudgets(ctx, []Budget{billed})
	require.ErrorIs(t, err, ErrBilledBudgetsUnavailable)
	require.Equal(t, 0, store.replaceCalls)

	// Provider-cost budgets are unaffected by the gate.
	provider := billed
	provider.CostBasis = CostBasisProvider
	require.NoError(t, service.UpsertBudgets(ctx, []Budget{provider}))
}

// A stored billed budget must not stop the gateway when billed budgets become
// unavailable: it keeps limiting spend, on provider cost.
func TestServiceEnforcesStoredBilledBudgetOnProviderCostWhenDisabled(t *testing.T) {
	ctx := context.Background()
	store := &fakeStore{
		budgets: []Budget{{
			Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled,
			PeriodSeconds: PeriodMonthlySeconds, Amount: 100, Source: SourceManual,
		}},
		sum: func(SpendWindow) (float64, bool, error) { return 100, true, nil },
	}
	service, err := NewService(ctx, store, WithBilledBudgets(false), WithBilledCacheHits(true))
	require.NoError(t, err)

	budgets := service.Budgets()
	require.Len(t, budgets, 1)
	assert.Equal(t, CostBasisBilled, budgets[0].CostBasis, "the stored basis is kept")
	assert.Equal(t, CostBasisProvider, budgets[0].EnforcedBasis())

	err = service.Check(ctx, Subjects{UserPath: "/", Labels: []string{"acme"}}, time.Now())
	var exceeded *ExceededError
	require.ErrorAs(t, err, &exceeded)
	require.Len(t, store.lastWindows, 1)
	assert.Equal(t, CostBasisProvider, store.lastWindows[0].CostBasis)

	// Enforced on provider cost, the budget no longer gates cache hits.
	store.sumCalls = 0
	require.NoError(t, service.CheckCacheHit(ctx, Subjects{UserPath: "/", Labels: []string{"acme"}}, time.Now()))
	assert.Zero(t, store.sumCalls)
}

func TestServiceKeepsStoredBilledBasisOnEditWhenDisabled(t *testing.T) {
	ctx := context.Background()
	store := &fakeStore{budgets: []Budget{{
		Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled,
		PeriodSeconds: PeriodMonthlySeconds, Amount: 100, Source: SourceManual,
	}}}
	service, err := NewService(ctx, store, WithBilledBudgets(false))
	require.NoError(t, err)

	assert.True(t, service.StoredAsBilled(ScopeLabel, "acme", PeriodMonthlySeconds))
	assert.False(t, service.StoredAsBilled(ScopeLabel, "acme", PeriodDailySeconds))
	assert.False(t, service.StoredAsBilled(ScopeLabel, "other", PeriodMonthlySeconds))

	// Editing the amount keeps the stored billed basis.
	require.NoError(t, service.UpsertBudgets(ctx, []Budget{{
		Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled,
		PeriodSeconds: PeriodMonthlySeconds, Amount: 200, Source: SourceManual,
	}}))
	// A new billed budget still needs the capability.
	err = service.UpsertBudgets(ctx, []Budget{{
		Scope: ScopeLabel, Subject: "other", CostBasis: CostBasisBilled,
		PeriodSeconds: PeriodMonthlySeconds, Amount: 200, Source: SourceManual,
	}})
	require.ErrorIs(t, err, ErrBilledBudgetsUnavailable)
	// Config budgets get the full check, even for a stored billed key.
	err = service.ReplaceConfigBudgets(ctx, []Budget{{
		Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled,
		PeriodSeconds: PeriodMonthlySeconds, Amount: 200, Source: SourceConfig,
	}})
	require.ErrorIs(t, err, ErrBilledBudgetsUnavailable)
	assert.Zero(t, store.replaceCalls)
}

func TestServicePassesCostBasisToSpendWindows(t *testing.T) {
	ctx := context.Background()
	store := &fakeStore{budgets: []Budget{
		{Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled, PeriodSeconds: PeriodDailySeconds, Amount: 10},
		{Scope: ScopeUserPath, Subject: "/", CostBasis: CostBasisProvider, PeriodSeconds: PeriodDailySeconds, Amount: 10},
	}}
	service, err := NewService(ctx, store)
	require.NoError(t, err)

	_, err = service.CheckWithResults(ctx, Subjects{UserPath: "/team", Labels: []string{"acme"}}, time.Now())
	require.NoError(t, err)
	require.Len(t, store.lastWindows, 2)
	bases := map[Scope]CostBasis{}
	for _, window := range store.lastWindows {
		bases[window.Scope] = window.CostBasis
	}
	assert.Equal(t, CostBasisBilled, bases[ScopeLabel])
	assert.Equal(t, CostBasisProvider, bases[ScopeUserPath])
}

func TestServiceCheckCacheHit(t *testing.T) {
	billedLabel := Budget{Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled, PeriodSeconds: PeriodDailySeconds, Amount: 10}
	providerLabel := Budget{Scope: ScopeLabel, Subject: "acme", PeriodSeconds: PeriodDailySeconds, Amount: 10}
	billedProvider := Budget{Scope: ScopeProvider, Subject: "openai", CostBasis: CostBasisBilled, PeriodSeconds: PeriodDailySeconds, Amount: 10}
	billedOther := Budget{Scope: ScopeLabel, Subject: "other", CostBasis: CostBasisBilled, PeriodSeconds: PeriodDailySeconds, Amount: 10}
	tests := []struct {
		name            string
		budgets         []Budget
		billedCacheHits bool
		wantExceeded    bool
		wantStoreCall   bool
	}{
		{name: "billed budget exhausted", budgets: []Budget{billedLabel}, billedCacheHits: true, wantExceeded: true, wantStoreCall: true},
		{name: "cache hits not billed", budgets: []Budget{billedLabel}},
		{name: "provider cost budget", budgets: []Budget{providerLabel}, billedCacheHits: true},
		{name: "billed provider-scope budget", budgets: []Budget{billedProvider}, billedCacheHits: true},
		{name: "billed budget for another label", budgets: []Budget{billedOther}, billedCacheHits: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			store := &fakeStore{
				budgets: tt.budgets,
				sum:     func(SpendWindow) (float64, bool, error) { return 10, true, nil },
			}
			service, err := NewService(ctx, store, WithBilledCacheHits(tt.billedCacheHits))
			require.NoError(t, err)

			err = service.CheckCacheHit(ctx, Subjects{UserPath: "/team", Labels: []string{"acme"}, Provider: "openai"}, time.Now())
			if tt.wantExceeded {
				var exceeded *ExceededError
				require.ErrorAs(t, err, &exceeded)
				assert.Equal(t, "acme", exceeded.Result.Budget.Subject)
			} else {
				require.NoError(t, err)
			}
			assert.Equal(t, tt.wantStoreCall, store.sumCalls > 0)
		})
	}
}

func TestSeedConfiguredBudgetsCarriesCostBasis(t *testing.T) {
	ctx := context.Background()
	store := &fakeStore{}
	err := seedConfiguredBudgets(ctx, store, config.BudgetsConfig{
		Labels: []config.BudgetLabelConfig{{Label: "acme", Limits: []config.BudgetLimitConfig{
			{Period: "monthly", Amount: 500, CostBasis: "billed"},
			{Period: "daily", Amount: 20},
		}}},
	}, allCapabilities)
	require.NoError(t, err)
	require.Len(t, store.replacedBudgets, 2)
	assert.Equal(t, CostBasisBilled, store.replacedBudgets[0].CostBasis)
	assert.Equal(t, CostBasisProvider, store.replacedBudgets[1].CostBasis)

	err = seedConfiguredBudgets(ctx, store, config.BudgetsConfig{
		Labels: []config.BudgetLabelConfig{{Label: "acme", Limits: []config.BudgetLimitConfig{
			{Period: "monthly", Amount: 500, CostBasis: "billed"},
		}}},
	}, capabilities{quotaTemplates: true})
	require.ErrorIs(t, err, ErrBilledBudgetsUnavailable)
	assert.Equal(t, 1, store.replaceCalls)
}

func newSQLiteStorage(t *testing.T) storage.Storage {
	t.Helper()
	shared, err := storage.NewSQLite(storage.SQLiteConfig{Path: filepath.Join(t.TempDir(), "budgets.db")})
	require.NoError(t, err)
	t.Cleanup(func() { _ = shared.Close() })
	return shared
}

func billedLabelConfig(basis string) []config.BudgetLabelConfig {
	return []config.BudgetLabelConfig{{Label: "acme", Limits: []config.BudgetLimitConfig{
		{Period: "monthly", Amount: 500, CostBasis: basis},
	}}}
}

// A billed budget in the current config aborts startup unless the capability
// is present and markups are enabled, mirroring per-child quota templates.
func TestNewGatesConfiguredBilledBudgets(t *testing.T) {
	tests := []struct {
		name       string
		capability bool
		markups    bool
		wantErr    bool
	}{
		{name: "capability and markups", capability: true, markups: true},
		{name: "no capability", capability: false, markups: true, wantErr: true},
		{name: "markups disabled", capability: true, markups: false, wantErr: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := &config.Config{}
			cfg.Budgets.Enabled = true
			cfg.Pricing.MarkupsEnabled = tt.markups
			cfg.Budgets.Labels = billedLabelConfig("billed")

			result, err := New(context.Background(), cfg, newSQLiteStorage(t), false, tt.capability)
			if tt.wantErr {
				require.ErrorIs(t, err, ErrBilledBudgetsUnavailable)
				return
			}
			require.NoError(t, err)
			t.Cleanup(func() { _ = result.Close() })
			budgets := result.Service.Budgets()
			require.Len(t, budgets, 1)
			assert.Equal(t, CostBasisBilled, budgets[0].CostBasis)
		})
	}
}

// Restarting after billed budgets become unavailable must start: a config
// change that drops or switches the billed budget applies, and a manual billed
// budget keeps limiting spend on provider cost.
func TestNewStartsWithStoredBilledBudgetsAfterEntitlementLoss(t *testing.T) {
	tests := []struct {
		name      string
		nextLabel []config.BudgetLabelConfig
		manual    bool
		stored    []CostBasis
		enforced  []CostBasis
	}{
		{name: "config budget switched to provider", nextLabel: billedLabelConfig("provider"), stored: []CostBasis{CostBasisProvider}, enforced: []CostBasis{CostBasisProvider}},
		{name: "config budget removed"},
		{name: "manual billed budget", manual: true, stored: []CostBasis{CostBasisBilled}, enforced: []CostBasis{CostBasisProvider}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			shared := newSQLiteStorage(t)

			cfg := &config.Config{}
			cfg.Budgets.Enabled = true
			cfg.Pricing.MarkupsEnabled = true
			if !tt.manual {
				cfg.Budgets.Labels = billedLabelConfig("billed")
			}
			first, err := New(ctx, cfg, shared, false, true)
			require.NoError(t, err)
			if tt.manual {
				require.NoError(t, first.Service.UpsertBudgets(ctx, []Budget{{
					Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled,
					PeriodSeconds: PeriodMonthlySeconds, Amount: 500, Source: SourceManual,
				}}))
			}
			require.NoError(t, first.Close())

			cfg.Pricing.MarkupsEnabled = false
			cfg.Budgets.Labels = tt.nextLabel
			next, err := New(ctx, cfg, shared, false, false)
			require.NoError(t, err)
			t.Cleanup(func() { _ = next.Close() })

			var stored, enforced []CostBasis
			for _, item := range next.Service.Budgets() {
				stored = append(stored, item.CostBasis)
				enforced = append(enforced, item.EnforcedBasis())
			}
			assert.Equal(t, tt.stored, stored)
			assert.Equal(t, tt.enforced, enforced)
		})
	}
}

// billedSpendEntries covers every row shape a billed window must handle: a
// marked-up row, a row written before markups were enabled (no billed cost), a
// billed and an unbilled response-cache hit, a pre-markup cache hit, and a row
// of unknown cost.
func billedSpendEntries(now time.Time) []*usage.UsageEntry {
	withBilled := func(entry *usage.UsageEntry, billed float64) *usage.UsageEntry {
		entry.BilledCost = &billed
		return entry
	}
	unknown := usageEntryWithCost("unknown", "/acme", "", now, 0, "acme")
	unknown.InputCost, unknown.OutputCost, unknown.TotalCost = nil, nil, nil
	return []*usage.UsageEntry{
		withBilled(usageEntryWithCost("marked-up", "/acme", "", now, 1, "acme"), 1.25),
		usageEntryWithCost("pre-markup", "/acme", "", now, 2, "acme"),
		withBilled(usageEntryWithCost("billed-hit", "/acme", usage.CacheTypeExact, now, 4, "acme"), 4.5),
		withBilled(usageEntryWithCost("free-hit", "/acme", usage.CacheTypeSemantic, now, 8, "acme"), 0),
		usageEntryWithCost("pre-markup-hit", "/acme", usage.CacheTypeExact, now, 16, "acme"),
		unknown,
		withBilled(usageEntryWithCost("only-free-hit", "/cache-only", usage.CacheTypeExact, now, 3, "cache-only"), 0),
	}
}

// assertBilledSpend checks provider and billed windows over billedSpendEntries
// in one batch, so a mixed batch is covered too.
func assertBilledSpend(t *testing.T, store Store, now time.Time) {
	t.Helper()
	start, end := now.Add(-time.Hour), now.Add(time.Hour)
	windows := []SpendWindow{
		{Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisProvider, Start: start, End: end},
		{Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled, Start: start, End: end},
		{Scope: ScopeUserPath, Subject: "/acme", CostBasis: CostBasisBilled, Start: start, End: end},
		{Scope: ScopeLabel, Subject: "cache-only", CostBasis: CostBasisProvider, Start: start, End: end},
		{Scope: ScopeLabel, Subject: "cache-only", CostBasis: CostBasisBilled, Start: start, End: end},
		{Scope: ScopeLabel, Subject: "absent", CostBasis: CostBasisBilled, Start: start, End: end},
		{Scope: ScopeProvider, Subject: "test", CostBasis: CostBasisBilled, Start: start, End: end},
		{Scope: ScopeProvider, Subject: "test", CostBasis: CostBasisBilled, Start: start, End: end, ExcludeCacheHits: true},
		{Scope: ScopeLabel, Subject: "cache-only", CostBasis: CostBasisBilled, Start: start, End: end, ExcludeCacheHits: true},
	}
	want := []Spend{
		{Total: 3, HasUsage: true},    // provider: uncached marked-up + pre-markup
		{Total: 7.75, HasUsage: true}, // billed: 1.25 + 2 (fallback) + 4.5 (billed hit) + 0
		{Total: 7.75, HasUsage: true}, // same rows by user path
		{},                            // provider never counts cache hits
		{Total: 0, HasUsage: true},    // an unbilled hit is billed usage worth 0
		{},
		{Total: 7.75, HasUsage: true}, // billed provider budget counts billed hits by default
		{Total: 3.25, HasUsage: true}, // without them: 1.25 + 2 (fallback)
		{},                            // nothing left once the only rows are hits
	}

	got, err := store.SumSpend(context.Background(), windows)
	require.NoError(t, err)
	require.Len(t, got, len(want))
	for i := range want {
		assert.Equal(t, want[i].HasUsage, got[i].HasUsage, "spend[%d] has usage", i)
		assert.InDelta(t, want[i].Total, got[i].Total, 1e-9, "spend[%d] total", i)
	}
}

func TestServiceExcludesBilledCacheHitsFromProviderBudgetsWhenConfigured(t *testing.T) {
	budgets := []Budget{
		{Scope: ScopeProvider, Subject: "openai", CostBasis: CostBasisBilled, PeriodSeconds: PeriodDailySeconds, Amount: 10},
		{Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled, PeriodSeconds: PeriodDailySeconds, Amount: 10},
	}
	for _, tt := range []struct {
		name    string
		options []ServiceOption
		exclude map[Scope]bool
	}{
		{name: "default counts hits", exclude: map[Scope]bool{ScopeProvider: false, ScopeLabel: false}},
		{name: "disabled excludes provider hits", options: []ServiceOption{WithProviderBilledCacheHits(false)}, exclude: map[Scope]bool{ScopeProvider: true, ScopeLabel: false}},
	} {
		t.Run(tt.name, func(t *testing.T) {
			store := &fakeStore{budgets: budgets}
			options := append([]ServiceOption{WithBilledBudgets(true)}, tt.options...)
			service, err := NewService(context.Background(), store, options...)
			require.NoError(t, err)
			_, err = service.Statuses(context.Background(), time.Now())
			require.NoError(t, err)
			got := map[Scope]bool{}
			for _, window := range store.lastWindows {
				got[window.Scope] = window.ExcludeCacheHits
			}
			assert.Equal(t, tt.exclude, got)
		})
	}
}

func TestSQLStoreSumSpendOnBilledCost(t *testing.T) {
	sqlxtest.Run(t, func(t *testing.T, db sqlx.DB) {
		ctx := context.Background()
		usageStore, err := usage.NewSQLStore(ctx, db, 0)
		require.NoError(t, err)
		now := time.Date(2026, time.April, 25, 12, 0, 0, 0, time.UTC)
		require.NoError(t, usageStore.WriteBatch(ctx, billedSpendEntries(now)))

		store, err := NewSQLStore(ctx, db)
		require.NoError(t, err)
		assertBilledSpend(t, store, now)
	})
}

func TestMongoDBStoreSumSpendOnBilledCost(t *testing.T) {
	mongotest.Run(t, func(t *testing.T, db *mongo.Database) {
		ctx := context.Background()
		usageStore, err := usage.NewMongoDBStore(db, 0)
		require.NoError(t, err)
		now := time.Date(2026, time.April, 25, 12, 0, 0, 0, time.UTC)
		require.NoError(t, usageStore.WriteBatch(ctx, billedSpendEntries(now)))

		store, err := NewMongoDBStore(ctx, db)
		require.NoError(t, err)
		assertBilledSpend(t, store, now)
	})
}

func TestSQLStoreRoundTripsCostBasis(t *testing.T) {
	runSQLStoreTest(t, func(t *testing.T, store *SQLStore) {
		assertStoreRoundTripsCostBasis(t, store)
	})
}

func TestMongoDBStoreRoundTripsCostBasis(t *testing.T) {
	mongotest.Run(t, func(t *testing.T, db *mongo.Database) {
		store, err := NewMongoDBStore(context.Background(), db)
		require.NoError(t, err)
		assertStoreRoundTripsCostBasis(t, store)
	})
}

// assertStoreRoundTripsCostBasis persists both bases, then checks that a
// config re-seed leaves a manual budget's basis alone while updating a
// config-sourced one.
func assertStoreRoundTripsCostBasis(t *testing.T, store Store) {
	t.Helper()
	ctx := context.Background()
	err := store.UpsertBudgets(ctx, []Budget{
		{Scope: ScopeLabel, Subject: "acme", CostBasis: CostBasisBilled, PeriodSeconds: PeriodMonthlySeconds, Amount: 100, Source: SourceManual},
		{Scope: ScopeLabel, Subject: "beta", PeriodSeconds: PeriodMonthlySeconds, Amount: 50, Source: SourceConfig},
	})
	require.NoError(t, err)
	err = store.ReplaceConfigBudgets(ctx, []Budget{
		{Scope: ScopeLabel, Subject: "acme", PeriodSeconds: PeriodMonthlySeconds, Amount: 1},
		{Scope: ScopeLabel, Subject: "beta", CostBasis: CostBasisBilled, PeriodSeconds: PeriodMonthlySeconds, Amount: 50},
	})
	require.NoError(t, err)

	budgets, err := store.ListBudgets(ctx)
	require.NoError(t, err)
	bases := map[string]CostBasis{}
	for _, item := range budgets {
		bases[item.Subject] = item.CostBasis
	}
	assert.Equal(t, map[string]CostBasis{"acme": CostBasisBilled, "beta": CostBasisBilled}, bases)

	err = store.UpsertBudgets(ctx, []Budget{
		{Scope: ScopeLabel, Subject: "acme", PeriodSeconds: PeriodMonthlySeconds, Amount: 100, Source: SourceManual},
	})
	require.NoError(t, err)
	budgets, err = store.ListBudgets(ctx)
	require.NoError(t, err)
	for _, item := range budgets {
		if item.Subject == "acme" {
			assert.Equal(t, CostBasisProvider, item.CostBasis)
		}
	}
}

// Budgets stored before cost bases existed keep limiting provider cost.
func TestSQLStoreMigratesPreCostBasisRows(t *testing.T) {
	sqlxtest.Run(t, func(t *testing.T, db sqlx.DB) {
		ctx := context.Background()
		err := db.Schema(ctx, `CREATE TABLE budgets (
			scope TEXT NOT NULL DEFAULT 'user_path',
			subject TEXT NOT NULL,
			per_child `+sqlx.TypeBool+` NOT NULL DEFAULT FALSE,
			period_seconds `+sqlx.TypeInt64+` NOT NULL,
			amount `+sqlx.TypeFloat+` NOT NULL,
			source TEXT NOT NULL DEFAULT '',
			last_reset_at `+sqlx.TypeInt64+`,
			created_at `+sqlx.TypeInt64+` NOT NULL,
			updated_at `+sqlx.TypeInt64+` NOT NULL,
			PRIMARY KEY (scope, subject, period_seconds)
		)`)
		require.NoError(t, err)
		_, err = db.Exec(ctx, `
			INSERT INTO budgets (scope, subject, period_seconds, amount, source, created_at, updated_at)
			VALUES (?, ?, ?, ?, ?, ?, ?)
		`, ScopeUserPath, "/legacy", PeriodDailySeconds, 10.0, SourceManual, int64(1), int64(1))
		require.NoError(t, err)

		store, err := NewSQLStore(ctx, db)
		require.NoError(t, err)
		budgets, err := store.ListBudgets(ctx)
		require.NoError(t, err)
		require.Len(t, budgets, 1)
		assert.Equal(t, CostBasisProvider, budgets[0].CostBasis)
	})
}

func TestMongoDBStoreReadsPreCostBasisDocumentsAsProvider(t *testing.T) {
	mongotest.Run(t, func(t *testing.T, db *mongo.Database) {
		ctx := context.Background()
		now := time.Date(2026, time.April, 25, 12, 0, 0, 0, time.UTC)
		_, err := db.Collection("budgets").InsertOne(ctx, bson.D{
			{Key: "scope", Value: string(ScopeUserPath)}, {Key: "subject", Value: "/legacy"},
			{Key: "period_seconds", Value: PeriodDailySeconds}, {Key: "amount", Value: 10.0},
			{Key: "source", Value: SourceManual}, {Key: "created_at", Value: now}, {Key: "updated_at", Value: now},
		})
		require.NoError(t, err)

		store, err := NewMongoDBStore(ctx, db)
		require.NoError(t, err)
		budgets, err := store.ListBudgets(ctx)
		require.NoError(t, err)
		require.Len(t, budgets, 1)
		assert.Equal(t, CostBasisProvider, budgets[0].CostBasis)
	})
}