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
252 lines
9.1 KiB
Go
252 lines
9.1 KiB
Go
//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)
|
|
}
|