Files
crawler-plugin/frontend-vue/tests/brand-rows-version-wiring.test.ts
huangzd1997 5ea52e5291 perf(F5+): 行数据按需拉取(结果行版本信号)+ 修复 progress/light 恒判 missing
行数据按需拉取(审查 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 全绿
2026-09-14 12:08:25 +08:00

62 lines
3.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
/**
* 「行数据按需拉取」接线守卫(审查 F5 后续):
* 5 个品牌工具页用轻量端点的结果行版本跳过重型 batch;这里守住两件最容易写错的事
* ——①接线没被误删;②跳过分支的返回值必须符合调用方契约(曾经把 shopDataCrawl 写成裸 return
* 而调用方 waitForTerminal 把 falsy 当「任务已消失 → FAILED」,会误杀正在跑的任务)。
*/
const PAGES_DIR = join(dirname(fileURLToPath(import.meta.url)), '../src/pages/brand/components')
const WIRED_PAGES = [
'BrandQueryAsinTab.vue',
'BrandWithdrawTab.vue',
'BrandPatrolDeleteTab.vue',
'BrandShopDataCrawlTab.vue',
'BrandAppearancePatentTab.vue',
]
function sourceOf(page: string) {
return readFileSync(join(PAGES_DIR, page), 'utf8')
}
test('已接线的品牌工具页仍使用结果行版本跳过重型 batch', () => {
for (const page of WIRED_PAGES) {
const source = sourceOf(page)
assert.ok(source.includes('createRowsVersionTracker'), `${page} 缺少版本跟踪器声明`)
assert.ok(source.includes('selectTasksNeedingRows'), `${page} 缺少按需拉取判断`)
assert.ok(source.includes('getModuleProgressLight'), `${page} 缺少轻量端点调用`)
}
})
test('轻量请求失败必须回退为全量拉取(不得因此少拉任务)', () => {
// 多任务页:catch 里把 heavyIds 还原成全部任务
for (const page of ['BrandQueryAsinTab.vue', 'BrandWithdrawTab.vue', 'BrandPatrolDeleteTab.vue']) {
assert.match(sourceOf(page), /catch\s*\{[\s\S]{0,200}?heavyIds = ids/, `${page} 缺少轻量失败回退`)
}
assert.match(sourceOf('BrandAppearancePatentTab.vue'), /catch\s*\{[\s\S]{0,200}?heavyIds = taskIds/,
'BrandAppearancePatentTab 缺少轻量失败回退')
// 单任务页:catch 里不能有 return(直接落到批量拉取)
const shopDataCrawl = sourceOf('BrandShopDataCrawlTab.vue')
const catchBlock = shopDataCrawl.match(/catch\s*\{([^}]*)\}/)?.[1] ?? ''
assert.ok(catchBlock.length > 0, 'BrandShopDataCrawlTab 未找到 catch 分支')
assert.ok(!catchBlock.includes('return'), 'BrandShopDataCrawlTab 轻量失败时不得提前返回,必须照旧拉批量')
})
test('跳过分支的返回值符合调用方契约', () => {
const shopDataCrawl = sourceOf('BrandShopDataCrawlTab.vue')
// waitForTerminal: `if (!exists) return 'FAILED'` —— 任务仍在时必须返回 true
assert.match(shopDataCrawl, /selectTasksNeedingRows\([\s\S]{0,200}?return true/,
'BrandShopDataCrawlTab 跳过分支必须 return truefalse/undefined 会被当成任务已消失)')
const patrolDelete = sourceOf('BrandPatrolDeleteTab.vue')
// waitForTaskTerminal: `missingTaskIds.includes(taskId)` —— 未消失时返回空数组
assert.match(patrolDelete, /selectTasksNeedingRows\([\s\S]{0,300}?return \[\]/,
'BrandPatrolDeleteTab 跳过分支必须返回空 missingTaskIds 数组')
})