task-32: 退避封顶

8 个测试固化退避封顶语义:达到 maxMs 后保持恒定(含低 maxMs、
高 factor 提前封顶)、成功后从基数重新起算、恰等于 maxMs 不溢出、
循环内失败计数映射退避间隔的集成、非法 maxMs 配置拒绝。

行为由任务 29/30 的 factor 与 maxMs 配置已实现,本次为验证型测试。
This commit is contained in:
2026-08-31 20:27:29 +08:00
parent ab55b20edd
commit 3e45aa2a91
+110
View File
@@ -0,0 +1,110 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import {
getTaskPollBackoffMs,
configureTaskPolling,
resetTaskPollingConfig,
} from '../src/shared/task-progress-config.ts'
import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts'
test('test_backoff_cap_reached', () => {
resetTaskPollingConfig()
assert.equal(getTaskPollBackoffMs(4), 8000)
assert.equal(getTaskPollBackoffMs(5), 10000, 'attempt 5 达到封顶')
})
test('test_backoff_stays_capped', () => {
resetTaskPollingConfig()
for (const attempt of [5, 6, 7, 10, 50]) {
assert.equal(getTaskPollBackoffMs(attempt), 10000, `attempt ${attempt} 保持 maxMs`)
}
})
test('test_cap_with_low_max', () => {
resetTaskPollingConfig()
configureTaskPolling({ backoffMaxMs: 1500 })
assert.equal(getTaskPollBackoffMs(0), 500)
assert.equal(getTaskPollBackoffMs(1), 1000)
assert.equal(getTaskPollBackoffMs(2), 1500, '低 maxMs 提前封顶')
assert.equal(getTaskPollBackoffMs(3), 1500)
resetTaskPollingConfig()
})
test('test_cap_with_high_factor', () => {
resetTaskPollingConfig()
configureTaskPolling({ backoffFactor: 5 })
assert.equal(getTaskPollBackoffMs(0), 500)
assert.equal(getTaskPollBackoffMs(1), 2500)
assert.equal(getTaskPollBackoffMs(2), 10000, '高 factor 快速封顶')
assert.equal(getTaskPollBackoffMs(3), 10000)
resetTaskPollingConfig()
})
test('test_cap_resets_after_success', () => {
resetTaskPollingConfig()
// 封顶只由 attempt 驱动:attempt 归 0 即从基数重新开始
assert.equal(getTaskPollBackoffMs(0), 500)
assert.equal(getTaskPollBackoffMs(50), 10000)
assert.equal(getTaskPollBackoffMs(0), 500, '成功后从 500 起')
assert.equal(getTaskPollBackoffMs(1), 1000)
})
test('test_cap_attempt_boundary', () => {
resetTaskPollingConfig()
configureTaskPolling({ backoffMaxMs: 2000 })
// 恰好等于封顶那一档:base × factor^1 = 1000 < 2000^2 = 2000 = 封顶
assert.equal(getTaskPollBackoffMs(1), 1000)
assert.equal(getTaskPollBackoffMs(2), 2000, '恰等于 maxMs 不溢出')
assert.equal(getTaskPollBackoffMs(3), 2000)
resetTaskPollingConfig()
})
test('test_cap_integration_loop', async () => {
resetTaskPollingConfig()
const storage = new Map<string, string>()
;(globalThis as Record<string, unknown>).window = {
localStorage: {
getItem: (k: string) => storage.get(k) ?? null,
setItem: (k: string, v: string) => { storage.set(k, v) },
removeItem: (k: string) => { storage.delete(k) },
},
setTimeout: (fn: () => void, ms: number) => globalThis.setTimeout(fn, ms),
clearTimeout: (id: unknown) => globalThis.clearTimeout(id as number),
setInterval: (fn: () => void, ms: number) => globalThis.setInterval(fn, ms),
clearInterval: (id: unknown) => globalThis.clearInterval(id as number),
}
const attempts: number[] = []
const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({
scope: 'cap-integration',
fetchProgress: async () => { throw new Error('net') },
extractTaskId: (d) => d?.taskId,
extractStatus: (d) => d?.status,
getIntervalMs: () => 60000,
onError: (_e, attempt) => attempts.push(attempt ?? -1),
})
loop.reset([1])
await new Promise<void>((r) => globalThis.setTimeout(r, 0))
for (let i = 0; i < 12; i++) {
await loop.refreshOnce()
}
const intervals = attempts.map((a) => getTaskPollBackoffMs(a))
assert.deepEqual(
intervals.slice(0, 7),
[500, 1000, 2000, 4000, 8000, 10000, 10000],
'失败计数映射到退避间隔,达到 maxMs 后保持',
)
const tail = intervals.slice(7)
assert.ok(tail.every((v) => v === 10000), '封顶后间隔恒等于 maxMs')
loop.dispose()
resetTaskPollingConfig()
})
test('test_cap_config_negative', () => {
resetTaskPollingConfig()
assert.throws(() => configureTaskPolling({ backoffMaxMs: 0 }), /退避上限必须为正数/)
assert.throws(() => configureTaskPolling({ backoffMaxMs: -5 }), /退避上限必须为正数/)
// 拒绝后退避仍按默认值工作,不产生非法间隔
assert.equal(getTaskPollBackoffMs(0), 500)
assert.equal(getTaskPollBackoffMs(50), 10000)
resetTaskPollingConfig()
})