main-loop.test.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import os from 'node:os'
  4. import path from 'node:path'
  5. import { promises as fs } from 'node:fs'
  6. import { execFile } from 'node:child_process'
  7. import { promisify } from 'node:util'
  8. import { CacheManager } from '../../src/cache/index.js'
  9. import { persistCreateBook, persistDraftOutline } from '../../src/state-machine/persist.js'
  10. import { runReviews } from '../../src/review/index.js'
  11. import { finalizeChapter } from '../../src/finalize/index.js'
  12. import { determineNextState } from '../../src/state-machine/index.js'
  13. const exec = promisify(execFile)
  14. // 桩两审:零问题通过,用于驱动主循环不引入真模型
  15. const stubReviewers = {
  16. factCheck: async () => ({ chapter: 1, issues: [] }),
  17. editorial: async () => ({ chapter: 1, issues: [] }),
  18. }
  19. const charCard = `---\n姓名: 林晚\n状态: 在世\n位置: 青云宗\n境界: 练气三层\n---\n## 设定\n玉佩线索。`
  20. const timeline = '| 章 | 一句话事件 |\n| --- | --- |\n| 1 | 林晚得玉佩 |\n'
  21. /**
  22. * 主循环端到端(review 推荐的 P0 锁定测试):
  23. * 建书(persistCreateBook 内部 git init) → 备料 → 两审(桩) → 定稿(刷新缓存) → next
  24. * 期望 next 报序6 且 nextChapter = 已定稿章 + 1(不重抄)。
  25. * 这条在 P0-1 修复前会红:定稿不刷新缓存 → next 仍说 maxChapter=0 → 起草第 1 章。
  26. */
  27. test('主循环:建书→备料→两审(桩)→定稿→next 不重抄最新章', async () => {
  28. const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-loop-'))
  29. const dbDir = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-loop-db-'))
  30. const cache = new CacheManager(path.join(dbDir, 'index.db'))
  31. const ctx = { repoPath: root, cache }
  32. const git = (a) => exec('git', a, { cwd: root })
  33. try {
  34. // 1. 建书:persistCreateBook 内部完成 git init + .gitignore + core.quotepath(P0-2)
  35. const r1 = await persistCreateBook(ctx, {
  36. book: { spec_version: '7.0', 书名: '测', 卷规模: 40, 体检周期: 50 },
  37. 总纲: '# 总纲\n## 结局\nx',
  38. 卷纲: '# 第1卷\n入门',
  39. })
  40. assert.equal(r1.ok, true, r1.error)
  41. // 建书产物 + 角色卡 + 时间线 一起入档(避免序2 手改误触)
  42. await fs.mkdir(path.join(root, '定稿/设定/角色'), { recursive: true })
  43. await fs.writeFile(path.join(root, '定稿/设定/角色/林晚.md'), charCard, 'utf8')
  44. await fs.mkdir(path.join(root, '定稿/设定/时间线'), { recursive: true })
  45. await fs.writeFile(path.join(root, '定稿/设定/时间线/第01卷.md'), timeline, 'utf8')
  46. await git(['config', 'user.email', 't@example.com'])
  47. await git(['config', 'user.name', 'test'])
  48. await git(['add', '-A'])
  49. await git(['commit', '-q', '-m', 'init book'])
  50. // .gitignore 真的 ignore 了 .cache / 工作区(P0-2 旁证)
  51. const gi = await fs.readFile(path.join(root, '.gitignore'), 'utf8')
  52. assert.ok(gi.includes('.cache/') && gi.includes('工作区/'), '.gitignore 应 ignore .cache 与 工作区')
  53. // 2. next → 序6 起草第 1 章
  54. await cache.ensureReady(root)
  55. let s = await determineNextState(ctx)
  56. assert.equal(s.序, 6, `建书后应序6,实际:${JSON.stringify(s)}`)
  57. assert.equal(s.dto.nextChapter, 1)
  58. // 3. 备料:细纲 + 草稿
  59. await persistDraftOutline(ctx, { 细纲: '## 本章要写到的事\n林晚突破练气四层。\n' })
  60. await fs.mkdir(path.join(root, '工作区'), { recursive: true })
  61. await fs.writeFile(path.join(root, '工作区/草稿-1.md'), '林晚运转功法,突破到练气四层。', 'utf8')
  62. // 4. 两审(桩)→ 落 审稿.md + 评审报告/
  63. const rv = await runReviews(ctx, {
  64. chapterNum: 1,
  65. draftPath: '工作区/草稿-1.md',
  66. mode: 'complete',
  67. reviewers: stubReviewers,
  68. 待确认新专名: [],
  69. 章摘要: '林晚突破。',
  70. })
  71. assert.equal(rv.ok, true, rv.errors?.join('; '))
  72. // 5. 定稿第 1 章(P0-1:定稿后刷新缓存)
  73. const fr = await finalizeChapter(ctx, {
  74. chapterNum: 1,
  75. frontMatter: {
  76. 章号: 1, 标题: '初露', 卷: 1, 视角: '林晚', 字数: 100,
  77. 章定位: '推进', 钩子: '危机钩-强', 情绪定位: '铺垫',
  78. },
  79. body: '林晚运转功法,突破到练气四层。',
  80. summary: '林晚突破练气四层。',
  81. workspaceFiles: ['草稿-1.md', '细纲.md', '审稿.md'],
  82. })
  83. assert.equal(fr.ok, true, fr.error)
  84. // 6. next → 序6 起草第 2 章(P0-1 核心断言:不重抄第 1 章)
  85. s = await determineNextState(ctx)
  86. assert.equal(s.序, 6, `定稿后应仍序6,实际:${JSON.stringify(s)}`)
  87. assert.equal(s.dto.nextChapter, 2, '定稿第1章后 next 应推进到第2章,不应重抄第1章')
  88. } finally {
  89. await cache.close()
  90. await fs.rm(root, { recursive: true, force: true })
  91. await fs.rm(dbDir, { recursive: true, force: true })
  92. }
  93. })