task-36: 终态任务仅一次 onTerminal

8 个测试固化 markTerminal 语义:同一任务终态只回调一次(幂等)、
终态任务从轮询集合移除、重复响应不回调、重新 add 后可再触发、
多任务各自回调、markTerminal 先于移除、终态后 localStorage 同步
清理、停止轮询不触发回调。
This commit is contained in:
2026-08-31 20:36:18 +08:00
parent 745e3fe2c4
commit 9e8ecb57e5
+132
View File
@@ -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<string, string>()
;(globalThis as Record<string, unknown>).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<void>((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<string, string>()
storage.set('term:storage', JSON.stringify([11]))
;(globalThis as Record<string, unknown>).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<void>((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), '任务仍在集合,仅停止轮询不清理')
})