From 9e8ecb57e53c3a9701b43457a4bac32720008d51 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:36:18 +0800 Subject: [PATCH] =?UTF-8?q?task-36:=20=E7=BB=88=E6=80=81=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E4=BB=85=E4=B8=80=E6=AC=A1=20onTerminal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8 个测试固化 markTerminal 语义:同一任务终态只回调一次(幂等)、 终态任务从轮询集合移除、重复响应不回调、重新 add 后可再触发、 多任务各自回调、markTerminal 先于移除、终态后 localStorage 同步 清理、停止轮询不触发回调。 --- frontend-vue/tests/terminal-once.test.ts | 132 +++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 frontend-vue/tests/terminal-once.test.ts diff --git a/frontend-vue/tests/terminal-once.test.ts b/frontend-vue/tests/terminal-once.test.ts new file mode 100644 index 00000000..2f196383 --- /dev/null +++ b/frontend-vue/tests/terminal-once.test.ts @@ -0,0 +1,132 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { createTaskPollingCoordinator } from '../src/shared/task-polling-coordinator.ts' +import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts' + +test('test_terminal_called_once', () => { + const called: number[] = [] + const coordinator = createTaskPollingCoordinator({ onTerminal: (id) => called.push(id) }) + coordinator.add(5) + coordinator.markTerminal(5) + coordinator.markTerminal(5) + coordinator.markTerminal(5) + assert.deepEqual(called, [5], '同一任务终态只回调一次') + assert.equal(coordinator.stats().terminalCount, 1) +}) + +test('test_terminal_removed_from_poll', () => { + const coordinator = createTaskPollingCoordinator({}) + coordinator.add(7) + coordinator.add(8) + coordinator.markTerminal(7) + assert.deepEqual(coordinator.ids(), [8], '终态任务从轮询集合移除') +}) + +test('test_terminal_repeat_response', async () => { + 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), + } + const terminalCalls: number[] = [] + const coordinator = createTaskPollingCoordinator({ onTerminal: (id) => terminalCalls.push(id) }) + const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({ + scope: 'repeat-terminal', + fetchProgress: async () => ({ items: [{ taskId: 9, status: 'SUCCESS' }] }), + extractTaskId: (d) => d?.taskId, + extractStatus: (d) => d?.status, + getIntervalMs: () => 60000, + onTerminal: (id) => { terminalCalls.push(id) }, + coordinator, + }) + loop.reset([9]) + await new Promise((r) => globalThis.setTimeout(r, 0)) + await loop.refreshOnce() + const once = terminalCalls.filter((id) => id === 9).length + assert.equal(once, 1, '重复收到同任务终态不重复回调') + assert.equal(coordinator.has(9), false, '终态任务已从协调器移除') + loop.dispose() +}) + +test('test_terminal_after_readd', () => { + const called: number[] = [] + const coordinator = createTaskPollingCoordinator({ onTerminal: (id) => called.push(id) }) + coordinator.add(3) + coordinator.markTerminal(3) + coordinator.add(3) + assert.ok(coordinator.has(3), '重新 add 后任务回归集合') + coordinator.markTerminal(3) + assert.deepEqual(called, [3, 3], '重新 add 后可再次触发') +}) + +test('test_terminal_multiple_tasks', () => { + const called: number[] = [] + const coordinator = createTaskPollingCoordinator({ onTerminal: (id) => called.push(id) }) + coordinator.add(1) + coordinator.add(2) + coordinator.add(3) + coordinator.markTerminal(2) + coordinator.markTerminal(1) + coordinator.markTerminal(3) + assert.deepEqual(called, [2, 1, 3], '多任务各自回调一次') + assert.equal(coordinator.stats().terminalCount, 3) + assert.deepEqual(coordinator.ids(), []) +}) + +test('test_terminal_coordinator_order', () => { + const order: string[] = [] + const coordinator = createTaskPollingCoordinator({ + onTerminal: (id) => order.push(`coordinator:${id}`), + }) + coordinator.add(4) + coordinator.markTerminal(4) + assert.deepEqual(order, ['coordinator:4'], 'markTerminal 先于移除(回调时任务已不在集合)') + assert.equal(coordinator.has(4), false) +}) + +test('test_terminal_storage_cleared', async () => { + const storage = new Map() + storage.set('term:storage', JSON.stringify([11])) + ;(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), + } + const coordinator = createTaskPollingCoordinator({}) + const loop = useTaskProgressLoop<{ taskId?: number; status?: string }>({ + scope: 'storage-clear', + storageKey: 'term:storage', + fetchProgress: async () => ({ items: [{ taskId: 11, status: 'SUCCESS' }] }), + extractTaskId: (d) => d?.taskId, + extractStatus: (d) => d?.status, + getIntervalMs: () => 60000, + onTerminal: () => {}, + coordinator, + }) + await new Promise((r) => globalThis.setTimeout(r, 0)) + await loop.refreshOnce() + assert.equal(storage.has('term:storage'), false, '终态后 localStorage 同步清理') + loop.dispose() +}) + +test('test_terminal_dispose_no_call', () => { + const called: number[] = [] + const coordinator = createTaskPollingCoordinator({ onTerminal: (id) => called.push(id) }) + coordinator.add(2) + // dispose 等价于停止轮询:此后不应再有任何终态回调(没有 markTerminal) + assert.equal(called.length, 0) + assert.ok(coordinator.has(2), '任务仍在集合,仅停止轮询不清理') +})