Files
2026-07-13 12:35:43 +08:00

305 lines
8.9 KiB
Go

package auth
import (
"context"
"errors"
"os"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
)
type recordingCooldownStateStore struct {
saveCount atomic.Int32
mu sync.Mutex
records []CooldownStateRecord
load []CooldownStateRecord
}
func (s *recordingCooldownStateStore) Load(context.Context) ([]CooldownStateRecord, error) {
s.mu.Lock()
defer s.mu.Unlock()
return cloneCooldownStateRecords(s.load), nil
}
func (s *recordingCooldownStateStore) Save(_ context.Context, records []CooldownStateRecord) error {
s.saveCount.Add(1)
s.mu.Lock()
defer s.mu.Unlock()
s.records = cloneCooldownStateRecords(records)
return nil
}
func cloneCooldownStateRecords(records []CooldownStateRecord) []CooldownStateRecord {
if len(records) == 0 {
return nil
}
cloned := make([]CooldownStateRecord, len(records))
for i := range records {
cloned[i] = records[i]
cloned[i].LastError = cloneError(records[i].LastError)
}
return cloned
}
func TestFileCooldownStateStore_StateRelativePath(t *testing.T) {
authDir := filepath.Join(t.TempDir(), "auths")
store := NewFileCooldownStateStoreWithAuthDir(authDir, authDir)
cases := []struct {
name string
record CooldownStateRecord
want string
}{
{
name: "absolute auth file under auth dir",
record: CooldownStateRecord{
AuthID: "auth-1",
AuthFile: filepath.Join(authDir, "nested", "xai.json"),
},
want: filepath.Join("nested", "xai.cds"),
},
{
name: "relative auth file",
record: CooldownStateRecord{
AuthID: "auth-2",
AuthFile: filepath.Join("team", "xai.json"),
},
want: filepath.Join("team", "xai.cds"),
},
{
name: "absolute auth file outside auth dir",
record: CooldownStateRecord{
AuthID: "auth-3",
AuthFile: filepath.Join(t.TempDir(), "outside.json"),
},
want: "outside.cds",
},
{
name: "relative parent escape is rejected",
record: CooldownStateRecord{
AuthID: "auth-4",
AuthFile: filepath.Join("..", "escape.json"),
},
want: "",
},
{
name: "auth id fallback",
record: CooldownStateRecord{
AuthID: "auth/id 5",
},
want: "auth_id_5.cds",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := store.stateRelativePath(tc.record); got != tc.want {
t.Fatalf("stateRelativePath() = %q, want %q", got, tc.want)
}
})
}
}
func TestFileCooldownStateStore_SaveLoadAndCleanStale(t *testing.T) {
authDir := t.TempDir()
store := NewFileCooldownStateStoreWithAuthDir(authDir, authDir)
ctx := context.Background()
stalePath := filepath.Join(authDir, "stale.cds")
if errWrite := os.WriteFile(stalePath, []byte("{}\n"), 0o600); errWrite != nil {
t.Fatalf("write stale file: %v", errWrite)
}
nextRetry := time.Now().Add(time.Hour).UTC().Truncate(time.Second)
updatedAt := time.Now().UTC().Truncate(time.Second)
record := CooldownStateRecord{
Provider: "xai",
AuthID: "auth-1",
AuthFile: filepath.Join(authDir, "xai.json"),
Model: "grok-4",
Status: "cooling",
NextRetryAfter: nextRetry,
Reason: "quota",
Quota: QuotaState{
Exceeded: true,
Reason: "quota",
NextRecoverAt: nextRetry,
BackoffLevel: 1,
},
LastError: &Error{Message: "rate limited", HTTPStatus: 429},
UpdatedAt: updatedAt,
}
if errSave := store.Save(ctx, []CooldownStateRecord{record}); errSave != nil {
t.Fatalf("Save() returned error: %v", errSave)
}
if _, errStat := os.Stat(filepath.Join(authDir, "xai.cds")); errStat != nil {
t.Fatalf("expected xai.cds to exist: %v", errStat)
}
if _, errStat := os.Stat(stalePath); !errors.Is(errStat, os.ErrNotExist) {
t.Fatalf("expected stale.cds to be removed, stat error = %v", errStat)
}
loaded, errLoad := store.Load(ctx)
if errLoad != nil {
t.Fatalf("Load() returned error: %v", errLoad)
}
if len(loaded) != 1 {
t.Fatalf("loaded records = %d, want 1", len(loaded))
}
if loaded[0].AuthID != record.AuthID || loaded[0].Model != record.Model || !loaded[0].NextRetryAfter.Equal(nextRetry) {
t.Fatalf("loaded record = %+v, want auth/model/retry from %+v", loaded[0], record)
}
if loaded[0].LastError == nil || loaded[0].LastError.HTTPStatus != 429 {
t.Fatalf("loaded last error = %+v, want HTTP 429", loaded[0].LastError)
}
if errSave := store.Save(ctx, nil); errSave != nil {
t.Fatalf("Save(nil) returned error: %v", errSave)
}
if _, errStat := os.Stat(filepath.Join(authDir, "xai.cds")); !errors.Is(errStat, os.ErrNotExist) {
t.Fatalf("expected xai.cds to be removed, stat error = %v", errStat)
}
}
func TestFileCooldownStateStore_ConcurrentSave(t *testing.T) {
authDir := t.TempDir()
store := NewFileCooldownStateStoreWithAuthDir(authDir, authDir)
ctx := context.Background()
nextRetry := time.Now().Add(time.Hour).UTC().Truncate(time.Second)
var wg sync.WaitGroup
errs := make(chan error, 16)
for i := 0; i < 16; i++ {
i := i
wg.Add(1)
go func() {
defer wg.Done()
errs <- store.Save(ctx, []CooldownStateRecord{
{
Provider: "xai",
AuthID: "auth-1",
AuthFile: filepath.Join(authDir, "xai.json"),
Model: "grok-4",
Status: "cooling",
NextRetryAfter: nextRetry.Add(time.Duration(i) * time.Second),
UpdatedAt: nextRetry,
},
})
}()
}
wg.Wait()
close(errs)
for errSave := range errs {
if errSave != nil {
t.Fatalf("Save() returned error: %v", errSave)
}
}
loaded, errLoad := store.Load(ctx)
if errLoad != nil {
t.Fatalf("Load() returned error: %v", errLoad)
}
if len(loaded) != 1 {
t.Fatalf("loaded records = %d, want 1", len(loaded))
}
tmpMatches, errGlob := filepath.Glob(filepath.Join(authDir, "*.tmp"))
if errGlob != nil {
t.Fatalf("glob temp files: %v", errGlob)
}
if len(tmpMatches) != 0 {
t.Fatalf("leftover temp files = %v, want none", tmpMatches)
}
}
func TestManager_MarkResult_PersistsCooldownOnlyWhenStateChanges(t *testing.T) {
store := &recordingCooldownStateStore{}
manager := NewManager(nil, nil, nil)
manager.SetCooldownStateStore(store)
auth := &Auth{ID: "auth-1", Provider: "xai", Status: StatusActive}
if _, errRegister := manager.Register(WithSkipPersist(context.Background()), auth); errRegister != nil {
t.Fatalf("Register() returned error: %v", errRegister)
}
manager.MarkResult(context.Background(), Result{AuthID: auth.ID, Provider: "xai", Model: "grok-4", Success: true})
if got := store.saveCount.Load(); got != 0 {
t.Fatalf("healthy success saved cooldown state %d times, want 0", got)
}
manager.MarkResult(context.Background(), Result{
AuthID: auth.ID,
Provider: "xai",
Model: "grok-4",
Success: false,
Error: &Error{Message: "upstream unavailable", HTTPStatus: 500},
})
if got := store.saveCount.Load(); got != 1 {
t.Fatalf("cooldown failure saved cooldown state %d times, want 1", got)
}
manager.MarkResult(context.Background(), Result{AuthID: auth.ID, Provider: "xai", Model: "grok-4", Success: true})
if got := store.saveCount.Load(); got != 2 {
t.Fatalf("cooldown clear saved cooldown state %d times, want 2", got)
}
manager.MarkResult(context.Background(), Result{AuthID: auth.ID, Provider: "xai", Model: "grok-4", Success: true})
if got := store.saveCount.Load(); got != 2 {
t.Fatalf("clean success saved cooldown state %d times, want 2", got)
}
}
func TestManager_RestoreCooldownStates(t *testing.T) {
nextRetry := time.Now().Add(time.Hour).UTC().Truncate(time.Second)
store := &recordingCooldownStateStore{
load: []CooldownStateRecord{
{
Provider: "xai",
AuthID: "auth-1",
Model: "grok-4",
Status: "cooling",
NextRetryAfter: nextRetry,
Reason: "quota",
Quota: QuotaState{
Exceeded: true,
Reason: "quota",
NextRecoverAt: nextRetry,
},
LastError: &Error{Message: "rate limited", HTTPStatus: 429},
UpdatedAt: nextRetry.Add(-time.Minute),
},
},
}
manager := NewManager(nil, nil, nil)
manager.SetCooldownStateStore(store)
if _, errRegister := manager.Register(WithSkipPersist(context.Background()), &Auth{ID: "auth-1", Provider: "xai"}); errRegister != nil {
t.Fatalf("Register() returned error: %v", errRegister)
}
if errRestore := manager.RestoreCooldownStates(context.Background()); errRestore != nil {
t.Fatalf("RestoreCooldownStates() returned error: %v", errRestore)
}
auth, ok := manager.GetByID("auth-1")
if !ok {
t.Fatal("restored auth was not found")
}
state := auth.ModelStates["grok-4"]
if state == nil {
t.Fatal("model state was not restored")
}
if !state.Unavailable || state.Status != StatusError || !state.NextRetryAfter.Equal(nextRetry) {
t.Fatalf("restored state = %+v, want unavailable status error until %v", state, nextRetry)
}
if state.LastError == nil || state.LastError.HTTPStatus != 429 {
t.Fatalf("restored last error = %+v, want HTTP 429", state.LastError)
}
if got := store.saveCount.Load(); got != 1 {
t.Fatalf("restore cleanup saved cooldown state %d times, want 1", got)
}
}