Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_NoModel_AlwaysAvailable(t *testing.T) {
|
||||
repo := &mockAccountRepoForPlatform{accounts: nil, accountsByID: map[int64]*Account{}}
|
||||
svc := &GatewayService{accountRepo: repo, cfg: testConfig()}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), nil, "", PlatformOpenAI)
|
||||
|
||||
require.True(t, diag.HasAccountsInPool, "empty model must return HasAccountsInPool=true so caller stays on 503")
|
||||
require.True(t, diag.HasModelSupport, "empty model must return HasModelSupport=true so caller stays on 503")
|
||||
}
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_EmptyPlatform_AlwaysAvailable(t *testing.T) {
|
||||
repo := &mockAccountRepoForPlatform{accounts: nil, accountsByID: map[int64]*Account{}}
|
||||
svc := &GatewayService{accountRepo: repo, cfg: testConfig()}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), nil, "gpt-5", "")
|
||||
|
||||
require.True(t, diag.HasAccountsInPool)
|
||||
require.True(t, diag.HasModelSupport, "empty platform must fall back to {true,true} so caller stays on 503")
|
||||
}
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_NilReceiver(t *testing.T) {
|
||||
var svc *GatewayService
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), nil, "gpt-5", PlatformOpenAI)
|
||||
|
||||
require.True(t, diag.HasAccountsInPool)
|
||||
require.True(t, diag.HasModelSupport)
|
||||
}
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_NoAccountsInPool(t *testing.T) {
|
||||
repo := &mockAccountRepoForPlatform{accounts: nil, accountsByID: map[int64]*Account{}}
|
||||
svc := &GatewayService{accountRepo: repo, cfg: testConfig()}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), nil, "gpt-5", PlatformOpenAI)
|
||||
|
||||
require.False(t, diag.HasAccountsInPool)
|
||||
require.False(t, diag.HasModelSupport, "no accounts means no support; caller stays on 503 (empty-pool branch)")
|
||||
}
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_ExplicitMappingMatches(t *testing.T) {
|
||||
repo := &mockAccountRepoForPlatform{
|
||||
accounts: []Account{
|
||||
{
|
||||
ID: 1,
|
||||
Platform: PlatformOpenAI,
|
||||
Status: StatusActive,
|
||||
Schedulable: true,
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{"gpt-5.1-codex-mini": "gpt-5.1-codex-mini"},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountsByID: map[int64]*Account{},
|
||||
}
|
||||
for i := range repo.accounts {
|
||||
repo.accountsByID[repo.accounts[i].ID] = &repo.accounts[i]
|
||||
}
|
||||
svc := &GatewayService{accountRepo: repo, cfg: testConfig()}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), nil, "gpt-5.1-codex-mini", PlatformOpenAI)
|
||||
|
||||
require.True(t, diag.HasAccountsInPool)
|
||||
require.True(t, diag.HasModelSupport)
|
||||
}
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_EmptyMappingAllowsAll(t *testing.T) {
|
||||
repo := &mockAccountRepoForPlatform{
|
||||
accounts: []Account{
|
||||
{ID: 1, Platform: PlatformOpenAI, Status: StatusActive, Schedulable: true /* no ModelMapping = allow all */},
|
||||
},
|
||||
accountsByID: map[int64]*Account{},
|
||||
}
|
||||
for i := range repo.accounts {
|
||||
repo.accountsByID[repo.accounts[i].ID] = &repo.accounts[i]
|
||||
}
|
||||
svc := &GatewayService{accountRepo: repo, cfg: testConfig()}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), nil, "gpt-5.1-codex-mini", PlatformOpenAI)
|
||||
|
||||
require.True(t, diag.HasModelSupport, "empty model_mapping must be treated as 'allow all' (Account.IsModelSupported semantics)")
|
||||
}
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_WildcardMappingMatches(t *testing.T) {
|
||||
repo := &mockAccountRepoForPlatform{
|
||||
accounts: []Account{
|
||||
{
|
||||
ID: 1,
|
||||
Platform: PlatformOpenAI,
|
||||
Status: StatusActive,
|
||||
Schedulable: true,
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{"*": "gpt-5"},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountsByID: map[int64]*Account{},
|
||||
}
|
||||
for i := range repo.accounts {
|
||||
repo.accountsByID[repo.accounts[i].ID] = &repo.accounts[i]
|
||||
}
|
||||
svc := &GatewayService{accountRepo: repo, cfg: testConfig()}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), nil, "gpt-5.1-codex-mini", PlatformOpenAI)
|
||||
|
||||
require.True(t, diag.HasModelSupport, "wildcard mapping must classify the request as 'serviceable'")
|
||||
}
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_NoMatchingModel_ReturnsNotFoundSignal(t *testing.T) {
|
||||
groupID := int64(42)
|
||||
repo := &mockAccountRepoForPlatform{
|
||||
accounts: []Account{
|
||||
{
|
||||
ID: 1,
|
||||
Platform: PlatformOpenAI,
|
||||
Status: StatusActive,
|
||||
Schedulable: true,
|
||||
AccountGroups: []AccountGroup{
|
||||
{GroupID: groupID},
|
||||
},
|
||||
Credentials: map[string]any{"model_mapping": map[string]any{"gpt-5": "gpt-5"}},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Platform: PlatformOpenAI,
|
||||
Status: StatusActive,
|
||||
Schedulable: true,
|
||||
AccountGroups: []AccountGroup{
|
||||
{GroupID: groupID},
|
||||
},
|
||||
Credentials: map[string]any{"model_mapping": map[string]any{"gpt-5-mini": "gpt-5-mini"}},
|
||||
},
|
||||
},
|
||||
accountsByID: map[int64]*Account{},
|
||||
}
|
||||
for i := range repo.accounts {
|
||||
repo.accountsByID[repo.accounts[i].ID] = &repo.accounts[i]
|
||||
}
|
||||
svc := &GatewayService{accountRepo: repo, cfg: testConfig()}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), &groupID, "gpt-5.1-codex-mini", PlatformOpenAI)
|
||||
|
||||
require.True(t, diag.HasAccountsInPool, "group has OpenAI accounts")
|
||||
require.False(t, diag.HasModelSupport, "no account mapping admits the requested model — handler should return 404")
|
||||
}
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_RateLimitedSupportingAccountRemainsConfigured(t *testing.T) {
|
||||
groupID := int64(42)
|
||||
cooldownUntil := time.Now().Add(time.Hour)
|
||||
repo := &mockAccountRepoForPlatform{
|
||||
accounts: []Account{
|
||||
{
|
||||
ID: 1,
|
||||
Platform: PlatformAnthropic,
|
||||
Status: StatusActive,
|
||||
Schedulable: true,
|
||||
RateLimitResetAt: &cooldownUntil,
|
||||
OverloadUntil: &cooldownUntil,
|
||||
TempUnschedulableUntil: &cooldownUntil,
|
||||
AccountGroups: []AccountGroup{{GroupID: groupID}},
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{"claude-opus-4-8": "claude-opus-4-8"},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountsByID: map[int64]*Account{},
|
||||
}
|
||||
require.False(t, repo.accounts[0].IsSchedulable(), "test account must be excluded from normal scheduling while cooling down")
|
||||
svc := &GatewayService{
|
||||
accountRepo: repo,
|
||||
cfg: testConfig(),
|
||||
schedulerSnapshot: &SchedulerSnapshotService{}, // diagnosis must bypass the transient-only snapshot
|
||||
}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), &groupID, "claude-opus-4-8", PlatformAnthropic)
|
||||
|
||||
require.True(t, diag.HasAccountsInPool)
|
||||
require.True(t, diag.HasModelSupport, "a configured model remains supported while every matching account is temporarily cooling down")
|
||||
}
|
||||
|
||||
func TestOpenAIDiagnoseModelAvailabilityForPlatform_RateLimitedSupportingAccountRemainsConfigured(t *testing.T) {
|
||||
groupID := int64(43)
|
||||
cooldownUntil := time.Now().Add(time.Hour)
|
||||
repo := &mockAccountRepoForPlatform{
|
||||
accounts: []Account{
|
||||
{
|
||||
ID: 2,
|
||||
Platform: PlatformOpenAI,
|
||||
Status: StatusActive,
|
||||
Schedulable: true,
|
||||
RateLimitResetAt: &cooldownUntil,
|
||||
OverloadUntil: &cooldownUntil,
|
||||
TempUnschedulableUntil: &cooldownUntil,
|
||||
AccountGroups: []AccountGroup{{GroupID: groupID}},
|
||||
Credentials: map[string]any{
|
||||
"model_mapping": map[string]any{"claude-opus-4-8": "claude-opus-4-8"},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountsByID: map[int64]*Account{},
|
||||
}
|
||||
require.False(t, repo.accounts[0].IsSchedulable(), "test account must be excluded from normal scheduling while cooling down")
|
||||
svc := &OpenAIGatewayService{
|
||||
accountRepo: repo,
|
||||
cfg: testConfig(),
|
||||
schedulerSnapshot: &SchedulerSnapshotService{}, // diagnosis must bypass the transient-only snapshot
|
||||
}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), &groupID, "claude-opus-4-8", PlatformOpenAI)
|
||||
|
||||
require.True(t, diag.HasAccountsInPool)
|
||||
require.True(t, diag.HasModelSupport, "OpenAI-compatible diagnosis must keep transiently limited supporting accounts in the configured pool")
|
||||
}
|
||||
|
||||
func TestDiagnoseModelAvailabilityForPlatform_WrongPlatformFiltersOut(t *testing.T) {
|
||||
// Group has only Anthropic accounts; user routes to OpenAI gateway.
|
||||
// Diagnosis must NOT see Anthropic accounts (listSchedulableAccounts filters
|
||||
// by platform), so HasAccountsInPool is false and the caller stays on 503.
|
||||
repo := &mockAccountRepoForPlatform{
|
||||
accounts: []Account{
|
||||
{
|
||||
ID: 1,
|
||||
Platform: PlatformAnthropic,
|
||||
Status: StatusActive,
|
||||
Schedulable: true,
|
||||
Credentials: map[string]any{"model_mapping": map[string]any{"claude-sonnet-4-5": "claude-sonnet-4-5"}},
|
||||
},
|
||||
},
|
||||
accountsByID: map[int64]*Account{},
|
||||
}
|
||||
for i := range repo.accounts {
|
||||
repo.accountsByID[repo.accounts[i].ID] = &repo.accounts[i]
|
||||
}
|
||||
svc := &GatewayService{accountRepo: repo, cfg: testConfig()}
|
||||
|
||||
diag := svc.DiagnoseModelAvailabilityForPlatform(context.Background(), nil, "gpt-5", PlatformOpenAI)
|
||||
|
||||
require.False(t, diag.HasAccountsInPool, "OpenAI route must not see Anthropic accounts in pool")
|
||||
require.False(t, diag.HasModelSupport)
|
||||
}
|
||||
Reference in New Issue
Block a user