Files
sub2api/frontend/src/utils/__tests__/usageLoadQueue.spec.ts
erio 99331a5285 fix: throttle Anthropic usage queries and pass through upstream HTTP errors
- Frontend: queue Anthropic OAuth/setup-token usage requests by proxy
  with random 1-1.5s interval to prevent upstream 429
- Backend: return ApplicationError with actual upstream status code
  instead of wrapping all errors as 500
- Handle component unmount to skip stale updates on page navigation
2026-03-05 19:12:49 +08:00

88 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it, vi } from 'vitest'
import { enqueueUsageRequest } from '../usageLoadQueue'
function delay(ms: number) {
return new Promise((r) => setTimeout(r, ms))
}
describe('usageLoadQueue', () => {
it('同组请求串行执行,间隔 >= 1s', async () => {
const timestamps: number[] = []
const makeFn = () => async () => {
timestamps.push(Date.now())
return 'ok'
}
const p1 = enqueueUsageRequest('anthropic', 'oauth', 1, makeFn())
const p2 = enqueueUsageRequest('anthropic', 'oauth', 1, makeFn())
const p3 = enqueueUsageRequest('anthropic', 'oauth', 1, makeFn())
await Promise.all([p1, p2, p3])
expect(timestamps).toHaveLength(3)
// 随机 1-1.5s 间隔,至少 950ms留一点误差
expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(950)
expect(timestamps[1] - timestamps[0]).toBeLessThan(1600)
expect(timestamps[2] - timestamps[1]).toBeGreaterThanOrEqual(950)
expect(timestamps[2] - timestamps[1]).toBeLessThan(1600)
})
it('不同组请求并行执行', async () => {
const timestamps: Record<string, number> = {}
const makeTracked = (key: string) => async () => {
timestamps[key] = Date.now()
return key
}
const p1 = enqueueUsageRequest('anthropic', 'oauth', 1, makeTracked('group1'))
const p2 = enqueueUsageRequest('anthropic', 'oauth', 2, makeTracked('group2'))
const p3 = enqueueUsageRequest('gemini', 'oauth', 1, makeTracked('group3'))
await Promise.all([p1, p2, p3])
// 不同组应几乎同时启动(差距 < 50ms
const values = Object.values(timestamps)
const spread = Math.max(...values) - Math.min(...values)
expect(spread).toBeLessThan(50)
})
it('请求失败时 reject后续任务继续执行', async () => {
const results: string[] = []
const p1 = enqueueUsageRequest('anthropic', 'oauth', 99, async () => {
throw new Error('fail')
})
const p2 = enqueueUsageRequest('anthropic', 'oauth', 99, async () => {
results.push('second')
return 'ok'
})
await expect(p1).rejects.toThrow('fail')
await p2
expect(results).toEqual(['second'])
})
it('返回值正确透传', async () => {
const result = await enqueueUsageRequest('test', 'oauth', null, async () => {
return { usage: 42 }
})
expect(result).toEqual({ usage: 42 })
})
it('proxy_id 为 null 的账号归为同一组', async () => {
const order: number[] = []
const makeFn = (n: number) => async () => {
order.push(n)
return n
}
const p1 = enqueueUsageRequest('anthropic', 'oauth', null, makeFn(1))
const p2 = enqueueUsageRequest('anthropic', 'oauth', null, makeFn(2))
await Promise.all([p1, p2])
// 同组串行,按入队顺序执行
expect(order).toEqual([1, 2])
})
})