|
@@ -0,0 +1,178 @@
|
|
|
|
|
+import { test } from 'node:test'
|
|
|
|
|
+import assert from 'node:assert/strict'
|
|
|
|
|
+import { makeGitBook, chapter } from './_helper.js'
|
|
|
|
|
+import { determineNextState } from '../../src/state-machine/index.js'
|
|
|
|
|
+import { persistVolumeReview, persistRepair } from '../../src/state-machine/persist.js'
|
|
|
|
|
+import { gotoChapter } from '../../src/state-machine/flows/goto-chapter.js'
|
|
|
|
|
+import { retcon } from '../../src/state-machine/flows/retcon.js'
|
|
|
|
|
+import { run as relinkRun } from '../../src/commands/relink.js'
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 流程间接力测试(07-03 review 测试盲区):每个流程自身绿不够,
|
|
|
|
|
+ * 流程走完之后 next 判定还得对——本文件专测「流程完 → next」的交接。
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+const BOOK = 'spec_version: "7.0"\n书名: 测\n'
|
|
|
|
|
+
|
|
|
|
|
+const volEndChapter = (n) =>
|
|
|
|
|
+ `---\n章号: ${n}\n标题: 第${n}章\n卷: 1\n字数: 100\n章定位: 推进\n钩子: 危机钩-强\n情绪定位: 高潮\n收卷: 是\n---\n大战落幕。`
|
|
|
|
|
+
|
|
|
|
|
+test('接力(P1-1/P1-2):卷复盘完 → 产物已入档、新伏笔进缓存、next 直进序6', async () => {
|
|
|
|
|
+ const { ctx, git, cleanup } = await makeGitBook(
|
|
|
|
|
+ { 'book.yaml': BOOK },
|
|
|
|
|
+ { commits: [{ message: 'ch(1): 收卷', files: { '定稿/正文/0001-收.md': volEndChapter(1) } }] }
|
|
|
|
|
+ )
|
|
|
|
|
+ try {
|
|
|
|
|
+ const before = await determineNextState(ctx)
|
|
|
|
|
+ assert.equal(before.序, 4, '收卷章 + 无卷摘要 → 应触发卷复盘')
|
|
|
|
|
+
|
|
|
|
|
+ const r = await persistVolumeReview(ctx, {
|
|
|
|
|
+ 卷号: 1,
|
|
|
|
|
+ 卷摘要: '第一卷收束。',
|
|
|
|
|
+ 下卷卷纲: '# 第2卷\n新地图。',
|
|
|
|
|
+ 伏笔条目: [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: '伏笔-900',
|
|
|
|
|
+ frontMatter: { 强度: '中', 状态: '进行', 开启章: 1, 预计收尾: '第2卷', 最后推进章: 1 },
|
|
|
|
|
+ body: '## 描述\n新卷暗线。\n\n## 履历\n- 第1章:埋下',
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ })
|
|
|
|
|
+ assert.equal(r.ok, true, r.error)
|
|
|
|
|
+
|
|
|
|
|
+ const { stdout } = await git(['log', '-1', '--format=%s'])
|
|
|
|
|
+ assert.match(stdout.trim(), /^vol\(01\): /)
|
|
|
|
|
+
|
|
|
|
|
+ // P1-2:新伏笔不等下次定稿,立即在缓存可见
|
|
|
|
|
+ const rows = await ctx.cache.query("SELECT id FROM threads WHERE id = '伏笔-900'")
|
|
|
|
|
+ assert.equal(rows.length, 1, '复盘产出的伏笔条目应立即进缓存')
|
|
|
|
|
+
|
|
|
|
|
+ // 接力终点:next 不误触序2(复盘产物已入档),直进序6 起草第2章
|
|
|
|
|
+ const after = await determineNextState(ctx)
|
|
|
|
|
+ assert.equal(after.序, 6, `复盘完 next 应进序6,实际序${after.序}:${after.message}`)
|
|
|
|
|
+ assert.equal(after.dto.nextChapter, 2)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ await cleanup()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('接力(P1-2):goto 回退完 → 缓存同步刷,next 起草的是正确章号', async () => {
|
|
|
|
|
+ const { ctx, cleanup } = await makeGitBook(
|
|
|
|
|
+ { 'book.yaml': BOOK },
|
|
|
|
|
+ {
|
|
|
|
|
+ commits: [
|
|
|
|
|
+ { message: 'ch(1): 起', files: { '定稿/正文/0001-起.md': chapter(1) } },
|
|
|
|
|
+ { message: 'ch(2): 承', files: { '定稿/正文/0002-承.md': chapter(2) } },
|
|
|
|
|
+ { message: 'ch(3): 转', files: { '定稿/正文/0003-转.md': chapter(3) } },
|
|
|
|
|
+ ],
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ try {
|
|
|
|
|
+ const before = await determineNextState(ctx)
|
|
|
|
|
+ assert.equal(before.dto.nextChapter, 4)
|
|
|
|
|
+
|
|
|
|
|
+ const r = await gotoChapter(ctx, { chapterNum: 1, confirm: true })
|
|
|
|
|
+ assert.equal(r.ok, true, r.error)
|
|
|
|
|
+ assert.equal(r.cacheRefresh?.ok, true, '回退后应同步刷新缓存')
|
|
|
|
|
+
|
|
|
|
|
+ const after = await determineNextState(ctx)
|
|
|
|
|
+ assert.equal(after.序, 6)
|
|
|
|
|
+ assert.equal(after.dto.nextChapter, 2, '回到第1章后应起草第2章(缓存不刷会报第4章)')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ await cleanup()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('接力(P1-2/P1-3):修复完缓存即时可见,序2 给清单,relink 补登后 next 进正事', async () => {
|
|
|
|
|
+ const { ctx, git, cleanup } = await makeGitBook({
|
|
|
|
|
+ 'book.yaml': BOOK,
|
|
|
|
|
+ '定稿/正文/0001-起.md': '---\n坏: yaml: :\n---\n正文',
|
|
|
|
|
+ })
|
|
|
|
|
+ try {
|
|
|
|
|
+ const s0 = await determineNextState(ctx)
|
|
|
|
|
+ assert.equal(s0.序, 0, '解析失败应先进修复确认')
|
|
|
|
|
+
|
|
|
|
|
+ const target = '定稿/正文/0001-起.md'
|
|
|
|
|
+ const r = await persistRepair(
|
|
|
|
|
+ ctx,
|
|
|
|
|
+ { repairs: [{ file: target, content: chapter(1) }] },
|
|
|
|
|
+ { allowedFiles: [target] }
|
|
|
|
|
+ )
|
|
|
|
|
+ assert.equal(r.ok, true, r.error)
|
|
|
|
|
+
|
|
|
|
|
+ // P1-2:修复件不等下次定稿,立即进缓存
|
|
|
|
|
+ const rows = await ctx.cache.query('SELECT chapter_num FROM chapters')
|
|
|
|
|
+ assert.equal(rows.length, 1, '修复件应立即进缓存')
|
|
|
|
|
+
|
|
|
|
|
+ // 序2:修复件未入档 → 手改补登,dto 带变更清单(P1-3)
|
|
|
|
|
+ const s2 = await determineNextState(ctx)
|
|
|
|
|
+ assert.equal(s2.序, 2)
|
|
|
|
|
+ assert.deepEqual(s2.dto.变更文件, [target])
|
|
|
|
|
+ assert.ok(s2.message.includes('relink'), '序2 message 应指路 relink 命令')
|
|
|
|
|
+
|
|
|
|
|
+ // relink:补登通道(此前是死胡同——检测得到、没命令可执行)
|
|
|
|
|
+ const rl = await relinkRun([], { message: '修复解析失败的第1章' }, ctx)
|
|
|
|
|
+ assert.equal(rl.ok, true, rl.error)
|
|
|
|
|
+ const { stdout } = await git(['log', '-1', '--format=%s'])
|
|
|
|
|
+ assert.match(stdout.trim(), /^fix\(手改\): 修复解析失败的第1章$/)
|
|
|
|
|
+
|
|
|
|
|
+ // 接力终点:next 进正事
|
|
|
|
|
+ const s6 = await determineNextState(ctx)
|
|
|
|
|
+ assert.equal(s6.序, 6, `补登完 next 应进序6,实际序${s6.序}:${s6.message}`)
|
|
|
|
|
+ assert.equal(s6.dto.nextChapter, 2)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ await cleanup()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('接力(P1-2):retcon 完 → threads 缓存即时更新', async () => {
|
|
|
|
|
+ const { ctx, cleanup } = await makeGitBook({
|
|
|
|
|
+ 'book.yaml': BOOK,
|
|
|
|
|
+ '大纲/伏笔/伏笔-001-暗线.md':
|
|
|
|
|
+ '---\n强度: 高\n状态: 进行\n开启章: 1\n预计收尾: 第2卷\n最后推进章: 1\n---\n## 描述\n暗线。\n\n## 履历\n- 第1章:埋下',
|
|
|
|
|
+ })
|
|
|
|
|
+ try {
|
|
|
|
|
+ const r = await retcon(ctx, {
|
|
|
|
|
+ chapterNum: 1,
|
|
|
|
|
+ 原因: '这条线提前收掉',
|
|
|
|
|
+ threadUpdates: [{ id: '伏笔-001', updates: { 状态: '回收' } }],
|
|
|
|
|
+ })
|
|
|
|
|
+ assert.equal(r.ok, true, r.error)
|
|
|
|
|
+ const rows = await ctx.cache.query("SELECT status FROM threads WHERE id = '伏笔-001'")
|
|
|
|
|
+ assert.equal(rows[0]?.status, '回收', '吃书后缓存应立即反映条目新状态')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ await cleanup()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('relink:缺 --message 报错;无手改时明说无需补登', async () => {
|
|
|
|
|
+ const { ctx, cleanup } = await makeGitBook({ 'book.yaml': BOOK })
|
|
|
|
|
+ try {
|
|
|
|
|
+ const noMsg = await relinkRun([], {}, ctx)
|
|
|
|
|
+ assert.equal(noMsg.ok, false)
|
|
|
|
|
+ assert.ok(noMsg.error.includes('--message'))
|
|
|
|
|
+
|
|
|
|
|
+ const clean = await relinkRun([], { message: '没改什么' }, ctx)
|
|
|
|
|
+ assert.equal(clean.ok, true)
|
|
|
|
|
+ assert.ok(clean.output.includes('无需补登'))
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ await cleanup()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('序3 resume:dto 带工作区现存与从哪继续(P2-5)', async () => {
|
|
|
|
|
+ const { ctx, cleanup } = await makeGitBook({
|
|
|
|
|
+ 'book.yaml': BOOK,
|
|
|
|
|
+ '工作区/细纲.md': '## 本章要写到的事\nx',
|
|
|
|
|
+ '工作区/草稿-A.md': '草稿',
|
|
|
|
|
+ })
|
|
|
|
|
+ try {
|
|
|
|
|
+ const s = await determineNextState(ctx)
|
|
|
|
|
+ assert.equal(s.序, 3)
|
|
|
|
|
+ assert.ok(s.dto.工作区现存.includes('细纲.md'))
|
|
|
|
|
+ assert.ok(s.dto.工作区现存.includes('草稿-A.md'))
|
|
|
|
|
+ assert.equal(s.dto.从哪继续, '机检与两审', '最深工件是草稿 → 从机检/两审继续')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ await cleanup()
|
|
|
|
|
+ }
|
|
|
|
|
+})
|