Files
cms-website/tests/unit/canonical.test.ts
T
浅念 0e8b2b5356 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>
2026-10-21 15:30:42 +08:00

47 lines
2.4 KiB
TypeScript
Raw 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.
// 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":[]}')
})
})