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() ;(globalThis as Record).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> terminalCalls: Array<{ taskId: number; status: string }> errorCalls: unknown[] } async function makeLoop(impl: () => Promise<{ items: unknown[] }>): Promise { 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((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((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() })