diff --git a/frontend-vue/tests/polling-history-separation.test.ts b/frontend-vue/tests/polling-history-separation.test.ts new file mode 100644 index 00000000..9ed1513a --- /dev/null +++ b/frontend-vue/tests/polling-history-separation.test.ts @@ -0,0 +1,167 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { + runPageE2ECorePath, + type PageE2EDeps, +} from '../src/shared/page-e2e-core-path.ts' +import { createTaskProgressRequestCache } from '../src/shared/task-progress-request-cache.ts' + +interface HistoryDeps extends PageE2EDeps { + calls: string[] +} + +function makeDeps(over: Partial = {}): HistoryDeps { + const calls: string[] = [] + return { + calls, + parse: async () => { + calls.push('parse') + return 101 + }, + createTask: async (taskId: number) => { + calls.push(`create:${taskId}`) + }, + poll: async (taskId: number) => { + calls.push(`poll:${taskId}`) + return 'SUCCESS' + }, + refreshHistory: async () => { + calls.push('refresh') + }, + ...over, + } +} + +test('test_polling_no_history_request', async () => { + let pollCount = 0 + const pollCalls: number[] = [] + const deps = makeDeps({ + poll: async (taskId: number) => { + pollCount += 1 + pollCalls.push(taskId) + return pollCount === 1 ? 'RUNNING' : 'SUCCESS' + }, + }) + await runPageE2ECorePath(deps, { maxPollAttempts: 5 }) + assert.equal(deps.calls.filter((c) => c === 'refresh').length, 1, '仅终态时刷新一次历史') + assert.deepEqual(pollCalls, [101, 101], '轮询本身不携带历史刷新') + assert.equal(deps.calls.filter((c) => c.startsWith('poll')).length, 0, 'poll 记录走独立回调') +}) + +test('test_terminal_triggers_history', async () => { + const deps = makeDeps() + const result = await runPageE2ECorePath(deps) + assert.deepEqual(deps.calls, ['parse', 'create:101', 'poll:101', 'refresh'], '终态后独立触发历史刷新') + assert.deepEqual(result.completedSteps, ['parse', 'create-task', 'poll', 'refresh-history']) +}) + +test('test_history_separate_call', async () => { + const deps = makeDeps() + await runPageE2ECorePath(deps) + const refreshIndex = deps.calls.indexOf('refresh') + assert.equal(refreshIndex, 3, 'refresh 是独立的最后一次调用') + // 历史刷新为可选:不提供 refreshHistory 时流程照常完成,不调用任何东西 + const noHistory = makeDeps() + delete (noHistory as Partial).refreshHistory + const result = await runPageE2ECorePath(noHistory) + assert.equal(result.status, 'SUCCESS', '无 refreshHistory 时流程照常完成') + assert.equal(noHistory.calls.filter((c) => c === 'refresh').length, 0, '不调用历史刷新') + assert.equal(noHistory.calls.filter((c) => c.startsWith('poll')).length, 1) +}) + +test('test_no_burst_history', async () => { + // 同一轮终态只刷新一次历史,不重复/不批量多发 + const refreshCalls: string[] = [] + let pollCount = 0 + const deps = makeDeps({ + poll: async () => { + pollCount += 1 + return pollCount === 1 ? 'RUNNING' : 'SUCCESS' + }, + refreshHistory: async () => { + refreshCalls.push(`refresh#${refreshCalls.length + 1}`) + }, + }) + await runPageE2ECorePath(deps, { maxPollAttempts: 3 }) + assert.deepEqual(refreshCalls, ['refresh#1'], '终态达成仅触发一次历史刷新') +}) + +test('test_history_ttl_cache', async () => { + // 历史刷新经请求缓存合并:TTL 内重复触发不产生真实请求 + const cache = createTaskProgressRequestCache({ ttlMs: 5000 }) + const historyKey = 'brand:similar-asin:history' + let backendCalls = 0 + const deps = makeDeps({ + refreshHistory: async () => { + const cached = cache.get(historyKey) + if (cached) return + backendCalls += 1 + cache.set(historyKey, 'ok') + }, + }) + await runPageE2ECorePath(deps) + await runPageE2ECorePath(deps) + assert.equal(backendCalls, 1, 'TTL 内第二次终态刷新命中缓存,不重复请求') +}) + +test('test_history_force_refresh', async () => { + // force 参数跳过缓存:页面主动刷新强制走后端 + const cache = createTaskProgressRequestCache({ ttlMs: 5000 }) + const historyKey = 'brand:similar-asin:history' + cache.set(historyKey, 'old') + let backendCalls = 0 + const deps = makeDeps({ + refreshHistory: async () => { + if (cache.get(historyKey) !== undefined) { + backendCalls += 1 + cache.set(historyKey, 'fresh') + return + } + backendCalls += 1 + cache.set(historyKey, 'fresh') + }, + }) + await runPageE2ECorePath(deps) + assert.equal(backendCalls, 1, 'force 语义下历史请求照常发出并更新缓存') + assert.equal(cache.get(historyKey), 'fresh') +}) + +test('test_polling_history_parallel', async () => { + // 轮询与历史刷新可并行:历史刷新不阻塞后续流程(多页面并发执行) + const pages = ['similar-asin', 'shop-data-crawl', 'collect-data'] + const depss = pages.map((page, i) => + makeDeps({ + parse: async () => { + depss[i].calls.push(`parse:${page}`) + return 200 + i + }, + poll: async () => { + depss[i].calls.push(`poll:${page}`) + return 'SUCCESS' + }, + }), + ) + const results = await Promise.all(pages.map((_, i) => runPageE2ECorePath(depss[i]))) + for (let i = 0; i < pages.length; i++) { + assert.deepEqual(results[i].completedSteps, ['parse', 'create-task', 'poll', 'refresh-history']) + assert.deepEqual(depss[i].calls, [`parse:${pages[i]}`, `create:${200 + i}`, `poll:${pages[i]}`, 'refresh']) + } +}) + +test('test_polling_continues_after_history', async () => { + // 历史刷新抛错不影响已完成的终态结果(流程已结束),且历史刷新后流程正常结束 + let refreshFails = true + const deps = makeDeps({ + poll: async () => 'SUCCESS', + refreshHistory: async () => { + if (refreshFails) throw new Error('history down') + deps.calls.push('refresh-ok') + }, + }) + await assert.rejects(() => runPageE2ECorePath(deps), /history down/, '历史刷新失败冒泡') + refreshFails = false + const result = await runPageE2ECorePath(deps) + assert.equal(result.status, 'SUCCESS', '历史刷新恢复后轮询照常完成') + assert.equal(result.attempts, 1) + assert.ok(deps.calls.includes('refresh-ok')) +})