orchestration.test.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { promises as fs } from 'node:fs'
  4. import path from 'node:path'
  5. import { assembleReviewInput, mergeReviews, runReviews } from '../../src/review/index.js'
  6. import { makeGitBook, chapter } from '../state-machine/_helper.js'
  7. const charCard = (name, 境界) => `---\n姓名: ${name}\n状态: 在世\n位置: 青云宗\n境界: ${境界}\n---\n## 设定\n。`
  8. async function makeReviewBook() {
  9. return makeGitBook({
  10. 'book.yaml': 'spec_version: "7.0"\n书名: 测\n卷规模: 40\n',
  11. '定稿/正文/0001-起.md': chapter(1, '过去的事。'),
  12. '定稿/设定/角色/林晚.md': charCard('林晚', '练气三层'),
  13. '定稿/设定/时间线/第01卷.md': '| 章 | 一句话事件 |\n| --- | --- |\n| 1 | 林晚得玉佩 |\n',
  14. '工作区/细纲.md': '## 本章要写到的事\n林晚突破练气四层。\n',
  15. '工作区/草稿.md': '林晚运转功法,突破到练气四层。她握紧青霜剑。',
  16. })
  17. }
  18. const fcIssue = (over = {}) => ({
  19. severity: 'high', category: 'setting', location: '第1段',
  20. description: '境界矛盾', evidence: '正文 vs 角色卡', fix_hint: '改回', blocking: false, ...over,
  21. })
  22. const edIssue = (over = {}) => ({
  23. severity: 'low', category: 'pacing', location: '全章',
  24. description: '节奏平', evidence: '无爽点', fix_hint: '加钩子', blocking: false, ...over,
  25. })
  26. test('assembleReviewInput:DTO 含草稿+要写到的事+相关角色,不泄漏路径', async () => {
  27. const { ctx, cleanup } = await makeReviewBook()
  28. try {
  29. const r = await assembleReviewInput(ctx, { chapterNum: 2, draftPath: '工作区/草稿.md' })
  30. assert.equal(r.ok, true)
  31. assert.match(r.input.草稿全文, /突破到练气四层/)
  32. assert.match(r.input.本章要写到的事, /练气四层/)
  33. assert.ok(r.input.相关角色.some((c) => c.正名 === '林晚'))
  34. const json = JSON.stringify(r.input)
  35. assert.ok(!json.includes(ctx.repoPath), '不泄漏仓库绝对路径')
  36. assert.ok(!json.includes('定稿/设定'), '不泄漏内部目录路径')
  37. } finally { await cleanup() }
  38. })
  39. test('mergeReviews:降级模式含兼容声明', () => {
  40. const m = mergeReviews(
  41. { factCheck: { issues: [] }, editorial: { issues: [] } },
  42. { mode: 'degraded', chapterNum: 2 }
  43. )
  44. assert.match(m.模式声明, /兼容模式/)
  45. assert.match(m.模式声明, /隔离度/)
  46. })
  47. test('mergeReviews:完整模式 + 合并计数', () => {
  48. const m = mergeReviews(
  49. {
  50. factCheck: { issues: [fcIssue({ severity: 'critical', blocking: true })] },
  51. editorial: { issues: [edIssue()] },
  52. },
  53. { mode: 'complete', chapterNum: 2 }
  54. )
  55. assert.equal(m.issues_count, 2)
  56. assert.equal(m.blocking_count, 1)
  57. assert.equal(m.has_blocking, true)
  58. assert.match(m.模式声明, /完整/)
  59. })
  60. test('runReviews:DI 注入两审 → 校验+合并+落盘审稿单与评审报告', async () => {
  61. const { ctx, cleanup, root } = await makeReviewBook()
  62. try {
  63. const reviewers = {
  64. factCheck: async (input) => ({ chapter: input.章号, issues: [fcIssue()] }),
  65. editorial: async (input) => ({ chapter: input.章号, issues: [edIssue()] }),
  66. }
  67. const r = await runReviews(ctx, { chapterNum: 2, draftPath: '工作区/草稿.md', mode: 'complete', reviewers })
  68. assert.equal(r.ok, true)
  69. const 审稿 = await fs.readFile(path.join(root, '工作区', '审稿.md'), 'utf8')
  70. assert.match(审稿, /突破到练气四层/, '审稿单含草稿')
  71. assert.match(审稿, /setting/)
  72. const factJson = await fs.readFile(path.join(root, '工作区', '评审报告', '事实审查.json'), 'utf8')
  73. assert.match(factJson, /setting/)
  74. } finally { await cleanup() }
  75. })
  76. test('runReviews:降级模式 → 审稿单含兼容声明', async () => {
  77. const { ctx, cleanup, root } = await makeReviewBook()
  78. try {
  79. const reviewers = { factCheck: async () => ({ issues: [] }), editorial: async () => ({ issues: [] }) }
  80. const r = await runReviews(ctx, { chapterNum: 2, draftPath: '工作区/草稿.md', mode: 'degraded', reviewers })
  81. assert.equal(r.ok, true)
  82. const 审稿 = await fs.readFile(path.join(root, '工作区', '审稿.md'), 'utf8')
  83. assert.match(审稿, /兼容模式/)
  84. } finally { await cleanup() }
  85. })
  86. test('runReviews:审稿单越界 category → ok=false 带错', async () => {
  87. const { ctx, cleanup } = await makeReviewBook()
  88. try {
  89. const reviewers = {
  90. factCheck: async () => ({ issues: [fcIssue({ category: 'pacing' })] }), // pacing 不属事实审查
  91. editorial: async () => ({ issues: [] }),
  92. }
  93. const r = await runReviews(ctx, { chapterNum: 2, draftPath: '工作区/草稿.md', mode: 'complete', reviewers })
  94. assert.equal(r.ok, false)
  95. assert.ok(r.errors.length > 0)
  96. } finally { await cleanup() }
  97. })