sub2api/backend/internal/service/scheduler_snapshot_hydration_test.go
moonagic a01686c637
fix antigravity gemini rate limit and account scheduling
Squash of 4 commits:
- Fix Gemini rate limit scheduling
- fix antigravity gemini rate limit scheduling
- fix antigravity gemini limited account scheduling
- fix antigravity test stubs for default lint
2026-05-31 22:00:03 +08:00

277 lines
7.4 KiB
Go

//go:build unit
package service
import (
"context"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
)
type snapshotHydrationCache struct {
snapshot []*Account
accounts map[int64]*Account
}
func (c *snapshotHydrationCache) GetSnapshot(ctx context.Context, bucket SchedulerBucket) ([]*Account, bool, error) {
return c.snapshot, true, nil
}
func (c *snapshotHydrationCache) SetSnapshot(ctx context.Context, bucket SchedulerBucket, accounts []Account) error {
return nil
}
func (c *snapshotHydrationCache) GetAccount(ctx context.Context, accountID int64) (*Account, error) {
if c.accounts == nil {
return nil, nil
}
return c.accounts[accountID], nil
}
func (c *snapshotHydrationCache) SetAccount(ctx context.Context, account *Account) error {
return nil
}
func (c *snapshotHydrationCache) DeleteAccount(ctx context.Context, accountID int64) error {
return nil
}
func (c *snapshotHydrationCache) UpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error {
return nil
}
func (c *snapshotHydrationCache) TryLockBucket(ctx context.Context, bucket SchedulerBucket, ttl time.Duration) (bool, error) {
return true, nil
}
func (c *snapshotHydrationCache) UnlockBucket(ctx context.Context, bucket SchedulerBucket) error {
return nil
}
func (c *snapshotHydrationCache) ListBuckets(ctx context.Context) ([]SchedulerBucket, error) {
return nil, nil
}
func (c *snapshotHydrationCache) GetOutboxWatermark(ctx context.Context) (int64, error) {
return 0, nil
}
func (c *snapshotHydrationCache) SetOutboxWatermark(ctx context.Context, id int64) error {
return nil
}
func TestOpenAISelectAccountWithLoadAwareness_HydratesSelectedAccountFromSchedulerSnapshot(t *testing.T) {
cache := &snapshotHydrationCache{
snapshot: []*Account{
{
ID: 1,
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Status: StatusActive,
Schedulable: true,
Concurrency: 1,
Priority: 1,
Credentials: map[string]any{
"model_mapping": map[string]any{
"gpt-4": "gpt-4",
},
},
},
},
accounts: map[int64]*Account{
1: {
ID: 1,
Platform: PlatformOpenAI,
Type: AccountTypeAPIKey,
Status: StatusActive,
Schedulable: true,
Concurrency: 1,
Priority: 1,
Credentials: map[string]any{
"api_key": "sk-live",
"model_mapping": map[string]any{"gpt-4": "gpt-4"},
},
},
},
}
schedulerSnapshot := NewSchedulerSnapshotService(cache, nil, nil, nil, nil)
groupID := int64(2)
svc := &OpenAIGatewayService{
schedulerSnapshot: schedulerSnapshot,
cache: &stubGatewayCache{},
}
selection, err := svc.SelectAccountWithLoadAwareness(context.Background(), &groupID, "", "gpt-4", nil)
if err != nil {
t.Fatalf("SelectAccountWithLoadAwareness error: %v", err)
}
if selection == nil || selection.Account == nil {
t.Fatalf("expected selected account")
}
if got := selection.Account.GetOpenAIApiKey(); got != "sk-live" {
t.Fatalf("expected hydrated api key, got %q", got)
}
}
func TestOpenAINewAcquiredSelectionResult_ReleasesSlotWhenHydrationFails(t *testing.T) {
cache := &snapshotHydrationCache{
accounts: map[int64]*Account{},
}
schedulerSnapshot := NewSchedulerSnapshotService(cache, nil, stubOpenAIAccountRepo{}, nil, nil)
svc := &OpenAIGatewayService{
schedulerSnapshot: schedulerSnapshot,
}
releaseCalls := 0
selection, err := svc.newAcquiredSelectionResult(context.Background(), &Account{ID: 1001}, func() {
releaseCalls++
})
if err == nil {
t.Fatalf("expected hydration error")
}
if selection != nil {
t.Fatalf("expected nil selection on hydration error")
}
if releaseCalls != 1 {
t.Fatalf("expected release to be called once, got %d", releaseCalls)
}
}
func TestGatewaySelectAccountWithLoadAwareness_HydratesSelectedAccountFromSchedulerSnapshot(t *testing.T) {
cache := &snapshotHydrationCache{
snapshot: []*Account{
{
ID: 9,
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
Status: StatusActive,
Schedulable: true,
Concurrency: 1,
Priority: 1,
},
},
accounts: map[int64]*Account{
9: {
ID: 9,
Platform: PlatformAnthropic,
Type: AccountTypeAPIKey,
Status: StatusActive,
Schedulable: true,
Concurrency: 1,
Priority: 1,
Credentials: map[string]any{
"api_key": "anthropic-live-key",
},
},
},
}
schedulerSnapshot := NewSchedulerSnapshotService(cache, nil, nil, nil, nil)
svc := &GatewayService{
schedulerSnapshot: schedulerSnapshot,
cache: &mockGatewayCacheForPlatform{},
cfg: testConfig(),
}
result, err := svc.SelectAccountWithLoadAwareness(context.Background(), nil, "", "claude-3-5-sonnet-20241022", nil, "", 0)
if err != nil {
t.Fatalf("SelectAccountWithLoadAwareness error: %v", err)
}
if result == nil || result.Account == nil {
t.Fatalf("expected selected account")
}
if got := result.Account.GetCredential("api_key"); got != "anthropic-live-key" {
t.Fatalf("expected hydrated api key, got %q", got)
}
}
func TestGatewaySelectAccountWithLoadAwareness_SkipsAntigravityGeminiFamilyRateLimitedSnapshot(t *testing.T) {
resetAt := time.Now().Add(10 * time.Minute).Format(time.RFC3339)
cache := &snapshotHydrationCache{
snapshot: []*Account{
{
ID: 1,
Platform: PlatformAntigravity,
Type: AccountTypeOAuth,
Status: StatusActive,
Schedulable: true,
Concurrency: 1,
Priority: 1,
AccountGroups: []AccountGroup{
{AccountID: 1, GroupID: 22},
},
GroupIDs: []int64{22},
Extra: map[string]any{
"mixed_scheduling": true,
modelRateLimitsKey: map[string]any{
antigravityGeminiModelRateLimitKey: map[string]any{
"rate_limit_reset_at": resetAt,
},
},
},
},
{
ID: 2,
Platform: PlatformAntigravity,
Type: AccountTypeOAuth,
Status: StatusActive,
Schedulable: true,
Concurrency: 1,
Priority: 2,
AccountGroups: []AccountGroup{
{AccountID: 2, GroupID: 22},
},
GroupIDs: []int64{22},
Extra: map[string]any{
"mixed_scheduling": true,
},
},
},
accounts: map[int64]*Account{
1: {ID: 1, Platform: PlatformAntigravity, Type: AccountTypeOAuth},
2: {ID: 2, Platform: PlatformAntigravity, Type: AccountTypeOAuth},
},
}
groupID := int64(22)
svc := &GatewayService{
schedulerSnapshot: NewSchedulerSnapshotService(cache, nil, nil, nil, nil),
groupRepo: &mockGroupRepoForGateway{
groups: map[int64]*Group{
groupID: {
ID: groupID,
Platform: PlatformGemini,
Status: StatusActive,
Hydrated: true,
},
},
},
concurrencyService: NewConcurrencyService(&mockConcurrencyCache{}),
cfg: &config.Config{
Gateway: config.GatewayConfig{
Scheduling: config.GatewaySchedulingConfig{
LoadBatchEnabled: true,
StickySessionMaxWaiting: 3,
StickySessionWaitTimeout: time.Second,
FallbackWaitTimeout: time.Second,
FallbackMaxWaiting: 10,
},
},
},
}
result, err := svc.SelectAccountWithLoadAwareness(context.Background(), &groupID, "", "gemini-3-flash-preview", nil, "", 0)
if err != nil {
t.Fatalf("SelectAccountWithLoadAwareness error: %v", err)
}
if result == nil || result.Account == nil {
t.Fatalf("expected selected account")
}
if result.Account.ID != 2 {
t.Fatalf("expected scheduler to skip Gemini-family limited antigravity account 1, got %d", result.Account.ID)
}
}