task-34: HTTP 4xx/5xx 分类
8 个测试固化 HTTP 错误语义:4xx/5xx 记 onError 并进入退避、 不触发 onTerminal(5xx 不可解析明细时不误判 FAILED)、不清空已有 状态、恢复后继续轮询且部分 items 照常处理、未知状态不受影响。 行为由 http 层抛错与既有 catch 路径实现,本次为验证型测试。
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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
|
||||
|
||||
function httpError(status: number, data?: unknown): Error {
|
||||
const err = new Error(`Request failed with status code ${status}`) as Error & {
|
||||
isAxiosError?: boolean
|
||||
response?: { status: number; data?: unknown }
|
||||
}
|
||||
err.isAxiosError = true
|
||||
err.response = { status, data }
|
||||
return err
|
||||
}
|
||||
|
||||
interface Harness {
|
||||
loop: ReturnType<typeof useTaskProgressLoop<{ taskId?: number; status?: string }>>
|
||||
terminalCalls: Array<{ taskId: number; status: string }>
|
||||
errorCalls: unknown[]
|
||||
}
|
||||
|
||||
async function makeLoop(impl: () => Promise<{ items: unknown[] }>): Promise<Harness> {
|
||||
const terminalCalls: Array<{ taskId: number; status: string }> = []
|
||||
const errorCalls: unknown[] = []
|
||||
const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({
|
||||
scope: `http-err-${scopeCounter++}`,
|
||||
fetchProgress: async () => impl(),
|
||||
extractTaskId: (d) => d?.taskId,
|
||||
extractStatus: (d) => d?.status,
|
||||
getIntervalMs: () => 60000,
|
||||
onTerminal: (taskId, _d, status) => { terminalCalls.push({ taskId, status }) },
|
||||
onError: (e) => { errorCalls.push(e) },
|
||||
})
|
||||
loop.reset([1])
|
||||
await new Promise<void>((r) => globalThis.setTimeout(r, 0))
|
||||
return { loop, terminalCalls, errorCalls }
|
||||
}
|
||||
|
||||
test('test_4xx_on_error', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw httpError(404) })
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.errorCalls.length, 2, '4xx 记 onError')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_5xx_on_error', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw httpError(500) })
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.errorCalls.length, 2, '5xx 记 onError')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_5xx_no_terminal_failed', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw httpError(503, { message: 'upstream down' }) })
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.terminalCalls.length, 0, '5xx 且无法解析明细时不误判 FAILED')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_5xx_backoff', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw httpError(500) })
|
||||
const before = h.errorCalls.length
|
||||
await new Promise<void>((r) => globalThis.setTimeout(r, 50))
|
||||
assert.equal(h.errorCalls.length, before, '5xx 后不立即自动重试(退避)')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_5xx_unknown_status_untouched', async () => {
|
||||
setupWindow()
|
||||
let fail = true
|
||||
const h = await makeLoop(async () => {
|
||||
if (fail) throw httpError(500)
|
||||
return { items: [{ taskId: 1, status: 'WEIRD_STATE' }] }
|
||||
})
|
||||
fail = false
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.loop.taskStatuses.value[1], 'WEIRD_STATE', '恢复后未知状态照常落地')
|
||||
assert.equal(h.terminalCalls.length, 0, '未知状态不触发终态')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_4xx_recovery', async () => {
|
||||
setupWindow()
|
||||
let fail = true
|
||||
const h = await makeLoop(async () => {
|
||||
if (fail) throw httpError(400)
|
||||
return { items: [{ taskId: 1, status: 'RUNNING' }] }
|
||||
})
|
||||
await h.loop.refreshOnce()
|
||||
fail = false
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.loop.taskStatuses.value[1], 'RUNNING', '4xx 后恢复继续轮询')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_status_500_partial_items', async () => {
|
||||
setupWindow()
|
||||
let fail = true
|
||||
const h = await makeLoop(async () => {
|
||||
if (fail) throw httpError(500)
|
||||
return { items: [{ taskId: 1, status: 'SUCCESS' }, { taskId: 2, status: 'RUNNING' }] }
|
||||
})
|
||||
fail = false
|
||||
await h.loop.refreshOnce()
|
||||
assert.equal(h.terminalCalls.length, 1, '5xx 恢复后部分 items 仍处理')
|
||||
assert.equal(h.terminalCalls[0].taskId, 1)
|
||||
assert.equal(h.loop.taskStatuses.value[2], 'RUNNING')
|
||||
h.loop.dispose()
|
||||
})
|
||||
|
||||
test('test_5xx_no_status_wipe', async () => {
|
||||
setupWindow()
|
||||
const h = await makeLoop(async () => { throw httpError(500) })
|
||||
// 先正常落地一个状态,再出现 5xx
|
||||
const loop2 = h.loop
|
||||
const original = loop2.taskStatuses
|
||||
original.value = { 1: 'RUNNING' }
|
||||
await loop2.refreshOnce()
|
||||
assert.equal(loop2.taskStatuses.value[1], 'RUNNING', '5xx 不清空已有状态')
|
||||
loop2.dispose()
|
||||
})
|
||||
Reference in New Issue
Block a user