import { test } from 'node:test' import assert from 'node:assert/strict' import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts' const tick = (ms = 0) => new Promise((r) => globalThis.setTimeout(r, ms)) interface StorageLike { map?: Map getItem: (k: string) => string | null setItem: (k: string, v: string) => void removeItem: (k: string) => void } let scopeCounter = 0 interface StorageHarness { loop: ReturnType> storage: Map fetchCount: () => number } async function makeStorageLoop( storage: StorageLike, key: string, items: Array<{ taskId?: number; status?: string }> = [], ): Promise { const storageMap = storage as unknown as { map: Map } let fetchCount = 0 ;(globalThis as Record).window = { localStorage: { getItem: (k: string) => storage.getItem(k), setItem: (k: string, v: string) => { storage.setItem(k, v) }, removeItem: (k: string) => { storage.removeItem(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), } const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({ scope: `storage-${scopeCounter++}`, storageKey: key, fetchProgress: async (ids) => ({ items: items.filter((item) => ids.includes(item.taskId ?? -1)), }), extractTaskId: (d) => d?.taskId, extractStatus: (d) => d?.status, getIntervalMs: () => 60000, }) return { loop, storage: storageMap.map, fetchCount: () => fetchCount } } function makeStorage(initial?: Record): StorageLike { const map = new Map(Object.entries(initial ?? {})) return { map, getItem: (k) => map.get(k) ?? null, setItem: (k, v) => { map.set(k, v) }, removeItem: (k) => { map.delete(k) }, } } test('test_storage_restore_valid_ids', async () => { const storage = makeStorage({ 'k:ids': JSON.stringify([3, 7]) }) const h = await makeStorageLoop(storage, 'k:ids') assert.deepEqual(h.loop.taskIds.value, [3, 7], '初始化从 storage 恢复任务列表') h.loop.dispose() }) test('test_storage_restore_filters_invalid', async () => { const storage = makeStorage({ 'k:ids': JSON.stringify([0, -5, 3.5, 'x', null, 9]) }) const h = await makeStorageLoop(storage, 'k:ids') // 规格只过滤"非正数/非数字":3.5 是正有限数,保留 assert.deepEqual(h.loop.taskIds.value, [3.5, 9], '过滤非正数/非数字,合法 id 保留') h.loop.dispose() }) test('test_storage_restore_empty_clears', async () => { const storage = makeStorage({ 'k:ids': JSON.stringify([]) }) const h = await makeStorageLoop(storage, 'k:ids') assert.deepEqual(h.loop.taskIds.value, []) await tick(10) assert.equal(h.storage.has('k:ids'), false, '空列表恢复后清除 storage key') h.loop.dispose() }) test('test_storage_terminal_removes', async () => { const storage = makeStorage({ 'k:ids': JSON.stringify([5, 6]) }) const h = await makeStorageLoop( storage, 'k:ids', [ { taskId: 5, status: 'SUCCESS' }, { taskId: 6, status: 'RUNNING' }, ], ) await tick(10) await h.loop.refreshOnce() assert.equal(h.storage.get('k:ids'), JSON.stringify([6]), '终态任务从 storage 同步移除') h.loop.dispose() }) test('test_storage_write_failure_ignored', async () => { const storage = { getItem: (k: string) => (k === 'k:ids' ? '[]' : null), setItem: (_k: string, _v: string) => { throw new Error('quota exceeded') }, removeItem: (_k: string) => { throw new Error('quota exceeded') }, } const h = await makeStorageLoop(storage, 'k:ids') await tick(10) await h.loop.refreshOnce() assert.equal(h.loop.taskIds.value.length, 0) assert.doesNotThrow(() => h.loop.reset([1])) assert.deepEqual(h.loop.taskIds.value, [1], '写失败不影响功能,任务仍入集合') h.loop.dispose() }) test('test_storage_bad_json', async () => { const storage = makeStorage({ 'k:ids': '{broken json' }) const h = await makeStorageLoop(storage, 'k:ids') assert.deepEqual(h.loop.taskIds.value, [], '坏 JSON 按空处理,不抛错') h.loop.dispose() }) test('test_storage_restore_starts_polling', async () => { let fetchCount = 0 const storage = makeStorage({ 'k:ids': JSON.stringify([11]) }) ;(globalThis as Record).window = { localStorage: { getItem: (k: string) => storage.getItem(k), setItem: (k: string, v: string) => { storage.setItem(k, v) }, removeItem: (k: string) => { storage.removeItem(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), } const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({ scope: `storage-poll-${scopeCounter++}`, storageKey: 'k:ids', fetchProgress: async () => { fetchCount += 1 return { items: [] } }, extractTaskId: (d) => d?.taskId, extractStatus: (d) => d?.status, getIntervalMs: () => 60000, }) await tick(20) assert.equal(fetchCount, 1, '从 storage 恢复后自动开始轮询') loop.dispose() }) test('test_storage_no_key_noop', async () => { const storage = makeStorage({}) const h = await makeStorageLoop(storage, 'k:ids') assert.deepEqual(h.loop.taskIds.value, [], '无 key 时任务列表为空') await tick(10) assert.equal(h.storage.has('k:ids'), false, '不写多余 key') assert.equal(h.fetchCount(), 0, '无任务不轮询') h.loop.dispose() })