import { test } from 'node:test' import assert from 'node:assert/strict' import { createTaskPollingBaseline, type TaskPollingBaseline, } from '../src/shared/task-polling-baseline.ts' import { useTaskProgressLoop } from '../src/shared/composables/useTaskProgressLoop.ts' interface FakeDetail { id: number name: string status: string } function detail(id: number): FakeDetail { return { id, name: `task-${id}`, status: id % 2 === 0 ? 'SUCCESS' : 'RUNNING' } } const byId = (d: FakeDetail) => d.id const enc = (v: unknown) => new TextEncoder().encode(JSON.stringify(v)).byteLength const tick = (ms = 0) => new Promise((r) => globalThis.setTimeout(r, ms)) let scopeCounter = 0 type Item = { taskId?: number; status?: string } interface BaselineLoopHarness { loop: ReturnType> } function makeLoop( options: Pick>[0], 'onBaseline'>, ): BaselineLoopHarness { 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 loop = useTaskProgressLoop({ scope: `baseline-loop-${scopeCounter++}`, fetchProgress: async (ids) => ({ items: ids.map((id) => ({ taskId: id, status: 'RUNNING' })), }), extractTaskId: (d) => d?.taskId, extractStatus: (d) => d?.status, getIntervalMs: () => 60000, ...options, }) return { loop } } test('test_task_081_polling_frontend_normal_default_path', () => { const baseline = createTaskPollingBaseline() baseline.recordRequest() baseline.recordRequest() baseline.recordRequest() const inserted = baseline.recordResponseItems([detail(1), detail(2)], byId) assert.equal(inserted, 2) const stats = baseline.stats() assert.equal(stats.requestCount, 3) assert.equal(stats.entryCount, 2) assert.equal(stats.droppedCount, 0) assert.ok(stats.cachedBytes > 0) assert.ok(stats.cachedBytes <= 1024 * 1024) assert.equal(stats.totalResponseBytes, enc([detail(1), detail(2)])) assert.equal(stats.largestResponseBytes, stats.totalResponseBytes) assert.deepEqual(stats.entryKeys, [1, 2]) }) test('test_task_081_polling_frontend_normal_multiple_items', () => { const baseline = createTaskPollingBaseline() const items = Array.from({ length: 10 }, (_, i) => detail(i + 1)) const inserted = baseline.recordResponseItems(items, byId) assert.equal(inserted, 10) const stats = baseline.stats() assert.equal(stats.entryCount, 10) assert.deepEqual(stats.entryKeys, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) // 覆盖更新不改变首次到达顺序 baseline.recordResponseItems([detail(5)], byId) const after = baseline.stats() assert.equal(after.entryCount, 10) assert.deepEqual(after.entryKeys, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) assert.equal(after.totalResponseBytes, enc(items) + enc([detail(5)])) assert.equal(after.largestResponseBytes, enc(items)) }) test('test_task_081_polling_frontend_normal_repeated_operation_is_idempotent', () => { const baseline = createTaskPollingBaseline() const items = [detail(1), detail(2), detail(3)] baseline.recordResponseItems(items, byId) baseline.recordResponseItems(items, byId) const stats = baseline.stats() assert.equal(stats.entryCount, 3) assert.equal(stats.droppedCount, 0) const single = createTaskPollingBaseline() single.recordResponseItems(items, byId) assert.equal(stats.cachedBytes, single.stats().cachedBytes) assert.equal(stats.totalResponseBytes, single.stats().totalResponseBytes * 2) }) test('test_task_081_polling_frontend_boundary_empty_input', () => { const baseline = createTaskPollingBaseline() assert.equal(baseline.recordResponseItems([], byId), 0) assert.equal(baseline.recordResponseItems([], byId), 0) const stats = baseline.stats() assert.equal(stats.requestCount, 0) assert.equal(stats.totalResponseBytes, 0) assert.equal(stats.largestResponseBytes, 0) assert.equal(stats.entryCount, 0) assert.equal(stats.cachedBytes, 0) assert.equal(stats.droppedCount, 0) assert.deepEqual(stats.entryKeys, []) }) test('test_task_081_polling_frontend_boundary_single_item', () => { const baseline = createTaskPollingBaseline() const inserted = baseline.recordResponseItems([detail(7)], byId) assert.equal(inserted, 1) const stats = baseline.stats() assert.equal(stats.entryCount, 1) assert.deepEqual(stats.entryKeys, [7]) assert.equal(stats.cachedBytes, enc(detail(7)) + 64) assert.equal(stats.totalResponseBytes, enc([detail(7)])) }) test('test_task_081_polling_frontend_boundary_limit_and_overflow', () => { // 条目数超限:驱逐最旧,缓存有界 const byEntries = createTaskPollingBaseline({ maxEntries: 3, maxCacheBytes: 1_000_000 }) byEntries.recordResponseItems([detail(1), detail(2), detail(3), detail(4), detail(5)], byId) const s1 = byEntries.stats() assert.equal(s1.entryCount, 3) assert.deepEqual(s1.entryKeys, [3, 4, 5]) assert.equal(s1.droppedCount, 2) // 单条超过字节上限:直接拒绝 const small = createTaskPollingBaseline({ maxCacheBytes: 200 }) const big = { id: 9, name: 'x'.repeat(1000), status: 'RUNNING' } const inserted = small.recordResponseItems([big], byId) assert.equal(inserted, 0) const s2 = small.stats() assert.equal(s2.entryCount, 0) assert.equal(s2.droppedCount, 1) assert.ok(s2.cachedBytes <= 200) // 批量累积超限:逐条驱逐最旧直到有界 const tiny = createTaskPollingBaseline({ maxCacheBytes: 300 }) tiny.recordResponseItems([detail(1), detail(2), detail(3)], byId) const s3 = tiny.stats() assert.equal(s3.entryCount, 2) assert.deepEqual(s3.entryKeys, [2, 3]) assert.equal(s3.droppedCount, 1) assert.ok(s3.cachedBytes <= 300) }) test('test_task_081_polling_frontend_invalid_input_rejected', () => { const baseline = createTaskPollingBaseline() assert.throws(() => baseline.recordResponseItems(null as never, byId), /items 必须是数组/) assert.throws(() => baseline.recordResponseItems([detail(1)], null as never), /extractKey 必须是函数/) assert.throws(() => createTaskPollingBaseline({ maxEntries: 0 }), /上限配置必须为正数/) assert.throws(() => createTaskPollingBaseline({ maxCacheBytes: -1 }), /上限配置必须为正数/) // 非法 key 跳过而不是抛错(与轮询层行为一致) const inserted = baseline.recordResponseItems( [{ id: 0, name: 'x', status: 'y' }, detail(2)], byId, ) assert.equal(inserted, 1) }) test('test_task_081_polling_frontend_dependency_failure_releases_resources', () => { const baseline = createTaskPollingBaseline() baseline.recordRequest() assert.throws( () => baseline.recordResponseItems([detail(1)], () => { throw new Error('boom') }), /boom/, ) const after = baseline.stats() assert.equal(after.entryCount, 0) assert.equal(after.totalResponseBytes, 0) assert.equal(after.requestCount, 1) assert.equal(after.droppedCount, 0) baseline.reset() assert.deepEqual(baseline.stats(), { requestCount: 0, totalResponseBytes: 0, largestResponseBytes: 0, entryCount: 0, cachedBytes: 0, droppedCount: 0, entryKeys: [], failureCount: 0, }) // 错误可恢复:修复后同一实例继续工作 baseline.recordResponseItems([detail(1)], byId) assert.equal(baseline.stats().entryCount, 1) }) test('test_baseline_request_count', async () => { const calls: TaskPollingBaseline[] = [] const { loop } = makeLoop({ onBaseline: (b) => { calls.push(b) } }) loop.reset([1]) await tick(10) assert.ok(calls.length >= 1, '每轮请求后回传基线') assert.equal(calls.at(-1)!.stats().requestCount, 1, '请求计数为 1(reset 触发一轮)') await loop.refreshOnce() assert.equal(calls.at(-1)!.stats().requestCount, 2, '手动 refreshOnce 再记一次') loop.dispose() }) test('test_baseline_response_size', async () => { const calls: TaskPollingBaseline[] = [] const { loop } = makeLoop({ onBaseline: (b) => { calls.push(b) } }) loop.reset([1, 2]) await tick(10) const stats = calls.at(-1)!.stats() loop.dispose() assert.ok(stats.totalResponseBytes > 0, '响应体大小累计') assert.equal( stats.totalResponseBytes, enc([ { taskId: 1, status: 'RUNNING' }, { taskId: 2, status: 'RUNNING' }, ]), '总字节 = 整轮响应体序列化字节数', ) assert.equal(stats.largestResponseBytes, stats.totalResponseBytes, '单次最大 = 当前唯一一轮') }) test('test_baseline_cache_usage', async () => { const calls: TaskPollingBaseline[] = [] const { loop } = makeLoop({ onBaseline: (b) => { calls.push(b) } }) loop.reset([1, 2]) await tick(10) const stats = calls.at(-1)!.stats() loop.dispose() assert.equal(stats.entryCount, 2, '每条进度一个缓存条目') assert.deepEqual(stats.entryKeys, [1, 2]) assert.ok(stats.cachedBytes >= enc({ taskId: 1, status: 'RUNNING' }), '缓存字节含条目编码') }) test('test_baseline_failure_count', async () => { const calls: TaskPollingBaseline[] = [] let fails = 0 const loop = useTaskProgressLoop({ scope: `baseline-fail-${scopeCounter++}`, fetchProgress: async () => { if (fails < 2) { fails += 1 throw new Error('network down') } return { items: [{ taskId: 1, status: 'RUNNING' }] } }, extractTaskId: (d) => d?.taskId, extractStatus: (d) => d?.status, getIntervalMs: () => 60000, onBaseline: (b) => { calls.push(b) }, }) loop.reset([1]) await tick(10) const first = calls.at(-1)!.stats() assert.equal(first.failureCount, 1, '首轮失败计数 1') await loop.refreshOnce() const second = calls.at(-1)!.stats() assert.equal(second.failureCount, 2, '连续失败累计到 2') await loop.refreshOnce() const third = calls.at(-1)!.stats() loop.dispose() assert.equal(third.failureCount, 2, '失败计数为累计值(清零仅经 reset;连续计数走 onError attempt)') }) test('test_baseline_callback_after_round', async () => { const calls: TaskPollingBaseline[] = [] const { loop } = makeLoop({ onBaseline: (b) => { calls.push(b) } }) loop.reset([1]) await tick(10) await loop.refreshOnce() assert.equal(calls.length, 2, '每轮 refreshOnce 结束后各回传一次基线') assert.equal(calls[1].stats().requestCount, 2, '回传的是同一实例(累计统计)') assert.strictEqual(calls[0], calls[1], '回传的是同一基线实例') loop.dispose() }) test('test_baseline_round_accuracy', async () => { const calls: TaskPollingBaseline[] = [] const { loop } = makeLoop({ onBaseline: (b) => { calls.push(b) } }) loop.reset([1]) await tick(10) // 重置后:请求量、条目数与请求一一对应,无跨轮串账 assert.equal(calls.at(-1)!.stats().requestCount, 1) loop.add(2) await tick(10) const second = calls.at(-1)!.stats() loop.dispose() assert.equal(second.requestCount, 2, 'add 触发新一轮请求') assert.equal(second.entryCount, 2, '第二轮响应写入新条目') }) test('test_baseline_reset', async () => { const calls: TaskPollingBaseline[] = [] const { loop } = makeLoop({ onBaseline: (b) => { calls.push(b) } }) loop.reset([1]) await tick(10) const baseline = calls.at(-1)! assert.equal(baseline.stats().requestCount, 1) baseline.reset() assert.deepEqual(baseline.stats(), { requestCount: 0, totalResponseBytes: 0, largestResponseBytes: 0, entryCount: 0, cachedBytes: 0, droppedCount: 0, entryKeys: [], failureCount: 0, }, '重置后基线清零') loop.dispose() }) test('test_baseline_optional_off', async () => { const { loop } = makeLoop({}) loop.reset([1]) await tick(10) // 不传 onBaseline:不创建基线,不抛错 loop.refreshOnce() await tick(10) assert.equal(loop.taskStatuses.value[1], 'RUNNING', '无基线时轮询照常工作') loop.dispose() })