| |
| |
| |
| |
|
|
| require('dotenv').config() |
| const { v4: uuidv4 } = require('uuid') |
| const redis = require('../src/models/redis') |
| const accountGroupService = require('../src/services/accountGroupService') |
| const claudeAccountService = require('../src/services/claudeAccountService') |
| const claudeConsoleAccountService = require('../src/services/claudeConsoleAccountService') |
| const apiKeyService = require('../src/services/apiKeyService') |
| const unifiedClaudeScheduler = require('../src/services/unifiedClaudeScheduler') |
|
|
| |
| const TEST_PREFIX = 'test_group_' |
| const CLEANUP_ON_FINISH = true |
|
|
| |
| const testData = { |
| groups: [], |
| accounts: [], |
| apiKeys: [] |
| } |
|
|
| |
| const colors = { |
| green: '\x1b[32m', |
| red: '\x1b[31m', |
| yellow: '\x1b[33m', |
| blue: '\x1b[34m', |
| reset: '\x1b[0m' |
| } |
|
|
| function log(message, type = 'info') { |
| const color = |
| { |
| success: colors.green, |
| error: colors.red, |
| warning: colors.yellow, |
| info: colors.blue |
| }[type] || colors.reset |
|
|
| console.log(`${color}${message}${colors.reset}`) |
| } |
|
|
| async function sleep(ms) { |
| return new Promise((resolve) => setTimeout(resolve, ms)) |
| } |
|
|
| |
| async function cleanup() { |
| log('\n🧹 清理测试数据...', 'info') |
|
|
| |
| for (const apiKey of testData.apiKeys) { |
| try { |
| await apiKeyService.deleteApiKey(apiKey.id) |
| log(`✅ 删除测试API Key: ${apiKey.name}`, 'success') |
| } catch (error) { |
| log(`❌ 删除API Key失败: ${error.message}`, 'error') |
| } |
| } |
|
|
| |
| for (const account of testData.accounts) { |
| try { |
| if (account.type === 'claude') { |
| await claudeAccountService.deleteAccount(account.id) |
| } else if (account.type === 'claude-console') { |
| await claudeConsoleAccountService.deleteAccount(account.id) |
| } |
| log(`✅ 删除测试账户: ${account.name}`, 'success') |
| } catch (error) { |
| log(`❌ 删除账户失败: ${error.message}`, 'error') |
| } |
| } |
|
|
| |
| for (const group of testData.groups) { |
| try { |
| await accountGroupService.deleteGroup(group.id) |
| log(`✅ 删除测试分组: ${group.name}`, 'success') |
| } catch (error) { |
| |
| if (error.message.includes('分组内还有账户')) { |
| const members = await accountGroupService.getGroupMembers(group.id) |
| for (const memberId of members) { |
| await accountGroupService.removeAccountFromGroup(memberId, group.id) |
| } |
| |
| await accountGroupService.deleteGroup(group.id) |
| log(`✅ 删除测试分组: ${group.name} (清空成员后)`, 'success') |
| } else { |
| log(`❌ 删除分组失败: ${error.message}`, 'error') |
| } |
| } |
| } |
| } |
|
|
| |
| async function test1_createGroups() { |
| log('\n📝 测试1: 创建账户分组', 'info') |
|
|
| try { |
| |
| const claudeGroup = await accountGroupService.createGroup({ |
| name: `${TEST_PREFIX}Claude组`, |
| platform: 'claude', |
| description: '测试用Claude账户分组' |
| }) |
| testData.groups.push(claudeGroup) |
| log(`✅ 创建Claude分组成功: ${claudeGroup.name} (ID: ${claudeGroup.id})`, 'success') |
|
|
| |
| const geminiGroup = await accountGroupService.createGroup({ |
| name: `${TEST_PREFIX}Gemini组`, |
| platform: 'gemini', |
| description: '测试用Gemini账户分组' |
| }) |
| testData.groups.push(geminiGroup) |
| log(`✅ 创建Gemini分组成功: ${geminiGroup.name} (ID: ${geminiGroup.id})`, 'success') |
|
|
| |
| const allGroups = await accountGroupService.getAllGroups() |
| const testGroups = allGroups.filter((g) => g.name.startsWith(TEST_PREFIX)) |
|
|
| if (testGroups.length === 2) { |
| log(`✅ 分组创建验证通过,共创建 ${testGroups.length} 个测试分组`, 'success') |
| } else { |
| throw new Error(`分组数量不正确,期望2个,实际${testGroups.length}个`) |
| } |
| } catch (error) { |
| log(`❌ 测试1失败: ${error.message}`, 'error') |
| throw error |
| } |
| } |
|
|
| |
| async function test2_createAccountsAndAddToGroup() { |
| log('\n📝 测试2: 创建账户并添加到分组', 'info') |
|
|
| try { |
| const claudeGroup = testData.groups.find((g) => g.platform === 'claude') |
|
|
| |
| const claudeAccount1 = await claudeAccountService.createAccount({ |
| name: `${TEST_PREFIX}Claude账户1`, |
| email: 'test1@example.com', |
| refreshToken: 'test_refresh_token_1', |
| accountType: 'group' |
| }) |
| testData.accounts.push({ ...claudeAccount1, type: 'claude' }) |
| log(`✅ 创建Claude OAuth账户1成功: ${claudeAccount1.name}`, 'success') |
|
|
| const claudeAccount2 = await claudeAccountService.createAccount({ |
| name: `${TEST_PREFIX}Claude账户2`, |
| email: 'test2@example.com', |
| refreshToken: 'test_refresh_token_2', |
| accountType: 'group' |
| }) |
| testData.accounts.push({ ...claudeAccount2, type: 'claude' }) |
| log(`✅ 创建Claude OAuth账户2成功: ${claudeAccount2.name}`, 'success') |
|
|
| |
| const consoleAccount = await claudeConsoleAccountService.createAccount({ |
| name: `${TEST_PREFIX}Console账户`, |
| apiUrl: 'https://api.example.com', |
| apiKey: 'test_api_key', |
| accountType: 'group' |
| }) |
| testData.accounts.push({ ...consoleAccount, type: 'claude-console' }) |
| log(`✅ 创建Claude Console账户成功: ${consoleAccount.name}`, 'success') |
|
|
| |
| await accountGroupService.addAccountToGroup(claudeAccount1.id, claudeGroup.id, 'claude') |
| log('✅ 添加账户1到分组成功', 'success') |
|
|
| await accountGroupService.addAccountToGroup(claudeAccount2.id, claudeGroup.id, 'claude') |
| log('✅ 添加账户2到分组成功', 'success') |
|
|
| await accountGroupService.addAccountToGroup(consoleAccount.id, claudeGroup.id, 'claude') |
| log('✅ 添加Console账户到分组成功', 'success') |
|
|
| |
| const members = await accountGroupService.getGroupMembers(claudeGroup.id) |
| if (members.length === 3) { |
| log(`✅ 分组成员验证通过,共有 ${members.length} 个成员`, 'success') |
| } else { |
| throw new Error(`分组成员数量不正确,期望3个,实际${members.length}个`) |
| } |
| } catch (error) { |
| log(`❌ 测试2失败: ${error.message}`, 'error') |
| throw error |
| } |
| } |
|
|
| |
| async function test3_platformConsistency() { |
| log('\n📝 测试3: 平台一致性验证', 'info') |
|
|
| try { |
| const geminiGroup = testData.groups.find((g) => g.platform === 'gemini') |
|
|
| |
| const claudeAccount = testData.accounts.find((a) => a.type === 'claude') |
|
|
| try { |
| await accountGroupService.addAccountToGroup(claudeAccount.id, geminiGroup.id, 'claude') |
| throw new Error('平台验证失败:Claude账户不应该能添加到Gemini分组') |
| } catch (error) { |
| if (error.message.includes('平台与分组平台不匹配')) { |
| log(`✅ 平台一致性验证通过:${error.message}`, 'success') |
| } else { |
| throw error |
| } |
| } |
| } catch (error) { |
| log(`❌ 测试3失败: ${error.message}`, 'error') |
| throw error |
| } |
| } |
|
|
| |
| async function test4_apiKeyBindGroup() { |
| log('\n📝 测试4: API Key绑定分组', 'info') |
|
|
| try { |
| const claudeGroup = testData.groups.find((g) => g.platform === 'claude') |
|
|
| |
| const apiKey = await apiKeyService.generateApiKey({ |
| name: `${TEST_PREFIX}API Key`, |
| description: '测试分组调度的API Key', |
| claudeAccountId: `group:${claudeGroup.id}`, |
| permissions: 'claude' |
| }) |
| testData.apiKeys.push(apiKey) |
| log(`✅ 创建API Key成功: ${apiKey.name} (绑定到分组: ${claudeGroup.name})`, 'success') |
|
|
| |
| const keyInfo = await redis.getApiKey(apiKey.id) |
| if (keyInfo && keyInfo.claudeAccountId === `group:${claudeGroup.id}`) { |
| log('✅ API Key分组绑定验证通过', 'success') |
| } else { |
| throw new Error('API Key分组绑定信息不正确') |
| } |
| } catch (error) { |
| log(`❌ 测试4失败: ${error.message}`, 'error') |
| throw error |
| } |
| } |
|
|
| |
| async function test5_groupSchedulingLoadBalance() { |
| log('\n📝 测试5: 分组调度负载均衡', 'info') |
|
|
| try { |
| const apiKey = testData.apiKeys[0] |
|
|
| |
| const selectionCount = {} |
| const totalSelections = 30 |
|
|
| for (let i = 0; i < totalSelections; i++) { |
| |
| const sessionHash = uuidv4() |
|
|
| const result = await unifiedClaudeScheduler.selectAccountForApiKey( |
| { |
| id: apiKey.id, |
| claudeAccountId: apiKey.claudeAccountId, |
| name: apiKey.name |
| }, |
| sessionHash |
| ) |
|
|
| if (!selectionCount[result.accountId]) { |
| selectionCount[result.accountId] = 0 |
| } |
| selectionCount[result.accountId]++ |
|
|
| |
| await sleep(50) |
| } |
|
|
| |
| log(`\n📊 负载均衡分布统计 (共${totalSelections}次选择):`, 'info') |
| const accounts = Object.keys(selectionCount) |
|
|
| for (const accountId of accounts) { |
| const count = selectionCount[accountId] |
| const percentage = ((count / totalSelections) * 100).toFixed(1) |
| const accountInfo = testData.accounts.find((a) => a.id === accountId) |
| log(` ${accountInfo.name}: ${count}次 (${percentage}%)`, 'info') |
| } |
|
|
| |
| const counts = Object.values(selectionCount) |
| const avgCount = totalSelections / accounts.length |
| const variance = |
| counts.reduce((sum, count) => sum + Math.pow(count - avgCount, 2), 0) / counts.length |
| const stdDev = Math.sqrt(variance) |
|
|
| log(`\n 平均选择次数: ${avgCount.toFixed(1)}`, 'info') |
| log(` 标准差: ${stdDev.toFixed(1)}`, 'info') |
|
|
| |
| if (stdDev < avgCount * 0.5) { |
| log('✅ 负载均衡验证通过,分布相对均匀', 'success') |
| } else { |
| log('⚠️ 负载分布不够均匀,但这可能是正常的随机波动', 'warning') |
| } |
| } catch (error) { |
| log(`❌ 测试5失败: ${error.message}`, 'error') |
| throw error |
| } |
| } |
|
|
| |
| async function test6_stickySession() { |
| log('\n📝 测试6: 会话粘性(Sticky Session)测试', 'info') |
|
|
| try { |
| const apiKey = testData.apiKeys[0] |
| const sessionHash = `test_session_${uuidv4()}` |
|
|
| |
| const firstSelection = await unifiedClaudeScheduler.selectAccountForApiKey( |
| { |
| id: apiKey.id, |
| claudeAccountId: apiKey.claudeAccountId, |
| name: apiKey.name |
| }, |
| sessionHash |
| ) |
|
|
| log(` 首次选择账户: ${firstSelection.accountId}`, 'info') |
|
|
| |
| let consistentCount = 0 |
| const testCount = 10 |
|
|
| for (let i = 0; i < testCount; i++) { |
| const selection = await unifiedClaudeScheduler.selectAccountForApiKey( |
| { |
| id: apiKey.id, |
| claudeAccountId: apiKey.claudeAccountId, |
| name: apiKey.name |
| }, |
| sessionHash |
| ) |
|
|
| if (selection.accountId === firstSelection.accountId) { |
| consistentCount++ |
| } |
|
|
| await sleep(100) |
| } |
|
|
| log(` 会话一致性: ${consistentCount}/${testCount} 次选择了相同账户`, 'info') |
|
|
| if (consistentCount === testCount) { |
| log('✅ 会话粘性验证通过,同一会话始终选择相同账户', 'success') |
| } else { |
| throw new Error(`会话粘性失败,只有${consistentCount}/${testCount}次选择了相同账户`) |
| } |
| } catch (error) { |
| log(`❌ 测试6失败: ${error.message}`, 'error') |
| throw error |
| } |
| } |
|
|
| |
| async function test7_accountAvailability() { |
| log('\n📝 测试7: 账户可用性检查', 'info') |
|
|
| try { |
| const apiKey = testData.apiKeys[0] |
| const accounts = testData.accounts.filter( |
| (a) => a.type === 'claude' || a.type === 'claude-console' |
| ) |
|
|
| |
| const firstAccount = accounts[0] |
| if (firstAccount.type === 'claude') { |
| await claudeAccountService.updateAccount(firstAccount.id, { isActive: false }) |
| } else { |
| await claudeConsoleAccountService.updateAccount(firstAccount.id, { isActive: false }) |
| } |
| log(` 已禁用账户: ${firstAccount.name}`, 'info') |
|
|
| |
| const selectionResults = [] |
| for (let i = 0; i < 20; i++) { |
| const sessionHash = uuidv4() |
| const result = await unifiedClaudeScheduler.selectAccountForApiKey( |
| { |
| id: apiKey.id, |
| claudeAccountId: apiKey.claudeAccountId, |
| name: apiKey.name |
| }, |
| sessionHash |
| ) |
|
|
| selectionResults.push(result.accountId) |
| } |
|
|
| |
| const selectedDisabled = selectionResults.includes(firstAccount.id) |
|
|
| if (!selectedDisabled) { |
| log('✅ 账户可用性验证通过,未选择禁用的账户', 'success') |
| } else { |
| throw new Error('错误:选择了已禁用的账户') |
| } |
|
|
| |
| if (firstAccount.type === 'claude') { |
| await claudeAccountService.updateAccount(firstAccount.id, { isActive: true }) |
| } else { |
| await claudeConsoleAccountService.updateAccount(firstAccount.id, { isActive: true }) |
| } |
| } catch (error) { |
| log(`❌ 测试7失败: ${error.message}`, 'error') |
| throw error |
| } |
| } |
|
|
| |
| async function test8_groupMemberManagement() { |
| log('\n📝 测试8: 分组成员管理', 'info') |
|
|
| try { |
| const claudeGroup = testData.groups.find((g) => g.platform === 'claude') |
| const account = testData.accounts.find((a) => a.type === 'claude') |
|
|
| |
| const accountGroups = await accountGroupService.getAccountGroup(account.id) |
| const hasTargetGroup = accountGroups.some((group) => group.id === claudeGroup.id) |
| if (hasTargetGroup) { |
| log('✅ 账户分组查询验证通过', 'success') |
| } else { |
| throw new Error('账户分组查询结果不正确') |
| } |
|
|
| |
| await accountGroupService.removeAccountFromGroup(account.id, claudeGroup.id) |
| log(` 从分组移除账户: ${account.name}`, 'info') |
|
|
| |
| const membersAfterRemove = await accountGroupService.getGroupMembers(claudeGroup.id) |
| if (!membersAfterRemove.includes(account.id)) { |
| log('✅ 账户移除验证通过', 'success') |
| } else { |
| throw new Error('账户移除失败') |
| } |
|
|
| |
| await accountGroupService.addAccountToGroup(account.id, claudeGroup.id, 'claude') |
| log(' 重新添加账户到分组', 'info') |
| } catch (error) { |
| log(`❌ 测试8失败: ${error.message}`, 'error') |
| throw error |
| } |
| } |
|
|
| |
| async function test9_emptyGroupHandling() { |
| log('\n📝 测试9: 空分组处理', 'info') |
|
|
| try { |
| |
| const emptyGroup = await accountGroupService.createGroup({ |
| name: `${TEST_PREFIX}空分组`, |
| platform: 'claude', |
| description: '测试空分组' |
| }) |
| testData.groups.push(emptyGroup) |
|
|
| |
| const apiKey = await apiKeyService.generateApiKey({ |
| name: `${TEST_PREFIX}空分组API Key`, |
| claudeAccountId: `group:${emptyGroup.id}`, |
| permissions: 'claude' |
| }) |
| testData.apiKeys.push(apiKey) |
|
|
| |
| try { |
| await unifiedClaudeScheduler.selectAccountForApiKey({ |
| id: apiKey.id, |
| claudeAccountId: apiKey.claudeAccountId, |
| name: apiKey.name |
| }) |
| throw new Error('空分组选择账户应该失败') |
| } catch (error) { |
| if (error.message.includes('has no members')) { |
| log(`✅ 空分组处理验证通过:${error.message}`, 'success') |
| } else { |
| throw error |
| } |
| } |
| } catch (error) { |
| log(`❌ 测试9失败: ${error.message}`, 'error') |
| throw error |
| } |
| } |
|
|
| |
| async function runTests() { |
| log('\n🚀 开始分组调度功能测试\n', 'info') |
|
|
| try { |
| |
| await redis.connect() |
| log('✅ Redis连接成功', 'success') |
|
|
| |
| await test1_createGroups() |
| await test2_createAccountsAndAddToGroup() |
| await test3_platformConsistency() |
| await test4_apiKeyBindGroup() |
| await test5_groupSchedulingLoadBalance() |
| await test6_stickySession() |
| await test7_accountAvailability() |
| await test8_groupMemberManagement() |
| await test9_emptyGroupHandling() |
|
|
| log('\n🎉 所有测试通过!分组调度功能工作正常', 'success') |
| } catch (error) { |
| log(`\n❌ 测试失败: ${error.message}`, 'error') |
| console.error(error) |
| } finally { |
| |
| if (CLEANUP_ON_FINISH) { |
| await cleanup() |
| } else { |
| log('\n⚠️ 测试数据未清理,请手动清理', 'warning') |
| } |
|
|
| |
| await redis.disconnect() |
| process.exit(0) |
| } |
| } |
|
|
| |
| runTests() |
|
|