feat(import): job 骨架与生命周期
Plan 3 Task 1:sf import 命令骨架 + import_jobs 生命周期(空管道,Task 6 接真实管道)。 - import.service.ts(零装饰器):createImportJob——站点/快照解析(缺省按 (connection, kind='wxr') 选最新;三快照绑定按前置事实 13 记入报告,job 行不扩列)、幂等建行 (默认 key=site+wxr快照+三语义版本;重复提交复用既有 job 不重跑,并发撞唯一约束 重查收敛)、状态推进 queued→running→succeeded/failed(attempt/started_at/finished_at/ error_code/error_summary)、报告落 artifacts/import/<jobId>/reports/import.json (全四版本块:adapter/mapping/sanitizer/parser;job 行落 mapping_version) - 可预期错误带 code:SITE_NOT_FOUND / SNAPSHOT_NOT_FOUND / NO_WXR_SNAPSHOT / PIPELINE_FAILED——解析期失败零 job 行 - adapter 事件加法式补 postId(前置事实 12):unsupported/error 事件可选携带 String(postId),缺失时键缺席不硬造 source_id;row 事件与报告聚合零影响、不触发版本 bump - canonicalJson 提取至 apps/api/src/common/canonical.ts 共享导出(前置事实 6), register 侧改 import 零行为变更(既有清单哈希测试守护) - sf CLI + import 命令(--site/--snapshot/--key/--connection),输出 job 摘要与版本块 - 测试:tests/integration/import-job.test.ts(5 例:生命周期观察/最新快照解析/幂等复用 与并发收敛/可预期失败零落行/failed 路径)+ adapter postId 3 例 + canonical 单测 6 例 (265→279 全绿);typecheck 四段链/lint 绿;临时库 CLI 冒烟(register→import→复用)通过 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -37,11 +37,16 @@ export interface RowResult {
|
||||
* 全链事件判别联合。unsupported/error 分支与 mapItem 的 MappedResult 同形,仅 error
|
||||
* 分支补 `sourceType`(`wp:${postType}` 报告锚点:post_id 缺失时 mapItem 拒猜身份,
|
||||
* 但 post_type 本身仍可作计数归桶依据——命名约定同 unsupported 分支,仅为报告可读性)。
|
||||
*
|
||||
* Plan 3 Task 1 加法扩展(前置事实 12):unsupported/error 事件可选携带 `postId`
|
||||
* (WxrItem.postId 的 String 化;缺失时**键缺席**——不硬造 source_id)。用途:阶段 3
|
||||
* 导入把 unsupported/error 行也写进 source_records(source_id=post_id)的可审计性。
|
||||
* 纯加法可选字段:row 事件(UniversalRow 契约)与报告聚合均不受影响,不触发版本 bump。
|
||||
*/
|
||||
export type AdapterEvent =
|
||||
| RowResult
|
||||
| { kind: 'unsupported'; sourceType: string; reason: string; rawLocation: string }
|
||||
| { kind: 'error'; sourceType: string; issue: string; rawLocation: string }
|
||||
| { kind: 'unsupported'; sourceType: string; reason: string; rawLocation: string; postId?: string }
|
||||
| { kind: 'error'; sourceType: string; issue: string; rawLocation: string; postId?: string }
|
||||
| { kind: 'term'; term: WxrTerm }
|
||||
|
||||
/** 分段计时累计(可注入对象,运行中被本模块累加;消费完成后读取终值) */
|
||||
@@ -103,9 +108,15 @@ function assembleItem(
|
||||
timings: AdapterTimings,
|
||||
): AdapterEvent {
|
||||
const mapped = mapItem(item, mapConfig)
|
||||
if (mapped.kind === 'unsupported') return mapped
|
||||
// postId 附加规则(前置事实 12):post_id 存在 → String 化挂上(与 row.sourceId 同口径,
|
||||
// 负值原样);缺失 → 键缺席。error 分支今天只在 post_id 缺失时触发,条件式保持
|
||||
// “存在即挂”的统一不变量,未来 error 触发缘由扩展时无需回改此处。
|
||||
if (mapped.kind === 'unsupported') {
|
||||
return item.postId === null ? mapped : { ...mapped, postId: String(item.postId) }
|
||||
}
|
||||
if (mapped.kind === 'error') {
|
||||
return { ...mapped, sourceType: `wp:${item.postType}` }
|
||||
const base = { ...mapped, sourceType: `wp:${item.postType}` }
|
||||
return item.postId === null ? base : { ...base, postId: String(item.postId) }
|
||||
}
|
||||
|
||||
const { row } = mapped
|
||||
|
||||
@@ -385,6 +385,42 @@ describe('unsupported / error 汇聚', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsupported / error 事件加法式补 postId(Plan 3 Task 1 前置事实 12)', () => {
|
||||
it('unsupported(nav_menu_item):post_id 存在 → 事件携带 postId(String 化,含负值)', async () => {
|
||||
const xml = wxrDoc(
|
||||
itemXml({ postId: 401, postType: 'nav_menu_item' }) + itemXml({ postId: -7, postType: 'custom_block' }),
|
||||
)
|
||||
const events = await collect(runWxrAdapter(fromXml(xml), { mapConfig: { locale: 'zh-CN' } }))
|
||||
expect(events[0]).toMatchObject({
|
||||
kind: 'unsupported',
|
||||
sourceType: 'wp:nav_menu_item',
|
||||
postId: '401',
|
||||
})
|
||||
// 负值 postId 与 row.sourceId 同口径(String 原样,'-7' 不归一为 '7')
|
||||
expect(events[1]).toMatchObject({ kind: 'unsupported', sourceType: 'wp:custom_block', postId: '-7' })
|
||||
})
|
||||
|
||||
it('error(post_id 缺失):postId 键缺席——不硬造 source_id(报告照常按计数归桶)', async () => {
|
||||
const xml = wxrDoc(itemXml({ postId: null, postType: 'post' }))
|
||||
const events = await collect(runWxrAdapter(fromXml(xml), { mapConfig: { locale: 'zh-CN' } }))
|
||||
const ev = events[0]
|
||||
if (ev === undefined || ev.kind !== 'error') throw new Error('期望 error 事件')
|
||||
expect('postId' in ev).toBe(false)
|
||||
expect((ev as { postId?: string }).postId).toBeUndefined()
|
||||
// 报告聚合不受加法字段影响:failed 计数照常
|
||||
const report = await runWxrAdapterReport(fromXml(xml), { mapConfig: { locale: 'zh-CN' } })
|
||||
expect(report.totals).toStrictEqual({ rows: 0, terms: 0, unsupported: 0, failed: 1 })
|
||||
})
|
||||
|
||||
it('row 事件不受影响:RowResult 五键结构不变(postId 只加在 unsupported/error 变体上)', async () => {
|
||||
const events = await collect(
|
||||
runWxrAdapter(fromXml(wxrDoc(itemXml({ postId: 402 }))), { mapConfig: { locale: 'zh-CN' } }),
|
||||
)
|
||||
const ev = soleRow(events)
|
||||
expect(Object.keys(ev).sort()).toStrictEqual(['changed', 'issues', 'kind', 'row', 'sanitizeDiffs'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('wppop 解析失败路径全链贯通(Task 4 已实现,Task 6 验证贯通)', () => {
|
||||
const RAW = 'not-php-at-all'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user