admin_service_composite_group_test.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "testing"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  7. "github.com/stretchr/testify/require"
  8. )
  9. type accountRepoStubForCompositeModelsList struct {
  10. accountRepoStub
  11. accounts []Account
  12. }
  13. func (s *accountRepoStubForCompositeModelsList) ListSchedulableByGroupID(_ context.Context, _ int64) ([]Account, error) {
  14. return s.accounts, nil
  15. }
  16. func TestAdminService_CreateCompositeGroupCopiesAccountsFromConcreteGroups(t *testing.T) {
  17. var copiedFrom []int64
  18. var boundGroupID int64
  19. var boundAccountIDs []int64
  20. groupRepo := &groupRepoStubForAdmin{
  21. createID: 99,
  22. getByIDByID: map[int64]*Group{
  23. 10: {ID: 10, Platform: PlatformOpenAI},
  24. 20: {ID: 20, Platform: PlatformGemini},
  25. },
  26. getAccountIDsByGroupIDsFn: func(groupIDs []int64) ([]int64, error) {
  27. copiedFrom = append([]int64{}, groupIDs...)
  28. return []int64{101, 202}, nil
  29. },
  30. bindAccountsToGroupFn: func(groupID int64, accountIDs []int64) error {
  31. boundGroupID = groupID
  32. boundAccountIDs = append([]int64{}, accountIDs...)
  33. return nil
  34. },
  35. }
  36. svc := &adminServiceImpl{groupRepo: groupRepo}
  37. group, err := svc.CreateGroup(context.Background(), &CreateGroupInput{
  38. Name: "Composite",
  39. Platform: PlatformComposite,
  40. RateMultiplier: 1,
  41. MaxReasoningEffort: "medium",
  42. MaxReasoningEffortOverLimit: ReasoningEffortOverLimitDeny,
  43. ReasoningEffortMappings: []ReasoningEffortMapping{
  44. {From: "max", To: "xhigh"},
  45. },
  46. CopyAccountsFromGroupIDs: []int64{10, 20, 10},
  47. })
  48. require.NoError(t, err)
  49. require.Equal(t, PlatformComposite, groupRepo.created.Platform)
  50. require.Equal(t, "medium", groupRepo.created.MaxReasoningEffort)
  51. require.Equal(t, ReasoningEffortOverLimitDeny, groupRepo.created.MaxReasoningEffortOverLimit)
  52. require.Equal(t, []ReasoningEffortMapping{{From: "max", To: "xhigh"}}, groupRepo.created.ReasoningEffortMappings)
  53. require.Equal(t, int64(99), group.ID)
  54. require.Equal(t, int64(2), group.AccountCount)
  55. require.ElementsMatch(t, []int64{10, 20}, copiedFrom)
  56. require.Equal(t, int64(99), boundGroupID)
  57. require.ElementsMatch(t, []int64{101, 202}, boundAccountIDs)
  58. }
  59. func TestAdminService_UpdateCompositeGroupCopiesAccountsFromConcreteGroups(t *testing.T) {
  60. var clearedGroupID int64
  61. var copiedFrom []int64
  62. var boundGroupID int64
  63. var boundAccountIDs []int64
  64. groupRepo := &groupRepoStubForAdmin{
  65. getByIDByID: map[int64]*Group{
  66. 10: {ID: 10, Platform: PlatformOpenAI},
  67. 20: {ID: 20, Platform: PlatformGrok},
  68. 99: {ID: 99, Platform: PlatformComposite, RateMultiplier: 1, SubscriptionType: SubscriptionTypeStandard},
  69. },
  70. deleteAccountGroupsByGroupIDFn: func(groupID int64) (int64, error) {
  71. clearedGroupID = groupID
  72. return 2, nil
  73. },
  74. getAccountIDsByGroupIDsFn: func(groupIDs []int64) ([]int64, error) {
  75. copiedFrom = append([]int64{}, groupIDs...)
  76. return []int64{301, 302}, nil
  77. },
  78. bindAccountsToGroupFn: func(groupID int64, accountIDs []int64) error {
  79. boundGroupID = groupID
  80. boundAccountIDs = append([]int64{}, accountIDs...)
  81. return nil
  82. },
  83. }
  84. svc := &adminServiceImpl{groupRepo: groupRepo}
  85. maxReasoningEffort := "low"
  86. maxReasoningEffortOverLimit := ReasoningEffortOverLimitDeny
  87. reasoningEffortMappings := []ReasoningEffortMapping{{From: "max", To: "high"}}
  88. group, err := svc.UpdateGroup(context.Background(), 99, &UpdateGroupInput{
  89. MaxReasoningEffort: &maxReasoningEffort,
  90. MaxReasoningEffortOverLimit: &maxReasoningEffortOverLimit,
  91. ReasoningEffortMappings: &reasoningEffortMappings,
  92. CopyAccountsFromGroupIDs: []int64{10, 20},
  93. })
  94. require.NoError(t, err)
  95. require.Equal(t, PlatformComposite, group.Platform)
  96. require.Equal(t, "low", group.MaxReasoningEffort)
  97. require.Equal(t, ReasoningEffortOverLimitDeny, group.MaxReasoningEffortOverLimit)
  98. require.Equal(t, reasoningEffortMappings, group.ReasoningEffortMappings)
  99. require.Equal(t, int64(99), clearedGroupID)
  100. require.ElementsMatch(t, []int64{10, 20}, copiedFrom)
  101. require.Equal(t, int64(99), boundGroupID)
  102. require.ElementsMatch(t, []int64{301, 302}, boundAccountIDs)
  103. }
  104. func TestAdminService_CreateAccountAllowsCompositeGroupAssignment(t *testing.T) {
  105. accountRepo := &accountRepoStubForBulkUpdate{createID: 7}
  106. groupRepo := &groupRepoStubForAdmin{
  107. getByIDByID: map[int64]*Group{
  108. 99: {ID: 99, Platform: PlatformComposite},
  109. },
  110. }
  111. svc := &adminServiceImpl{accountRepo: accountRepo, groupRepo: groupRepo}
  112. account, err := svc.CreateAccount(context.Background(), &CreateAccountInput{
  113. Name: "OpenAI account",
  114. Platform: PlatformOpenAI,
  115. Type: AccountTypeAPIKey,
  116. Concurrency: 1,
  117. GroupIDs: []int64{99},
  118. SkipDefaultGroupBind: true,
  119. SkipMixedChannelCheck: true,
  120. })
  121. require.NoError(t, err)
  122. require.Equal(t, int64(7), account.ID)
  123. require.Equal(t, PlatformOpenAI, accountRepo.createAccount.Platform)
  124. require.ElementsMatch(t, []int64{99}, accountRepo.bindGroupsByAccount[7])
  125. }
  126. func TestAdminService_UpdateAccountAllowsCompositeGroupAssignment(t *testing.T) {
  127. accountRepo := &accountRepoStubForBulkUpdate{
  128. getByIDAccounts: map[int64]*Account{
  129. 7: {ID: 7, Platform: PlatformGemini, Type: AccountTypeAPIKey, Status: StatusActive, Extra: map[string]any{}},
  130. },
  131. }
  132. groupRepo := &groupRepoStubForAdmin{
  133. getByIDByID: map[int64]*Group{
  134. 99: {ID: 99, Platform: PlatformComposite},
  135. },
  136. }
  137. svc := &adminServiceImpl{accountRepo: accountRepo, groupRepo: groupRepo}
  138. groupIDs := []int64{99}
  139. account, err := svc.UpdateAccount(context.Background(), 7, &UpdateAccountInput{
  140. GroupIDs: &groupIDs,
  141. SkipMixedChannelCheck: true,
  142. })
  143. require.NoError(t, err)
  144. require.Equal(t, int64(7), account.ID)
  145. require.Len(t, accountRepo.updatedAccounts, 1)
  146. require.ElementsMatch(t, []int64{99}, accountRepo.bindGroupsByAccount[7])
  147. }
  148. func TestAdminService_CompositeModelsListCandidatesIncludeConcreteAccountMappings(t *testing.T) {
  149. accountRepo := &accountRepoStubForCompositeModelsList{
  150. accounts: []Account{
  151. {
  152. ID: 1,
  153. Platform: PlatformOpenAI,
  154. Credentials: map[string]any{
  155. "model_mapping": map[string]any{"gpt-custom": "gpt-5"},
  156. },
  157. },
  158. {
  159. ID: 2,
  160. Platform: PlatformGemini,
  161. Credentials: map[string]any{
  162. "model_mapping": map[string]any{"gemini-custom": "gemini-2.5-flash"},
  163. },
  164. },
  165. {
  166. ID: 3,
  167. Platform: PlatformKimi,
  168. Credentials: map[string]any{
  169. "model_mapping": map[string]any{"kimi-custom": "kimi-k2"},
  170. },
  171. },
  172. },
  173. }
  174. groupRepo := &groupRepoStubForAdmin{
  175. getByIDByID: map[int64]*Group{
  176. 99: {ID: 99, Platform: PlatformComposite},
  177. },
  178. }
  179. svc := &adminServiceImpl{accountRepo: accountRepo, groupRepo: groupRepo}
  180. candidates, err := svc.GetGroupModelsListCandidates(context.Background(), 99, PlatformComposite)
  181. require.NoError(t, err)
  182. require.Contains(t, candidates, "gpt-custom")
  183. require.Contains(t, candidates, "gemini-custom")
  184. require.Contains(t, candidates, "kimi-custom")
  185. require.Contains(t, candidates, "gpt-5.5")
  186. require.Contains(t, candidates, "gemini-2.5-flash")
  187. }
  188. // 独立 CN 分组的模型列表候选沿用 default 分支的 Claude 默认列表;
  189. // composite 支持不得改变独立分组的候选语义。
  190. func TestAdminService_CNProviderModelsListCandidatesKeepClaudeDefaults(t *testing.T) {
  191. want := make([]string, 0, len(claude.DefaultModels))
  192. for _, model := range claude.DefaultModels {
  193. want = append(want, model.ID)
  194. }
  195. for _, platform := range []string{PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
  196. require.Equal(t, want, defaultModelsListCandidateIDs(platform), "platform=%s", platform)
  197. }
  198. }