diff --git a/internal/repository/test/usage_identity_priority_order_test.go b/internal/repository/test/usage_identity_priority_order_test.go new file mode 100644 index 00000000..0bc97735 --- /dev/null +++ b/internal/repository/test/usage_identity_priority_order_test.go @@ -0,0 +1,64 @@ +package test + +import ( + "context" + "reflect" + "testing" + "time" + + "cpa-usage-keeper/internal/entities" + "cpa-usage-keeper/internal/repository" +) + +func TestAuthFilePrioritySortUsesCPAFileNameAsTieBreak(t *testing.T) { + db := openTestDatabase(t) + now := time.Date(2026, 7, 30, 12, 0, 0, 0, time.UTC) + priority := 5 + enabled := false + laterFileName := "codex-bbbbbbbb-user@example.com-free.json" + earlierFileName := "codex-6ac2d0d0-user@example.com-free.json" + rows := []entities.UsageIdentity{ + { + Identity: "later-file-name", + Name: "user@example.com", + FileName: &laterFileName, + AuthType: entities.UsageIdentityAuthTypeAuthFile, + Priority: &priority, + Disabled: &enabled, + CreatedAt: now, + UpdatedAt: now, + }, + { + Identity: "earlier-file-name", + Name: "user@example.com", + FileName: &earlierFileName, + AuthType: entities.UsageIdentityAuthTypeAuthFile, + Priority: &priority, + Disabled: &enabled, + CreatedAt: now, + UpdatedAt: now, + }, + } + if err := db.Create(&rows).Error; err != nil { + t.Fatalf("seed usage identities: %v", err) + } + + authType := entities.UsageIdentityAuthTypeAuthFile + activeOnly := true + items, total, _, err := repository.ListActiveUsageIdentitiesPage(context.Background(), db, repository.ListUsageIdentitiesPageRequest{ + AuthType: &authType, + ActiveOnly: &activeOnly, + Sort: repository.UsageIdentityPageSortPriority, + Page: 1, + PageSize: 10, + }) + if err != nil { + t.Fatalf("list auth files by priority: %v", err) + } + if total != 2 { + t.Fatalf("expected 2 auth files, got %d", total) + } + if got := []string{items[0].Identity, items[1].Identity}; !reflect.DeepEqual(got, []string{"earlier-file-name", "later-file-name"}) { + t.Fatalf("expected equal-priority auth files sorted by CPA file name, got %v", got) + } +} diff --git a/internal/repository/usage_identities.go b/internal/repository/usage_identities.go index 76a62724..9fa8cf4b 100644 --- a/internal/repository/usage_identities.go +++ b/internal/repository/usage_identities.go @@ -290,10 +290,10 @@ func applyUsageIdentityTypesFilter(query *gorm.DB, types []string) *gorm.DB { func applyUsageIdentityPageSort(query *gorm.DB, sort string, authType *entities.UsageIdentityAuthType) *gorm.DB { switch sort { case UsageIdentityPageSortPriority: - // Auth Files 的 priority 同分需要稳定按名称排列;AI Provider 只保留同步顺序兜底。 + // Auth Files 的 priority 同分按 CPA 文件名排列;AI Provider 只保留同步顺序兜底。 query = query.Order("priority IS NULL ASC").Order("priority DESC") if authType != nil && *authType == entities.UsageIdentityAuthTypeAuthFile { - query = query.Order("LOWER(name) ASC") + query = query.Order("LOWER(file_name) ASC") } return query.Order("id ASC") case UsageIdentityPageSortTotalTokens: diff --git a/internal/repository/usage_identities_test.go b/internal/repository/usage_identities_test.go index 4ceba02d..b543159e 100644 --- a/internal/repository/usage_identities_test.go +++ b/internal/repository/usage_identities_test.go @@ -821,13 +821,13 @@ func TestUsageIdentityListActivePageFiltersEnabledAuthFilesAndOrdersByPriority(t disabled := true enabled := false rows := []entities.UsageIdentity{ - {Identity: "default", Name: "Default", AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: nil, Disabled: nil, TotalRequests: 40, TotalTokens: 400, CreatedAt: now, UpdatedAt: now}, - {Identity: "priority-5-zeta", Name: "Zeta", AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(5), Disabled: &enabled, TotalRequests: 25, TotalTokens: 250, CreatedAt: now, UpdatedAt: now}, - {Identity: "priority-5-alpha", Name: "Alpha", AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(5), Disabled: &enabled, TotalRequests: 22, TotalTokens: 220, CreatedAt: now, UpdatedAt: now}, - {Identity: "priority-5-beta-lower", Name: "beta", AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(5), Disabled: &enabled, TotalRequests: 21, TotalTokens: 210, CreatedAt: now, UpdatedAt: now}, - {Identity: "priority-1", Name: "Priority 1", AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(1), Disabled: &enabled, TotalRequests: 10, TotalTokens: 100, CreatedAt: now, UpdatedAt: now}, - {Identity: "priority-5", Name: "Priority 5", AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(5), Disabled: &enabled, TotalRequests: 20, TotalTokens: 200, CreatedAt: now, UpdatedAt: now}, - {Identity: "disabled", Name: "Disabled", AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(0), Disabled: &disabled, TotalRequests: 99, TotalTokens: 999, CreatedAt: now, UpdatedAt: now}, + {Identity: "default", Name: "Default", FileName: strPtr("default.json"), AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: nil, Disabled: nil, TotalRequests: 40, TotalTokens: 400, CreatedAt: now, UpdatedAt: now}, + {Identity: "priority-5-zeta", Name: "Zeta", FileName: strPtr("zeta.json"), AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(5), Disabled: &enabled, TotalRequests: 25, TotalTokens: 250, CreatedAt: now, UpdatedAt: now}, + {Identity: "priority-5-alpha", Name: "Alpha", FileName: strPtr("Alpha.json"), AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(5), Disabled: &enabled, TotalRequests: 22, TotalTokens: 220, CreatedAt: now, UpdatedAt: now}, + {Identity: "priority-5-beta-lower", Name: "beta", FileName: strPtr("beta.json"), AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(5), Disabled: &enabled, TotalRequests: 21, TotalTokens: 210, CreatedAt: now, UpdatedAt: now}, + {Identity: "priority-1", Name: "Priority 1", FileName: strPtr("priority-1.json"), AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(1), Disabled: &enabled, TotalRequests: 10, TotalTokens: 100, CreatedAt: now, UpdatedAt: now}, + {Identity: "priority-5", Name: "Priority 5", FileName: strPtr("priority-5.json"), AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(5), Disabled: &enabled, TotalRequests: 20, TotalTokens: 200, CreatedAt: now, UpdatedAt: now}, + {Identity: "disabled", Name: "Disabled", FileName: strPtr("disabled.json"), AuthType: entities.UsageIdentityAuthTypeAuthFile, AuthTypeName: "oauth", Type: "claude", Provider: "Claude", Priority: intPtr(0), Disabled: &disabled, TotalRequests: 99, TotalTokens: 999, CreatedAt: now, UpdatedAt: now}, } if err := db.Create(&rows).Error; err != nil { t.Fatalf("seed usage identities: %v", err) @@ -843,7 +843,7 @@ func TestUsageIdentityListActivePageFiltersEnabledAuthFilesAndOrdersByPriority(t t.Fatalf("expected total 6, got %d", total) } if got := []string{items[0].Identity, items[1].Identity, items[2].Identity, items[3].Identity, items[4].Identity, items[5].Identity}; !reflect.DeepEqual(got, []string{"priority-5-alpha", "priority-5-beta-lower", "priority-5", "priority-5-zeta", "priority-1", "default"}) { - t.Fatalf("expected enabled auth files sorted by priority desc, name asc case-insensitively, then missing priority last, got %v", got) + t.Fatalf("expected enabled auth files sorted by priority desc, file name asc case-insensitively, then missing priority last, got %v", got) } } @@ -949,7 +949,7 @@ func TestUsageIdentityListActivePageOrdersAIProvidersByPriorityWithoutNameTieBre } } -func TestUsageIdentityPrioritySortUsesPortableNullAndNameOrdering(t *testing.T) { +func TestUsageIdentityPrioritySortUsesPortableNullAndFileNameOrdering(t *testing.T) { db := openTestDatabase(t).Session(&gorm.Session{DryRun: true}) authType := entities.UsageIdentityAuthTypeAuthFile var rows []entities.UsageIdentity @@ -960,7 +960,7 @@ func TestUsageIdentityPrioritySortUsesPortableNullAndNameOrdering(t *testing.T) for _, expected := range []string{ "priority IS NULL ASC", "priority DESC", - "LOWER(name) ASC", + "LOWER(file_name) ASC", "id ASC", } { if !strings.Contains(sql, expected) { @@ -970,6 +970,9 @@ func TestUsageIdentityPrioritySortUsesPortableNullAndNameOrdering(t *testing.T) if strings.Contains(sql, "COLLATE NOCASE") { t.Fatalf("expected priority sort SQL to avoid sqlite-specific COLLATE NOCASE, got %s", sql) } + if strings.Contains(sql, "LOWER(name) ASC") { + t.Fatalf("expected auth file priority ties not to use the display name, got %s", sql) + } } func TestUsageIdentityListOrdersByAuthTypeNameIDAndIncludesDeletedRows(t *testing.T) {