From 89fff863331a23d26d3cedb745e1fd3997660a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Mon, 31 Aug 2026 20:52:02 +0800 Subject: [PATCH] =?UTF-8?q?task-38:=20=E5=90=8C=E8=BD=AE=E5=A4=9A=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=BB=88=E6=80=81=E5=88=86=E5=88=AB=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同一轮响应多任务同时终态:各自回调带各自 detail/status、 按响应顺序(非注册顺序)、一个抛错不影响其他、全部移除、 异步回调逐个 await。共 8 个测试。 --- .../tests/polling-multi-terminal.test.ts | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 frontend-vue/tests/polling-multi-terminal.test.ts diff --git a/frontend-vue/tests/polling-multi-terminal.test.ts b/frontend-vue/tests/polling-multi-terminal.test.ts new file mode 100644 index 00000000..063383fc --- /dev/null +++ b/frontend-vue/tests/polling-multi-terminal.test.ts @@ -0,0 +1,204 @@ +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 Item { + taskId?: number + status?: string + label?: string +} + +interface TerminalEvent { + taskId: number + detail: Item + status: string +} + +interface Harness { + loop: ReturnType> + terminalCalls: TerminalEvent[] + errors: unknown[] +} + +async function makeLoop( + items: Item[], + terminalImpl?: (event: TerminalEvent) => void, + resetIds: number[] = [1, 2, 3], +): Promise { + const terminalCalls: TerminalEvent[] = [] + const errors: unknown[] = [] + const loop = useTaskProgressLoop({ + scope: `multi-term-${scopeCounter++}`, + fetchProgress: async (ids) => ({ + items: items.filter((item) => ids.includes(item.taskId ?? -1)), + }), + extractTaskId: (d) => d?.taskId, + extractStatus: (d) => d?.status, + getIntervalMs: () => 60000, + onTerminal: (taskId, detail, status) => { + const event = { taskId, detail: detail ?? {}, status } + terminalCalls.push(event) + terminalImpl?.(event) + }, + onError: (e) => { errors.push(e) }, + }) + loop.reset(resetIds) + await new Promise((r) => globalThis.setTimeout(r, 0)) + return { loop, terminalCalls, errors } +} + +test('test_multi_terminal_each_called', async () => { + setupWindow() + const h = await makeLoop([ + { taskId: 1, status: 'SUCCESS' }, + { taskId: 2, status: 'FAILED' }, + { taskId: 3, status: 'SUCCESS' }, + ]) + await h.loop.refreshOnce() + assert.equal(h.terminalCalls.length, 3, '三个终态各回调一次') + assert.deepEqual(h.terminalCalls.map((e) => e.taskId), [1, 2, 3]) + h.loop.dispose() +}) + +test('test_multi_terminal_correct_detail', async () => { + setupWindow() + const h = await makeLoop([ + { taskId: 1, status: 'SUCCESS', label: 'a' }, + { taskId: 2, status: 'FAILED', label: 'b' }, + ]) + await h.loop.refreshOnce() + assert.equal(h.terminalCalls[0].detail.label, 'a', '回调携带各自 detail') + assert.equal(h.terminalCalls[1].detail.label, 'b') + h.loop.dispose() +}) + +test('test_multi_terminal_correct_status', async () => { + setupWindow() + const h = await makeLoop([ + { taskId: 1, status: 'SUCCESS' }, + { taskId: 2, status: 'FAILED' }, + ]) + await h.loop.refreshOnce() + assert.equal(h.terminalCalls[0].status, 'SUCCESS') + assert.equal(h.terminalCalls[1].status, 'FAILED', '各自 status 分别传入') + h.loop.dispose() +}) + +test('test_multi_terminal_order', async () => { + setupWindow() + // 轮询集合按 [3,2,1] 注册,响应按 [1,2,3] 返回:回调必须跟随响应顺序 + const h = await makeLoop( + [ + { taskId: 1, status: 'FAILED' }, + { taskId: 2, status: 'SUCCESS' }, + { taskId: 3, status: 'SUCCESS' }, + ], + undefined, + [3, 2, 1], + ) + await h.loop.refreshOnce() + assert.deepEqual( + h.terminalCalls.map((e) => e.taskId), + [1, 2, 3], + '回调顺序与响应顺序一致(非 taskIds 注册顺序)', + ) + h.loop.dispose() +}) + +test('test_multi_terminal_one_fails_other_runs', async () => { + setupWindow() + const h = await makeLoop( + [ + { taskId: 1, status: 'SUCCESS' }, + { taskId: 2, status: 'FAILED' }, + ], + (event) => { + if (event.taskId === 1) throw new Error('boom') + }, + ) + await h.loop.refreshOnce() + assert.equal(h.terminalCalls.length, 2, '一个抛错不影响另一个回调') + assert.equal(h.terminalCalls[1].taskId, 2) + h.loop.dispose() +}) + +test('test_multi_terminal_all_removed', async () => { + setupWindow() + const h = await makeLoop( + [ + { taskId: 1, status: 'SUCCESS' }, + { taskId: 2, status: 'FAILED' }, + ], + undefined, + [1, 2], + ) + await h.loop.refreshOnce() + assert.deepEqual(h.loop.taskIds.value, [], '全部终态任务移除') + h.loop.dispose() +}) + +test('test_multi_terminal_await_sequence', async () => { + setupWindow() + const order: string[] = [] + const h = await makeLoop( + [ + { taskId: 1, status: 'SUCCESS' }, + { taskId: 2, status: 'SUCCESS' }, + ], + (event) => { order.push(`cb:${event.taskId}`) }, + ) + await h.loop.refreshOnce() + assert.deepEqual(order, ['cb:1', 'cb:2'], '回调按响应顺序依次执行') + h.loop.dispose() +}) + +test('test_multi_terminal_async_on_terminal', async () => { + setupWindow() + const order: string[] = [] + let finished = 0 + const items = [ + { taskId: 1, status: 'SUCCESS' }, + { taskId: 2, status: 'SUCCESS' }, + ] + const loop = useTaskProgressLoop({ + scope: `multi-term-async-${scopeCounter++}`, + fetchProgress: async (ids) => ({ + items: items.filter((item) => ids.includes(item.taskId ?? -1)), + }), + extractTaskId: (d) => d?.taskId, + extractStatus: (d) => d?.status, + getIntervalMs: () => 60000, + onTerminal: async (taskId) => { + order.push(`start:${taskId}`) + await new Promise((r) => globalThis.setTimeout(r, 10)) + order.push(`end:${taskId}`) + finished += 1 + }, + }) + // reset 触发 watch 自动轮询一轮;等待该轮全部回调完成(勿手动 refreshOnce 双发) + loop.reset([1, 2]) + const deadline = Date.now() + 3000 + while (finished < 2 && Date.now() < deadline) { + await new Promise((r) => globalThis.setTimeout(r, 5)) + } + assert.equal(finished, 2, '两个异步回调均完成') + assert.deepEqual(order, ['start:1', 'end:1', 'start:2', 'end:2'], '异步回调逐个 await,不并发') + loop.dispose() +})