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:
@@ -0,0 +1,46 @@
|
||||
// canonicalJson 共享导出测试(Plan 3 Task 1,前置事实 6):从 register.service.ts 提取到
|
||||
// apps/api/src/common/canonical.ts——register 侧改 import 零行为变更(既有 media_bundle 清单
|
||||
// 哈希测试继续绿的回归责任在 tests/integration/source-register.test.ts,此处钉导出本身的
|
||||
// 序列化定义:对象键递归排序、无空白、数组保序)。
|
||||
// 独立重算对照:期望串手写钉死,不 import 实现——定义漂移即爆红。
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { canonicalJson } from '../../apps/api/src/common/canonical.js'
|
||||
|
||||
describe('canonicalJson(共享导出,前置事实 6)', () => {
|
||||
it('对象键递归排序:任意嵌套层级都按键名字典序', () => {
|
||||
expect(canonicalJson({ b: 1, a: 2 })).toBe('{"a":2,"b":1}')
|
||||
expect(canonicalJson({ b: 1, a: { d: 2, c: 3 } })).toBe('{"a":{"c":3,"d":2},"b":1}')
|
||||
expect(canonicalJson({ z: { y: { x: { w: 1, v: 2 } } } })).toBe('{"z":{"y":{"x":{"v":2,"w":1}}}}')
|
||||
})
|
||||
|
||||
it('无空白:键与值、数组元素间均无空格/换行', () => {
|
||||
expect(canonicalJson({ a: [1, 2, 3], b: { c: 'x' } })).toBe('{"a":[1,2,3],"b":{"c":"x"}}')
|
||||
expect(canonicalJson({ a: 1, b: 2 })).not.toContain(' ')
|
||||
expect(canonicalJson({ a: { b: { c: [1, { d: 2 }] } } })).not.toMatch(/\s/)
|
||||
})
|
||||
|
||||
it('数组保序:元素顺序是语义的一部分,不排序', () => {
|
||||
expect(canonicalJson([3, 1, 2])).toBe('[3,1,2]')
|
||||
expect(canonicalJson([{ z: 1, a: 2 }, { y: 3, b: 4 }])).toBe('[{"a":2,"z":1},{"b":4,"y":3}]')
|
||||
})
|
||||
|
||||
it('原始值走 JSON.stringify 语义:字符串/数字/布尔/null', () => {
|
||||
expect(canonicalJson('x')).toBe('"x"')
|
||||
expect(canonicalJson('a"b')).toBe('"a\\"b"')
|
||||
expect(canonicalJson(3)).toBe('3')
|
||||
expect(canonicalJson(true)).toBe('true')
|
||||
expect(canonicalJson(null)).toBe('null')
|
||||
expect(canonicalJson({ a: null, b: true, c: 0, d: '' })).toBe('{"a":null,"b":true,"c":0,"d":""}')
|
||||
})
|
||||
|
||||
it('键排序按 UTF-16 码元(与 Array.prototype.sort 默认一致)——大写在前、非 ASCII 原样', () => {
|
||||
expect(canonicalJson({ b: 1, A: 2, a: 3 })).toBe('{"A":2,"a":3,"b":1}')
|
||||
expect(canonicalJson({ 图: 1, 中: 2 })).toBe(JSON.stringify({ 中: 2, 图: 1 }))
|
||||
})
|
||||
|
||||
it('空对象/空数组序列化', () => {
|
||||
expect(canonicalJson({})).toBe('{}')
|
||||
expect(canonicalJson([])).toBe('[]')
|
||||
expect(canonicalJson({ a: {}, b: [] })).toBe('{"a":{},"b":[]}')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user