material-supplement-checks.mjs 4.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. import assert from 'node:assert/strict'
  2. import fs from 'node:fs'
  3. import path from 'node:path'
  4. import { execFileSync } from 'node:child_process'
  5. import { pathToFileURL } from 'node:url'
  6. export async function checkMaterialSupplements({ root, workspace, main, execute, check, report, withTurn }) {
  7. const core = await import(pathToFileURL(path.join(root, 'native-write-core.mjs')).href)
  8. await check('补充原文预览保存、恢复审读与源变化失效', async () => {
  9. const book = path.join(workspace, '补料验收书')
  10. const key = { 卷: 1, 章: 3, 章名: '章3' }
  11. core.makeFixtureBook(book, 3)
  12. const put = (target, text) => { fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, text) }
  13. const contract = path.join(book, '作品契约/契约.md')
  14. fs.writeFileSync(contract, fs.readFileSync(contract, 'utf8').replace('---', '---\n书id: supplement-book'))
  15. put(path.join(book, '大纲/卷规划/卷01/卷纲.md'), '# 卷纲\n\n' + ['叙事结构', '弧线', '线索推进', '卷末兑现'].map(name => `## ${name}\n\n章3核对城门线索。\n`).join('\n'))
  16. put(path.join(book, '草稿区/草稿/卷01-章3/稿1.md'), core.serializeDocument({ 角色: '待审稿', 版本: 1 }, '主角在城门前看见旧铜符。'))
  17. const source = path.join(workspace, '书房/作者记忆/补料决定.md')
  18. put(source, '# 作者决定\n本章保留旧铜符来历,不揭示真名。【宿主补料标记】\n')
  19. const operation = { 操作: '设置', 编号: 'decision', 标题: '早期决定', 理由: '保持作者确定的信息边界', 性质: '建议', 来源: { 域: '书房', 路径: '作者记忆/补料决定.md' } }
  20. const call = async extra => {
  21. const result = await withTurn(main, () => execute(main, 'novel_assemble_materials', { bookId: 'supplement-book', ...key, ...extra }))
  22. assert.equal(result.isError, false, JSON.stringify(result))
  23. return result.value
  24. }
  25. const dir = path.join(book, '草稿区/材料包/卷01-章3')
  26. const preview = await call({ 模式: '预览', 补充操作: [operation] })
  27. assert.equal(preview.ok, true, JSON.stringify(preview))
  28. assert.equal(fs.existsSync(dir), false)
  29. assert.match(preview.补充预览[0].原文, /宿主补料标记/)
  30. const saveArgs = { 模式: '保存', 补充操作: preview.补充预览.map(item => item.操作), 材料清单哈希: preview.材料清单哈希 }
  31. const saved = await call(saveArgs)
  32. assert.equal(saved.ok, true, JSON.stringify(saved))
  33. assert.equal((await call(saveArgs)).ok, true)
  34. for (const material of [core.loadDraftContext(book, key).材料段, core.rebuildResume(book, key).injection.材料段, core.computeReview(book, key).材料段]) assert.match(JSON.stringify(material), /宿主补料标记/)
  35. // Exercise the packaged thin entry and its opt-in JSON output for the review handoff.
  36. fs.copyFileSync(path.join(root, 'lib/webnovel.mjs'), path.join(root, 'lib/index.js'))
  37. const cli = execFileSync(process.execPath, [path.join(root, 'skills/novel-review/scripts/确定性检查.mjs'), '--book', book, '--卷', '1', '--章', '3', '--章名', '章3', '--审读材料', 'true'], { cwd: workspace, encoding: 'utf8', windowsHide: true, timeout: 30000, maxBuffer: 2 * 1024 * 1024 })
  38. const review = JSON.parse(cli)
  39. assert.equal(review.ok, true)
  40. assert.match(JSON.stringify(review.材料段), /宿主补料标记/)
  41. assert.ok(review.回写载荷.length > 0)
  42. const oldSnapshot = fs.readFileSync(path.join(dir, '补充/decision.md'), 'utf8')
  43. fs.appendFileSync(source, '作者现在补充了一条意见。\n')
  44. const stale = await call({})
  45. assert.equal(stale.ok, false)
  46. assert.equal(stale.状态, '已过期')
  47. assert.equal(fs.readFileSync(path.join(dir, '补充/decision.md'), 'utf8'), oldSnapshot)
  48. assert.equal(core.computeReview(book, key).ok, false)
  49. const manifest = JSON.parse(fs.readFileSync(path.join(dir, '材料清单.json'), 'utf8'))
  50. report.materialSupplements = { previewReadOnly: true, saved: true, replayed: true, consumers: ['drafting', 'recovery', 'review', 'review-cli'], sourceDriftDetected: true, oldSnapshotPreserved: true,
  51. totalChars: saved.合计字数,
  52. baseChars: manifest.段.filter(item => item.类型 !== '补充').reduce((sum, item) => sum + item.字数, 0),
  53. supplementChars: manifest.段.filter(item => item.类型 === '补充').reduce((sum, item) => sum + item.字数, 0),
  54. sourcePreviewCount: 1, externalModelTokens: null,
  55. }
  56. })
  57. }