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
215 lines
7.1 KiB
Go
215 lines
7.1 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type accountRepoStubForCompositeModelsList struct {
|
|
accountRepoStub
|
|
accounts []Account
|
|
}
|
|
|
|
func (s *accountRepoStubForCompositeModelsList) ListSchedulableByGroupID(_ context.Context, _ int64) ([]Account, error) {
|
|
return s.accounts, nil
|
|
}
|
|
|
|
func TestAdminService_CreateCompositeGroupCopiesAccountsFromConcreteGroups(t *testing.T) {
|
|
var copiedFrom []int64
|
|
var boundGroupID int64
|
|
var boundAccountIDs []int64
|
|
groupRepo := &groupRepoStubForAdmin{
|
|
createID: 99,
|
|
getByIDByID: map[int64]*Group{
|
|
10: {ID: 10, Platform: PlatformOpenAI},
|
|
20: {ID: 20, Platform: PlatformGemini},
|
|
},
|
|
getAccountIDsByGroupIDsFn: func(groupIDs []int64) ([]int64, error) {
|
|
copiedFrom = append([]int64{}, groupIDs...)
|
|
return []int64{101, 202}, nil
|
|
},
|
|
bindAccountsToGroupFn: func(groupID int64, accountIDs []int64) error {
|
|
boundGroupID = groupID
|
|
boundAccountIDs = append([]int64{}, accountIDs...)
|
|
return nil
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{groupRepo: groupRepo}
|
|
|
|
group, err := svc.CreateGroup(context.Background(), &CreateGroupInput{
|
|
Name: "Composite",
|
|
Platform: PlatformComposite,
|
|
RateMultiplier: 1,
|
|
MaxReasoningEffort: "medium",
|
|
ReasoningEffortMappings: []ReasoningEffortMapping{
|
|
{From: "max", To: "xhigh"},
|
|
},
|
|
CopyAccountsFromGroupIDs: []int64{10, 20, 10},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, PlatformComposite, groupRepo.created.Platform)
|
|
require.Equal(t, "medium", groupRepo.created.MaxReasoningEffort)
|
|
require.Equal(t, []ReasoningEffortMapping{{From: "max", To: "xhigh"}}, groupRepo.created.ReasoningEffortMappings)
|
|
require.Equal(t, int64(99), group.ID)
|
|
require.Equal(t, int64(2), group.AccountCount)
|
|
require.ElementsMatch(t, []int64{10, 20}, copiedFrom)
|
|
require.Equal(t, int64(99), boundGroupID)
|
|
require.ElementsMatch(t, []int64{101, 202}, boundAccountIDs)
|
|
}
|
|
|
|
func TestAdminService_UpdateCompositeGroupCopiesAccountsFromConcreteGroups(t *testing.T) {
|
|
var clearedGroupID int64
|
|
var copiedFrom []int64
|
|
var boundGroupID int64
|
|
var boundAccountIDs []int64
|
|
groupRepo := &groupRepoStubForAdmin{
|
|
getByIDByID: map[int64]*Group{
|
|
10: {ID: 10, Platform: PlatformOpenAI},
|
|
20: {ID: 20, Platform: PlatformGrok},
|
|
99: {ID: 99, Platform: PlatformComposite, RateMultiplier: 1, SubscriptionType: SubscriptionTypeStandard},
|
|
},
|
|
deleteAccountGroupsByGroupIDFn: func(groupID int64) (int64, error) {
|
|
clearedGroupID = groupID
|
|
return 2, nil
|
|
},
|
|
getAccountIDsByGroupIDsFn: func(groupIDs []int64) ([]int64, error) {
|
|
copiedFrom = append([]int64{}, groupIDs...)
|
|
return []int64{301, 302}, nil
|
|
},
|
|
bindAccountsToGroupFn: func(groupID int64, accountIDs []int64) error {
|
|
boundGroupID = groupID
|
|
boundAccountIDs = append([]int64{}, accountIDs...)
|
|
return nil
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{groupRepo: groupRepo}
|
|
maxReasoningEffort := "low"
|
|
reasoningEffortMappings := []ReasoningEffortMapping{{From: "max", To: "high"}}
|
|
|
|
group, err := svc.UpdateGroup(context.Background(), 99, &UpdateGroupInput{
|
|
MaxReasoningEffort: &maxReasoningEffort,
|
|
ReasoningEffortMappings: &reasoningEffortMappings,
|
|
CopyAccountsFromGroupIDs: []int64{10, 20},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, PlatformComposite, group.Platform)
|
|
require.Equal(t, "low", group.MaxReasoningEffort)
|
|
require.Equal(t, reasoningEffortMappings, group.ReasoningEffortMappings)
|
|
require.Equal(t, int64(99), clearedGroupID)
|
|
require.ElementsMatch(t, []int64{10, 20}, copiedFrom)
|
|
require.Equal(t, int64(99), boundGroupID)
|
|
require.ElementsMatch(t, []int64{301, 302}, boundAccountIDs)
|
|
}
|
|
|
|
func TestAdminService_CreateAccountAllowsCompositeGroupAssignment(t *testing.T) {
|
|
accountRepo := &accountRepoStubForBulkUpdate{createID: 7}
|
|
groupRepo := &groupRepoStubForAdmin{
|
|
getByIDByID: map[int64]*Group{
|
|
99: {ID: 99, Platform: PlatformComposite},
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{accountRepo: accountRepo, groupRepo: groupRepo}
|
|
|
|
account, err := svc.CreateAccount(context.Background(), &CreateAccountInput{
|
|
Name: "OpenAI account",
|
|
Platform: PlatformOpenAI,
|
|
Type: AccountTypeAPIKey,
|
|
Concurrency: 1,
|
|
GroupIDs: []int64{99},
|
|
SkipDefaultGroupBind: true,
|
|
SkipMixedChannelCheck: true,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(7), account.ID)
|
|
require.Equal(t, PlatformOpenAI, accountRepo.createAccount.Platform)
|
|
require.ElementsMatch(t, []int64{99}, accountRepo.bindGroupsByAccount[7])
|
|
}
|
|
|
|
func TestAdminService_UpdateAccountAllowsCompositeGroupAssignment(t *testing.T) {
|
|
accountRepo := &accountRepoStubForBulkUpdate{
|
|
getByIDAccounts: map[int64]*Account{
|
|
7: {ID: 7, Platform: PlatformGemini, Type: AccountTypeAPIKey, Status: StatusActive, Extra: map[string]any{}},
|
|
},
|
|
}
|
|
groupRepo := &groupRepoStubForAdmin{
|
|
getByIDByID: map[int64]*Group{
|
|
99: {ID: 99, Platform: PlatformComposite},
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{accountRepo: accountRepo, groupRepo: groupRepo}
|
|
groupIDs := []int64{99}
|
|
|
|
account, err := svc.UpdateAccount(context.Background(), 7, &UpdateAccountInput{
|
|
GroupIDs: &groupIDs,
|
|
SkipMixedChannelCheck: true,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(7), account.ID)
|
|
require.Len(t, accountRepo.updatedAccounts, 1)
|
|
require.ElementsMatch(t, []int64{99}, accountRepo.bindGroupsByAccount[7])
|
|
}
|
|
|
|
func TestAdminService_CompositeModelsListCandidatesIncludeConcreteAccountMappings(t *testing.T) {
|
|
accountRepo := &accountRepoStubForCompositeModelsList{
|
|
accounts: []Account{
|
|
{
|
|
ID: 1,
|
|
Platform: PlatformOpenAI,
|
|
Credentials: map[string]any{
|
|
"model_mapping": map[string]any{"gpt-custom": "gpt-5"},
|
|
},
|
|
},
|
|
{
|
|
ID: 2,
|
|
Platform: PlatformGemini,
|
|
Credentials: map[string]any{
|
|
"model_mapping": map[string]any{"gemini-custom": "gemini-2.5-flash"},
|
|
},
|
|
},
|
|
{
|
|
ID: 3,
|
|
Platform: PlatformKimi,
|
|
Credentials: map[string]any{
|
|
"model_mapping": map[string]any{"kimi-custom": "kimi-k2"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
groupRepo := &groupRepoStubForAdmin{
|
|
getByIDByID: map[int64]*Group{
|
|
99: {ID: 99, Platform: PlatformComposite},
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{accountRepo: accountRepo, groupRepo: groupRepo}
|
|
|
|
candidates, err := svc.GetGroupModelsListCandidates(context.Background(), 99, PlatformComposite)
|
|
|
|
require.NoError(t, err)
|
|
require.Contains(t, candidates, "gpt-custom")
|
|
require.Contains(t, candidates, "gemini-custom")
|
|
require.Contains(t, candidates, "kimi-custom")
|
|
require.Contains(t, candidates, "gpt-5.5")
|
|
require.Contains(t, candidates, "gemini-2.5-flash")
|
|
}
|
|
|
|
// 独立 CN 分组的模型列表候选沿用 default 分支的 Claude 默认列表;
|
|
// composite 支持不得改变独立分组的候选语义。
|
|
func TestAdminService_CNProviderModelsListCandidatesKeepClaudeDefaults(t *testing.T) {
|
|
want := make([]string, 0, len(claude.DefaultModels))
|
|
for _, model := range claude.DefaultModels {
|
|
want = append(want, model.ID)
|
|
}
|
|
for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek} {
|
|
require.Equal(t, want, defaultModelsListCandidateIDs(platform), "platform=%s", platform)
|
|
}
|
|
}
|