Files
crawler-plugin/frontend-vue/tests/polling-network-error.test.ts
huangzd1997 dd0a32e89a task-33: 网络错误走退避且不触发终态
8 个测试固化网络错误语义:fetch 拒绝不触发 onTerminal、不修改
taskStatuses、任务集合不丢失、onError 上报、失败后不立即自动重试
(退避)、恢复后继续轮询并正常触发终态。

行为由任务 31 的失败计数与既有 catch 路径实现,本次为验证型测试。
2026-08-31 20:30:45 +08:00

135 lines
4.8 KiB
TypeScript

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()
})