File size: 17,666 Bytes
69b897d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 |
/**
* 分组调度功能测试脚本
* 用于测试账户分组管理和调度逻辑的正确性
*/
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')
// 删除测试API Keys
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')
}
}
}
}
// 测试1: 创建分组
async function test1_createGroups() {
log('\n📝 测试1: 创建账户分组', 'info')
try {
// 创建Claude分组
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')
// 创建Gemini分组
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
}
}
// 测试2: 创建账户并添加到分组
async function test2_createAccountsAndAddToGroup() {
log('\n📝 测试2: 创建账户并添加到分组', 'info')
try {
const claudeGroup = testData.groups.find((g) => g.platform === 'claude')
// 创建Claude OAuth账户
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')
// 创建Claude Console账户
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
}
}
// 测试3: 平台一致性验证
async function test3_platformConsistency() {
log('\n📝 测试3: 平台一致性验证', 'info')
try {
const geminiGroup = testData.groups.find((g) => g.platform === 'gemini')
// 尝试将Claude账户添加到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
}
}
// 测试4: API Key绑定分组
async function test4_apiKeyBindGroup() {
log('\n📝 测试4: API Key绑定分组', 'info')
try {
const claudeGroup = testData.groups.find((g) => g.platform === 'claude')
// 创建绑定到分组的API Key
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')
// 验证API Key信息
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
}
}
// 测试5: 分组调度负载均衡
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')
// 如果标准差小于平均值的50%,认为负载均衡效果良好
if (stdDev < avgCount * 0.5) {
log('✅ 负载均衡验证通过,分布相对均匀', 'success')
} else {
log('⚠️ 负载分布不够均匀,但这可能是正常的随机波动', 'warning')
}
} catch (error) {
log(`❌ 测试5失败: ${error.message}`, 'error')
throw error
}
}
// 测试6: 会话粘性测试
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')
// 使用相同的sessionHash多次请求
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
}
}
// 测试7: 账户可用性检查
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
}
}
// 测试8: 分组成员管理
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
}
}
// 测试9: 空分组处理
async function test9_emptyGroupHandling() {
log('\n📝 测试9: 空分组处理', 'info')
try {
// 创建一个空分组
const emptyGroup = await accountGroupService.createGroup({
name: `${TEST_PREFIX}空分组`,
platform: 'claude',
description: '测试空分组'
})
testData.groups.push(emptyGroup)
// 创建绑定到空分组的API Key
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 {
// 连接Redis
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')
}
// 关闭Redis连接
await redis.disconnect()
process.exit(0)
}
}
// 运行测试
runTests()
|