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 interface Harness { loop: ReturnType> terminalCalls: Array<{ taskId: number; status: string }> errorCalls: unknown[] fetchCalls: () => number } async function makeLoop(impl: () => Promise<{ items: unknown[] }>): Promise { 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((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((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() })