| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- //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)
- }
|