read-v6.test.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import path from 'node:path'
  4. import { promises as fs } from 'node:fs'
  5. import { readV6Project } from '../../src/migrate/read-v6.js'
  6. import { tempV6, tempV6Sqlite, inlineFixture } from './_v6.js'
  7. test('inline 形态:state 全量内联读取,键名/别名/三种正文命名全归一', async () => {
  8. const { v6Path, cleanup } = await tempV6(inlineFixture)
  9. try {
  10. const r = await readV6Project(v6Path)
  11. assert.equal(r.ok, true, r.error)
  12. const f = r.facts
  13. assert.equal(f.form, 'inline')
  14. assert.equal(f.project.title, '剑碎虚空') // project 键(非 project_info)
  15. assert.equal(f.project.genre, 'xianxia')
  16. // 三种正文命名归一:平坦带标题 / 遗留无标题 / 卷内 3 位
  17. assert.deepEqual(
  18. f.chapters.map((c) => [c.num, c.title, c.volumeHint]),
  19. [[1, '残剑出鞘', null], [2, null, null], [3, '剑灵初醒', 1]]
  20. )
  21. assert.match(f.chapters[0].body, /^晨雾未散/)
  22. // 实体 + alias_index 反查
  23. assert.equal(f.entities.length, 3)
  24. const luchen = f.entities.find((e) => e.id === 'luchen')
  25. assert.equal(luchen.name, '陆沉')
  26. assert.equal(luchen.isProtagonist, true)
  27. assert.ok(luchen.aliases.includes('小师弟'))
  28. // 伏笔规范化:status/tier 别名、planted 多键名
  29. assert.equal(f.foreshadowing.length, 2)
  30. assert.deepEqual(
  31. f.foreshadowing.map((x) => [x.status, x.tier, x.plantedChapter, x.resolvedChapter]),
  32. [['未回收', '核心', 1, null], ['已回收', '支线', 2, 3]]
  33. )
  34. // chapter_meta 键 "0001"/"3" 归一为数字
  35. assert.equal(f.chapterMeta.get(1).hook.type, '危机钩')
  36. assert.equal(f.chapterMeta.get(3).hook.strength, 'medium')
  37. assert.equal(f.summaries.get(1).frontMatter.hook_type, '危机钩')
  38. assert.equal(f.scratchpad.open_loops.length, 1)
  39. assert.equal(f.patterns.length, 2)
  40. assert.equal(f.outlines.volumes.length, 1)
  41. assert.match(f.outlines.volumes[0].详细大纲, /第3章:剑灵初醒/)
  42. assert.match(f.outlines.volumes[0].时间线, /演武场盘问/)
  43. assert.equal(f.settingFiles.length, 2)
  44. assert.equal(f.activeThreads.length, 1)
  45. } finally {
  46. await cleanup()
  47. }
  48. })
  49. test('sqlite 形态:精简 state + index.db 分置,实体/摘要/追读力从 db 读', async () => {
  50. const { v6Path, cleanup } = await tempV6Sqlite()
  51. try {
  52. const r = await readV6Project(v6Path)
  53. assert.equal(r.ok, true, r.error)
  54. const f = r.facts
  55. assert.equal(f.form, 'sqlite')
  56. assert.equal(f.project.title, '潮汐之下') // project_info 键
  57. const jy = f.entities.find((e) => e.id === 'jiangyao')
  58. assert.equal(jy.name, '江遥')
  59. assert.deepEqual(jy.aliases, ['小江'])
  60. assert.equal(jy.current.location, '滨海市') // current_json 解析
  61. assert.equal(f.dbSummaries.get(1), '江遥在退潮滩涂拾得停摆怀表,表盖刻字暗藏警告。')
  62. assert.equal(f.readingPower.get(1).hookType, '悬念钩')
  63. assert.deepEqual(f.readingPower.get(1).coolpointPatterns, ['异物入手'])
  64. assert.equal(f.stateChanges.length, 1)
  65. assert.equal(f.relationships.length, 1)
  66. // 缺 chase_debt 等表不炸(fixture db 故意没建)
  67. assert.equal(f.foreshadowing.length, 1) // 伏笔仍在精简 state.json
  68. } finally {
  69. await cleanup()
  70. }
  71. })
  72. test('容错:state.json 损坏 → 文件面照迁 + 如实 warning;源零写入', async () => {
  73. const { v6Path, cleanup } = await tempV6(inlineFixture)
  74. try {
  75. await fs.writeFile(path.join(v6Path, '.webnovel', 'state.json'), '{broken', 'utf8')
  76. const before = await fs.readFile(path.join(v6Path, '.webnovel', 'state.json'), 'utf8')
  77. const r = await readV6Project(v6Path)
  78. assert.equal(r.ok, true, r.error)
  79. assert.equal(r.facts.chapters.length, 3) // 正文照读
  80. assert.equal(r.facts.entities.length, 0)
  81. assert.ok(r.facts.warnings.some((w) => w.includes('state.json')))
  82. assert.equal(await fs.readFile(path.join(v6Path, '.webnovel', 'state.json'), 'utf8'), before)
  83. } finally {
  84. await cleanup()
  85. }
  86. })
  87. test('容错:整个 .webnovel/ 缺失 → 纯文件面迁移', async () => {
  88. const { v6Path, cleanup } = await tempV6(inlineFixture)
  89. try {
  90. await fs.rm(path.join(v6Path, '.webnovel'), { recursive: true, force: true })
  91. const r = await readV6Project(v6Path)
  92. assert.equal(r.ok, true, r.error)
  93. assert.equal(r.facts.chapters.length, 3)
  94. assert.equal(r.facts.foreshadowing.length, 0)
  95. assert.ok(r.facts.warnings.length > 0)
  96. } finally {
  97. await cleanup()
  98. }
  99. })
  100. test('不是 v6 项目(无 正文/ 无 .webnovel)→ 人话拒绝', async () => {
  101. const { v6Path, cleanup } = await tempV6(inlineFixture)
  102. try {
  103. await fs.rm(path.join(v6Path, '.webnovel'), { recursive: true, force: true })
  104. await fs.rm(path.join(v6Path, '正文'), { recursive: true, force: true })
  105. const r = await readV6Project(v6Path)
  106. assert.equal(r.ok, false)
  107. assert.match(r.error, /不像 v6 书项目/)
  108. } finally {
  109. await cleanup()
  110. }
  111. })