task-33: 网络错误走退避且不触发终态
8 个测试固化网络错误语义:fetch 拒绝不触发 onTerminal、不修改 taskStatuses、任务集合不丢失、onError 上报、失败后不立即自动重试 (退避)、恢复后继续轮询并正常触发终态。 行为由任务 31 的失败计数与既有 catch 路径实现,本次为验证型测试。
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts'
|
||||
|
||||
function setupWindow() {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
let scopeCounter = 0
|
||||
|
||||
interface Harness {
|
||||
loop: ReturnType<typeof useTaskProgressLoop<{ taskId?: number; status?: string }>>
|
||||
terminalCalls: Array<{ taskId: number; status: string }>
|
||||
errorCalls: unknown[]
|
||||
fetchCalls: () => number
|
||||
}
|
||||
|
||||
async function makeLoop(impl: () => Promise<{ items: unknown[] }>): Promise<Harness> {
|
||||
const terminalCalls: Array<{ taskId: number; status: string }> = []
|
||||
const errorCalls: unknown[] = []
|
||||
let fetchCount = 0
|
||||
const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({
|
||||
scope: `net-err-${scopeCounter++}`,
|
||||
fetchProgress: async (ids) => {
|
||||
fetchCount += 1
|
||||
return impl()
|
||||
},
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
onTerminal: (taskId, _detail, status) => { terminalCalls.push({ taskId, status }) },
|
||||
onError: (e) => { errorCalls.push(e) },
|
||||
})
|
||||
loop.reset([1, 2])
|
||||
await new Promise<void>((r) => globalThis.setTimeout(r, 0))
|
||||
return { loop, terminalCalls, errorCalls, fetchCalls: () => fetchCount }
|
||||
}
|
||||
|
||||
test('test_network_error_no_terminal', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw new Error('network down') })
|
||||
await h.loop.refreshOnce()
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.terminalCalls.length, 0, '网络错误不触发 onTerminal')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_network_error_no_status_change', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw new Error('network down') })
|
||||
await h.loop.refreshOnce()
|
||||
assert.deepEqual(h.loop.taskStatuses.value, {}, '网络错误不修改 taskStatuses')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_network_error_backoff_scheduled', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw new Error('network down') })
|
||||
// 首轮自动刷新失败后:不立刻重试(下一轮按 60s 正常间隔调度),
|
||||
// 短期内不应有新的 fetch 请求
|
||||
const before = h.fetchCalls()
|
||||
await new Promise<void>((r) => globalThis.setTimeout(r, 50))
|
||||
assert.equal(h.fetchCalls(), before, '失败后进入退避,不立即自动重试')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_network_error_on_error_called', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw new Error('network down') })
|
||||
assert.equal(h.errorCalls.length, 1, 'onError 被调用')
|
||||
assert.match((h.errorCalls[0] as Error).message, /network down/)
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_network_recovery', async () => {
|
||||
setupWindow()
|
||||
let fail = true
|
||||
const h = await makeLoop(async () => {
|
||||
if (fail) throw new Error('network down')
|
||||
return { items: [{ taskId: 1, status: 'RUNNING' }] }
|
||||
})
|
||||
await h.loop.refreshOnce()
|
||||
fail = false
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.loop.taskStatuses.value[1], 'RUNNING', '恢复后继续轮询并落地状态')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_network_error_repeated', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw new Error('network down') })
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await h.loop.refreshOnce()
|
||||
}
|
||||
assert.equal(h.errorCalls.length, 6, '多次失败仍退避上报')
|
||||
assert.equal(h.terminalCalls.length, 0)
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_network_error_keeps_task', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw new Error('network down') })
|
||||
await h.loop.refreshOnce()
|
||||
assert.deepEqual(h.loop.taskIds.value, [1, 2], '任务不丢失')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_network_error_then_success', async () => {
|
||||
setupWindow()
|
||||
let fail = true
|
||||
const h = await makeLoop(async () => {
|
||||
if (fail) throw new Error('network down')
|
||||
return { items: [{ taskId: 1, status: 'SUCCESS' }] }
|
||||
})
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.terminalCalls.length, 0, '网络错误阶段无终态回调')
|
||||
fail = false
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.terminalCalls.length, 1, '恢复后成功响应正常触发终态')
|
||||
assert.equal(h.terminalCalls[0].taskId, 1)
|
||||
assert.equal(h.terminalCalls[0].status, 'SUCCESS')
|
||||
h.loop.dispose()
|
||||
})
|
||||
Reference in New Issue
Block a user