relay.test.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { makeGitBook, chapter } from './_helper.js'
  4. import { determineNextState } from '../../src/state-machine/index.js'
  5. import { persistVolumeReview, persistRepair } from '../../src/state-machine/persist.js'
  6. import { gotoChapter } from '../../src/state-machine/flows/goto-chapter.js'
  7. import { retcon } from '../../src/state-machine/flows/retcon.js'
  8. import { run as relinkRun } from '../../src/commands/relink.js'
  9. /**
  10. * 流程间接力测试(07-03 review 测试盲区):每个流程自身绿不够,
  11. * 流程走完之后 next 判定还得对——本文件专测「流程完 → next」的交接。
  12. */
  13. const BOOK = 'spec_version: "7.0"\n书名: 测\n'
  14. const volEndChapter = (n) =>
  15. `---\n章号: ${n}\n标题: 第${n}章\n卷: 1\n字数: 100\n章定位: 推进\n钩子: 危机钩-强\n情绪定位: 高潮\n收卷: 是\n---\n大战落幕。`
  16. test('接力(P1-1/P1-2):卷复盘完 → 产物已入档、新伏笔进缓存、next 直进序6', async () => {
  17. const { ctx, git, cleanup } = await makeGitBook(
  18. { 'book.yaml': BOOK },
  19. { commits: [{ message: 'ch(1): 收卷', files: { '定稿/正文/0001-收.md': volEndChapter(1) } }] }
  20. )
  21. try {
  22. const before = await determineNextState(ctx)
  23. assert.equal(before.序, 4, '收卷章 + 无卷摘要 → 应触发卷复盘')
  24. const r = await persistVolumeReview(ctx, {
  25. 卷号: 1,
  26. 卷摘要: '第一卷收束。',
  27. 下卷卷纲: '# 第2卷\n新地图。',
  28. 伏笔条目: [
  29. {
  30. id: '伏笔-900',
  31. frontMatter: { 强度: '中', 状态: '进行', 开启章: 1, 预计收尾: '第2卷', 最后推进章: 1 },
  32. body: '## 描述\n新卷暗线。\n\n## 履历\n- 第1章:埋下',
  33. },
  34. ],
  35. })
  36. assert.equal(r.ok, true, r.error)
  37. const { stdout } = await git(['log', '-1', '--format=%s'])
  38. assert.match(stdout.trim(), /^vol\(01\): /)
  39. // P1-2:新伏笔不等下次定稿,立即在缓存可见
  40. const rows = await ctx.cache.query("SELECT id FROM threads WHERE id = '伏笔-900'")
  41. assert.equal(rows.length, 1, '复盘产出的伏笔条目应立即进缓存')
  42. // 接力终点:next 不误触序2(复盘产物已入档),直进序6 起草第2章
  43. const after = await determineNextState(ctx)
  44. assert.equal(after.序, 6, `复盘完 next 应进序6,实际序${after.序}:${after.message}`)
  45. assert.equal(after.dto.nextChapter, 2)
  46. } finally {
  47. await cleanup()
  48. }
  49. })
  50. test('接力(P1-2):goto 回退完 → 缓存同步刷,next 起草的是正确章号', async () => {
  51. const { ctx, cleanup } = await makeGitBook(
  52. { 'book.yaml': BOOK },
  53. {
  54. commits: [
  55. { message: 'ch(1): 起', files: { '定稿/正文/0001-起.md': chapter(1) } },
  56. { message: 'ch(2): 承', files: { '定稿/正文/0002-承.md': chapter(2) } },
  57. { message: 'ch(3): 转', files: { '定稿/正文/0003-转.md': chapter(3) } },
  58. ],
  59. }
  60. )
  61. try {
  62. const before = await determineNextState(ctx)
  63. assert.equal(before.dto.nextChapter, 4)
  64. const r = await gotoChapter(ctx, { chapterNum: 1, confirm: true })
  65. assert.equal(r.ok, true, r.error)
  66. assert.equal(r.cacheRefresh?.ok, true, '回退后应同步刷新缓存')
  67. const after = await determineNextState(ctx)
  68. assert.equal(after.序, 6)
  69. assert.equal(after.dto.nextChapter, 2, '回到第1章后应起草第2章(缓存不刷会报第4章)')
  70. } finally {
  71. await cleanup()
  72. }
  73. })
  74. test('接力(P1-2/P1-3):修复完缓存即时可见,序2 给清单,relink 补登后 next 进正事', async () => {
  75. const { ctx, git, cleanup } = await makeGitBook({
  76. 'book.yaml': BOOK,
  77. '定稿/正文/0001-起.md': '---\n坏: yaml: :\n---\n正文',
  78. })
  79. try {
  80. const s0 = await determineNextState(ctx)
  81. assert.equal(s0.序, 0, '解析失败应先进修复确认')
  82. const target = '定稿/正文/0001-起.md'
  83. const r = await persistRepair(
  84. ctx,
  85. { repairs: [{ file: target, content: chapter(1) }] },
  86. { allowedFiles: [target] }
  87. )
  88. assert.equal(r.ok, true, r.error)
  89. // P1-2:修复件不等下次定稿,立即进缓存
  90. const rows = await ctx.cache.query('SELECT chapter_num FROM chapters')
  91. assert.equal(rows.length, 1, '修复件应立即进缓存')
  92. // 序2:修复件未入档 → 手改补登,dto 带变更清单(P1-3)
  93. const s2 = await determineNextState(ctx)
  94. assert.equal(s2.序, 2)
  95. assert.deepEqual(s2.dto.变更文件, [target])
  96. assert.ok(s2.message.includes('relink'), '序2 message 应指路 relink 命令')
  97. // relink:补登通道(此前是死胡同——检测得到、没命令可执行)
  98. const rl = await relinkRun([], { message: '修复解析失败的第1章' }, ctx)
  99. assert.equal(rl.ok, true, rl.error)
  100. const { stdout } = await git(['log', '-1', '--format=%s'])
  101. assert.match(stdout.trim(), /^fix\(手改\): 修复解析失败的第1章$/)
  102. // 接力终点:next 进正事
  103. const s6 = await determineNextState(ctx)
  104. assert.equal(s6.序, 6, `补登完 next 应进序6,实际序${s6.序}:${s6.message}`)
  105. assert.equal(s6.dto.nextChapter, 2)
  106. } finally {
  107. await cleanup()
  108. }
  109. })
  110. test('接力(P1-2):retcon 完 → threads 缓存即时更新', async () => {
  111. const { ctx, cleanup } = await makeGitBook({
  112. 'book.yaml': BOOK,
  113. '大纲/伏笔/伏笔-001-暗线.md':
  114. '---\n强度: 高\n状态: 进行\n开启章: 1\n预计收尾: 第2卷\n最后推进章: 1\n---\n## 描述\n暗线。\n\n## 履历\n- 第1章:埋下',
  115. })
  116. try {
  117. const r = await retcon(ctx, {
  118. chapterNum: 1,
  119. 原因: '这条线提前收掉',
  120. threadUpdates: [{ id: '伏笔-001', updates: { 状态: '回收' } }],
  121. })
  122. assert.equal(r.ok, true, r.error)
  123. const rows = await ctx.cache.query("SELECT status FROM threads WHERE id = '伏笔-001'")
  124. assert.equal(rows[0]?.status, '回收', '吃书后缓存应立即反映条目新状态')
  125. } finally {
  126. await cleanup()
  127. }
  128. })
  129. test('relink:缺 --message 报错;无手改时明说无需补登', async () => {
  130. const { ctx, cleanup } = await makeGitBook({ 'book.yaml': BOOK })
  131. try {
  132. const noMsg = await relinkRun([], {}, ctx)
  133. assert.equal(noMsg.ok, false)
  134. assert.ok(noMsg.error.includes('--message'))
  135. const clean = await relinkRun([], { message: '没改什么' }, ctx)
  136. assert.equal(clean.ok, true)
  137. assert.ok(clean.output.includes('无需补登'))
  138. } finally {
  139. await cleanup()
  140. }
  141. })
  142. test('序3 resume:dto 带工作区现存与从哪继续(P2-5)', async () => {
  143. const { ctx, cleanup } = await makeGitBook({
  144. 'book.yaml': BOOK,
  145. '工作区/细纲.md': '## 本章要写到的事\nx',
  146. '工作区/草稿-A.md': '草稿',
  147. })
  148. try {
  149. const s = await determineNextState(ctx)
  150. assert.equal(s.序, 3)
  151. assert.ok(s.dto.工作区现存.includes('细纲.md'))
  152. assert.ok(s.dto.工作区现存.includes('草稿-A.md'))
  153. assert.equal(s.dto.从哪继续, '机检与两审', '最深工件是草稿 → 从机检/两审继续')
  154. } finally {
  155. await cleanup()
  156. }
  157. })