capsule AI-native Unix-like composition layer

src/internal/quota/store_test.go

4,894 bytes · 167 lines · capsule://quake0day/[email protected] raw on github

package quota

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

	"go.etcd.io/bbolt"
)

const testFingerprint = "9a3f2c1234567890d8e1"

func TestGetMissingFingerprintReturnsZero(t *testing.T) {
	t.Parallel()
	store := newTestStore(t, time.Date(2026, 5, 23, 10, 0, 0, 0, time.Local))

	got, err := store.Get(testFingerprint)
	if err != nil {
		t.Fatalf("Get returned error: %v", err)
	}
	if got.Balance != 0 || got.SignedToday {
		t.Fatalf("Get() = %+v, want zero unsigned quota", got)
	}
	if got.Cap != BalanceCap || got.DailyGrant != DailyGrant {
		t.Fatalf("Get() metadata = %+v, want quota constants", got)
	}
}

func TestCheckInGrantsDailyCreditsOnce(t *testing.T) {
	t.Parallel()
	now := time.Date(2026, 5, 23, 10, 0, 0, 0, time.Local)
	store := newTestStore(t, now)

	first, err := store.ApplyCheckIn(testFingerprint)
	if err != nil {
		t.Fatalf("ApplyCheckIn returned error: %v", err)
	}
	if first.Balance != DailyGrant || !first.SignedToday {
		t.Fatalf("first check-in = %+v, want %d signed credits", first, DailyGrant)
	}

	second, err := store.ApplyCheckIn(testFingerprint)
	if err != nil {
		t.Fatalf("second ApplyCheckIn returned error: %v", err)
	}
	if second.Balance != DailyGrant || !second.SignedToday {
		t.Fatalf("second check-in = %+v, want unchanged signed credits", second)
	}
}

func TestCheckInCapsBalanceAtOneHundred(t *testing.T) {
	t.Parallel()
	now := time.Date(2026, 5, 23, 10, 0, 0, 0, time.Local)
	store := newTestStore(t, now)
	writeTestAccount(t, store, testFingerprint, account{
		Balance:         95,
		LastCheckInDate: "2026-05-22",
		CreatedAt:       "2026-05-22T10:00:00+08:00",
		UpdatedAt:       "2026-05-22T10:00:00+08:00",
	})

	got, err := store.ApplyCheckIn(testFingerprint)
	if err != nil {
		t.Fatalf("ApplyCheckIn returned error: %v", err)
	}
	if got.Balance != BalanceCap || !got.SignedToday {
		t.Fatalf("ApplyCheckIn = %+v, want capped signed balance", got)
	}
}

func TestCheckInAtCapDoesNotConsumeToday(t *testing.T) {
	t.Parallel()
	now := time.Date(2026, 5, 23, 10, 0, 0, 0, time.Local)
	store := newTestStore(t, now)
	writeTestAccount(t, store, testFingerprint, account{
		Balance:         BalanceCap,
		LastCheckInDate: "2026-05-22",
		CreatedAt:       "2026-05-22T10:00:00+08:00",
		UpdatedAt:       "2026-05-22T10:00:00+08:00",
	})

	got, err := store.ApplyCheckIn(testFingerprint)
	if err != nil {
		t.Fatalf("ApplyCheckIn returned error: %v", err)
	}
	if got.Balance != BalanceCap || got.SignedToday {
		t.Fatalf("ApplyCheckIn = %+v, want full balance without today's sign-in", got)
	}

	spend, afterSpend, err := store.Spend(testFingerprint)
	if err != nil {
		t.Fatalf("Spend returned error: %v", err)
	}
	if afterSpend.Balance != BalanceCap-1 || afterSpend.SignedToday {
		t.Fatalf("Spend = %+v, want one credit spent and still unsigned today", afterSpend)
	}
	t.Cleanup(func() {
		if err := spend.Refund(); err != nil {
			t.Fatalf("Refund returned error: %v", err)
		}
	})

	afterCheckIn, err := store.ApplyCheckIn(testFingerprint)
	if err != nil {
		t.Fatalf("ApplyCheckIn after spend returned error: %v", err)
	}
	if afterCheckIn.Balance != BalanceCap || !afterCheckIn.SignedToday {
		t.Fatalf("ApplyCheckIn after spend = %+v, want today's sign-in restored to cap", afterCheckIn)
	}
}

func TestSpendRequiresCreditsAndRefunds(t *testing.T) {
	t.Parallel()
	store := newTestStore(t, time.Date(2026, 5, 23, 10, 0, 0, 0, time.Local))

	if _, _, err := store.Spend(testFingerprint); !errors.Is(err, ErrNoCredits) {
		t.Fatalf("Spend without credits error = %v, want ErrNoCredits", err)
	}

	if _, err := store.ApplyCheckIn(testFingerprint); err != nil {
		t.Fatalf("ApplyCheckIn returned error: %v", err)
	}
	spend, afterSpend, err := store.Spend(testFingerprint)
	if err != nil {
		t.Fatalf("Spend returned error: %v", err)
	}
	if afterSpend.Balance != DailyGrant-1 {
		t.Fatalf("Spend balance = %d, want %d", afterSpend.Balance, DailyGrant-1)
	}

	if err := spend.Refund(); err != nil {
		t.Fatalf("Refund returned error: %v", err)
	}
	afterRefund, err := store.Get(testFingerprint)
	if err != nil {
		t.Fatalf("Get after refund returned error: %v", err)
	}
	if afterRefund.Balance != DailyGrant {
		t.Fatalf("Refund balance = %d, want %d", afterRefund.Balance, DailyGrant)
	}
}

func newTestStore(t *testing.T, now time.Time) *Store {
	t.Helper()
	store, err := Open(filepath.Join(t.TempDir(), "quota.db"))
	if err != nil {
		t.Fatalf("Open test store: %v", err)
	}
	store.now = func() time.Time { return now }
	t.Cleanup(func() {
		if err := store.Close(); err != nil {
			t.Fatalf("Close test store: %v", err)
		}
	})
	return store
}

func writeTestAccount(t *testing.T, store *Store, fingerprint string, acct account) {
	t.Helper()
	if err := store.db.Update(func(tx *bbolt.Tx) error {
		return writeAccount(tx.Bucket(bucketAccounts), fingerprint, acct)
	}); err != nil {
		t.Fatalf("write test account: %v", err)
	}
}