5ea52e5291
行数据按需拉取(审查 F5 后续): - V125 给 biz_file_result 补 updated_at(DEFAULT/ON UPDATE 由数据库维护, 实体标注 insertStrategy/updateStrategy=NEVER —— 否则 selectById→updateById 的 写回会把旧值写回去、ON UPDATE 不触发,版本信号静默冻结) - 装配器回传 rowsVersion=「最后变更时间毫秒#行数」,5 个品牌工具页版本未变即跳过 带行明细的重型 batch;前端变更信号为 rowsVersion + status/fileStatus/fileReady 复合 (任务收尾常见「行早写完、之后才置成功」,只看行版本会把界面卡在旧状态) 修复线上缺陷(同一功能验证时暴露): - TaskProgressLightAssembler 列裁剪漏选 module_type 却用它做模块过滤 → getModuleType() 恒为 null → light 恒把任务判成 missing;第七批把 light 接进 跟价/定时匹配/商品风险的轮询后,消费方会把运行中任务判为 FAILED - 补选中列 + 守卫用例 taskQueryMustSelectModuleType(已反向验证:去掉修复即红) - 前端 lightClaimsAllTasksMissing:整体性 missing 结论用重型端点复核后再采信 契约与文档:light 白名单补 rowsVersion(Java 契约测试 / spec 06 §2 / 12 个端点描述) 测试:mvn test 2901 全绿;前端 npm test 765 全绿
226 lines
9.4 KiB
TypeScript
226 lines
9.4 KiB
TypeScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { http } from '../src/shared/api/http.ts'
|
||
import { API_ENDPOINTS } from '../src/shared/api/endpoints.ts'
|
||
import {
|
||
getModuleProgressLight,
|
||
type ProgressLightModule,
|
||
type TaskProgressLightItem,
|
||
} from '../src/shared/api/progress-light.ts'
|
||
import {
|
||
getPollingProgressBatch,
|
||
lightClaimsAllTasksMissing,
|
||
} from '../src/shared/api/task-progress-polling.ts'
|
||
import type { TaskProgressBatchVo } from '../src/shared/api/types/task.ts'
|
||
|
||
function setupWindow() {
|
||
;(globalThis as Record<string, unknown>).window = {
|
||
localStorage: { getItem: () => '42' },
|
||
location: { origin: 'http://localhost' },
|
||
}
|
||
}
|
||
|
||
function mockRequest(
|
||
t: Parameters<typeof test>[1] extends (t: infer T) => unknown ? T : never,
|
||
impl: (config: { url?: string; method?: string; params?: Record<string, unknown>; data?: unknown; timeout?: number }) => Promise<unknown>,
|
||
) {
|
||
t.mock.method(http, 'request', impl as never)
|
||
}
|
||
|
||
const okResponse = (data: unknown) => Promise.resolve({ data: { success: true, message: 'ok', data } })
|
||
|
||
const LIGHT_MODULES: ProgressLightModule[] = [
|
||
'publish', 'deleteBrand', 'productRisk', 'shopMatch', 'patrolDelete',
|
||
'queryAsin', 'shopDataCrawl', 'withdraw', 'priceTrack', 'similarAsin',
|
||
'appearancePatent', 'collectData',
|
||
]
|
||
|
||
test('test_light_url_module', async (t) => {
|
||
setupWindow()
|
||
const seen: { url?: string }[] = []
|
||
mockRequest(t, (config) => {
|
||
seen.push(config)
|
||
return okResponse({ items: [], missingTaskIds: [] })
|
||
})
|
||
for (const module of LIGHT_MODULES) {
|
||
const endpoint = (API_ENDPOINTS as Record<string, Record<string, string>>)[module].progressLight
|
||
assert.ok(endpoint && endpoint.endsWith('/tasks/progress/light'), `${module} 应有 progressLight 端点: ${endpoint}`)
|
||
await getModuleProgressLight(module, [1])
|
||
}
|
||
assert.equal(seen.length, LIGHT_MODULES.length)
|
||
assert.deepEqual(seen.map((c) => c.url), LIGHT_MODULES.map((m) => {
|
||
const raw = (API_ENDPOINTS as Record<string, Record<string, string>>)[m].progressLight
|
||
return `/newApi${raw}`
|
||
}), '每个模块的 light URL 与端点常量一一对应')
|
||
})
|
||
|
||
test('test_light_payload', async (t) => {
|
||
setupWindow()
|
||
let captured: { url?: string; method?: string; data?: unknown } = {}
|
||
mockRequest(t, (config) => {
|
||
captured = config
|
||
return okResponse({ items: [], missingTaskIds: [] })
|
||
})
|
||
await getModuleProgressLight('similarAsin', [5, 3, 5])
|
||
assert.equal(captured.url, '/newApi/api/similar-asin/tasks/progress/light')
|
||
assert.equal(captured.method, 'POST')
|
||
assert.deepEqual(captured.data, { taskIds: [3, 5] }, 'taskIds 去重排序后进 body')
|
||
|
||
const empty = await getModuleProgressLight('similarAsin', [])
|
||
assert.deepEqual(empty, { items: [], missingTaskIds: [] }, '空 taskIds 不发请求直接返回空结构')
|
||
})
|
||
|
||
test('test_light_response_shape', async (t) => {
|
||
setupWindow()
|
||
mockRequest(t, () => okResponse({
|
||
items: [
|
||
{ taskId: 1, status: 'RUNNING', statusCode: null, fileStatus: null, fileError: null, fileReady: false, updatedAt: '2026-09-01T10:00:00' },
|
||
{ taskId: 2, status: 'SUCCESS', statusCode: null, fileStatus: 'SUCCESS', fileError: null, fileReady: true, updatedAt: '2026-09-01T10:05:00' },
|
||
],
|
||
missingTaskIds: [],
|
||
}))
|
||
const result = await getModuleProgressLight('publish', [1, 2])
|
||
assert.equal(result.items.length, 2, 'items 数组保留')
|
||
assert.equal(result.items[0].taskId, 1)
|
||
assert.equal(result.items[0].status, 'RUNNING')
|
||
assert.equal(result.items[0].fileReady, false)
|
||
assert.equal(result.items[1].fileStatus, 'SUCCESS')
|
||
assert.equal(result.items[1].fileReady, true)
|
||
assert.equal(result.items[1].updatedAt, '2026-09-01T10:05:00')
|
||
assert.deepEqual(result.missingTaskIds, [])
|
||
})
|
||
|
||
test('test_light_fallback_flag', async (t) => {
|
||
setupWindow()
|
||
let called = 0
|
||
mockRequest(t, () => {
|
||
called += 1
|
||
return okResponse({ items: [], missingTaskIds: [] })
|
||
})
|
||
const result = await getModuleProgressLight('withdraw', [7], { useLight: false })
|
||
assert.equal(called, 0, 'useLight=false 时不发起 light 请求(调用方自行回退旧端点)')
|
||
assert.deepEqual(result, { items: [], missingTaskIds: [] })
|
||
|
||
const on = await getModuleProgressLight('withdraw', [7], { useLight: true })
|
||
assert.equal(called, 1, 'useLight=true(默认)时发起 light 请求')
|
||
assert.deepEqual(on, { items: [], missingTaskIds: [] })
|
||
})
|
||
|
||
test('test_light_missing_ids', async (t) => {
|
||
setupWindow()
|
||
mockRequest(t, () => okResponse({
|
||
items: [{ taskId: 1, status: 'RUNNING' }],
|
||
missingTaskIds: [9, 10],
|
||
}))
|
||
const result = await getModuleProgressLight('collectData', [1, 9, 10])
|
||
assert.deepEqual(result.missingTaskIds, [9, 10], 'missingTaskIds 映射为 number[]')
|
||
assert.equal(result.items.length, 1)
|
||
|
||
mockRequest(t, () => okResponse({ items: [{ taskId: 1, status: 'RUNNING' }] }))
|
||
const noKey = await getModuleProgressLight('collectData', [1], { force: true })
|
||
assert.deepEqual(noKey.missingTaskIds, [], '后端缺 missingTaskIds 字段时默认空数组')
|
||
})
|
||
|
||
test('test_light_timeout', async (t) => {
|
||
setupWindow()
|
||
let captured: { timeout?: number } = {}
|
||
mockRequest(t, (config) => {
|
||
captured = config
|
||
return okResponse({ items: [], missingTaskIds: [] })
|
||
})
|
||
await getModuleProgressLight('priceTrack', [1], { force: true })
|
||
assert.equal(captured.timeout, 10000, '进度类请求默认 10s 超时')
|
||
})
|
||
|
||
test('test_light_type_compat', async (t) => {
|
||
setupWindow()
|
||
mockRequest(t, () => okResponse({
|
||
items: [{ taskId: 1, status: 'PENDING' }],
|
||
missingTaskIds: [],
|
||
}))
|
||
const result = await getModuleProgressLight('shopMatch', [1], { force: true })
|
||
const batch: TaskProgressBatchVo<TaskProgressLightItem> = result
|
||
assert.ok('items' in batch && 'missingTaskIds' in batch, '结构与现有 TaskProgressBatchVo 兼容')
|
||
assert.equal(batch.items[0].taskId, 1)
|
||
assert.equal(batch.items[0].status, 'PENDING')
|
||
})
|
||
|
||
test('test_light_snapshot', async (t) => {
|
||
setupWindow()
|
||
const calls: Array<{ url?: string; method?: string; params?: Record<string, unknown>; data?: unknown }> = []
|
||
mockRequest(t, (config) => {
|
||
calls.push(config)
|
||
return okResponse({ items: [{ taskId: 1, status: 'RUNNING', fileReady: false }], missingTaskIds: [] })
|
||
})
|
||
await getModuleProgressLight('appearancePatent', [1], { force: true })
|
||
await getModuleProgressLight('deleteBrand', [2], { force: true })
|
||
assert.equal(calls[0].url, '/newApi/api/appearance-patent/tasks/progress/light')
|
||
assert.equal(calls[0].method, 'POST')
|
||
assert.deepEqual(calls[0].data, { taskIds: [1] })
|
||
assert.equal(calls[1].url, '/newApi/api/delete-brand/tasks/progress/light')
|
||
assert.deepEqual(calls[1].data, { taskIds: [2] })
|
||
})
|
||
|
||
test('test_light_reexport', async () => {
|
||
setupWindow()
|
||
const javaModules = await import('../src/shared/api/java-modules.ts')
|
||
const direct = await import('../src/shared/api/progress-light.ts')
|
||
assert.equal(javaModules.getModuleProgressLight, direct.getModuleProgressLight, 'java-modules 应 re-export getModuleProgressLight')
|
||
assert.equal(javaModules.TaskProgressLightItem, direct.TaskProgressLightItem, '类型应同一引用')
|
||
})
|
||
|
||
test('test_light_all_missing_needs_heavy_recheck', async (t) => {
|
||
setupWindow()
|
||
// light 恒判 missing(2026-09-14 线上真实故障形态)→ 必须用重型端点复核,不能直接采信
|
||
mockRequest(t, () => okResponse({ items: [], missingTaskIds: [9101] }))
|
||
let heavyCalls = 0
|
||
const heavy = { items: [{ taskId: 9101, status: 'RUNNING' }], missingTaskIds: [] }
|
||
|
||
const batch = await getPollingProgressBatch<{ items: unknown[]; missingTaskIds: number[] }>(
|
||
'priceTrack',
|
||
[9101],
|
||
{ fallback: () => { heavyCalls += 1; return Promise.resolve(heavy) } },
|
||
)
|
||
|
||
assert.equal(heavyCalls, 1, '整体性 missing 结论必须复核')
|
||
assert.deepEqual(batch, heavy, '以重型端点结果为准')
|
||
})
|
||
|
||
test('test_light_all_missing_confirmed_by_heavy', async (t) => {
|
||
setupWindow()
|
||
mockRequest(t, () => okResponse({ items: [], missingTaskIds: [9102] }))
|
||
const heavy = { items: [], missingTaskIds: [9102] }
|
||
|
||
const batch = await getPollingProgressBatch<{ items: unknown[]; missingTaskIds: number[] }>(
|
||
'withdraw',
|
||
[9102],
|
||
{ fallback: () => Promise.resolve(heavy) },
|
||
)
|
||
|
||
assert.deepEqual(batch, heavy, '重型端点也判不存在时结论一致')
|
||
})
|
||
|
||
test('test_light_with_items_skips_recheck', async (t) => {
|
||
setupWindow()
|
||
mockRequest(t, () => okResponse({ items: [{ taskId: 9103, status: 'RUNNING' }], missingTaskIds: [] }))
|
||
let heavyCalls = 0
|
||
|
||
const batch = await getPollingProgressBatch<{ items: unknown[]; missingTaskIds: number[] }>(
|
||
'shopMatch',
|
||
[9103],
|
||
{ fallback: () => { heavyCalls += 1; return Promise.resolve({ items: [], missingTaskIds: [] }) } },
|
||
)
|
||
|
||
assert.equal(heavyCalls, 0, '有条目时不做复核,保持省流')
|
||
assert.equal((batch.items || []).length, 1)
|
||
})
|
||
|
||
test('test_light_claims_all_missing_semantics', () => {
|
||
assert.equal(lightClaimsAllTasksMissing({ items: [], missingTaskIds: [1] }, [1]), true)
|
||
assert.equal(lightClaimsAllTasksMissing({ items: [], missingTaskIds: [1] }, [1, 2]), false, '部分缺失不算整体性结论')
|
||
assert.equal(lightClaimsAllTasksMissing({ items: [{ taskId: 1 }], missingTaskIds: [] }, [1]), false, '有条目即不成立')
|
||
assert.equal(lightClaimsAllTasksMissing({ items: [], missingTaskIds: [] }, [1]), false)
|
||
assert.equal(lightClaimsAllTasksMissing(null, [1]), false)
|
||
assert.equal(lightClaimsAllTasksMissing({ items: [], missingTaskIds: [1] }, []), false)
|
||
})
|