persist.test.js 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  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 {
  7. persistRepair,
  8. persistCreateBook,
  9. persistVolumeReview,
  10. persistDraftOutline,
  11. } from '../../src/state-machine/persist.js'
  12. async function tmpRepo() {
  13. const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-persist-'))
  14. return { ctx: { repoPath: root }, root, cleanup: () => fs.rm(root, { recursive: true, force: true }) }
  15. }
  16. const read = (root, rel) => fs.readFile(path.join(root, rel), 'utf8')
  17. test('persistDraftOutline(序6)→ 写 工作区/细纲.md', async () => {
  18. const { ctx, root, cleanup } = await tmpRepo()
  19. try {
  20. const r = await persistDraftOutline(ctx, { 细纲: '## 本章要写到的事\n林晚突破。\n' })
  21. assert.equal(r.ok, true)
  22. assert.match(await read(root, '工作区/细纲.md'), /林晚突破/)
  23. } finally { await cleanup() }
  24. })
  25. test('persistCreateBook(序1)→ 写 book.yaml + 总纲 + 第一卷卷纲', async () => {
  26. const { ctx, root, cleanup } = await tmpRepo()
  27. try {
  28. const r = await persistCreateBook(ctx, {
  29. book: { spec_version: '7.0', 书名: '剑起青云', 题材: '仙侠' },
  30. 总纲: '# 总纲\n主角逆袭。',
  31. 卷纲: '# 第1卷\n入门。',
  32. })
  33. assert.equal(r.ok, true)
  34. assert.match(await read(root, 'book.yaml'), /剑起青云/)
  35. assert.match(await read(root, '大纲/总纲.md'), /逆袭/)
  36. assert.match(await read(root, '大纲/第01卷.md'), /入门/)
  37. } finally { await cleanup() }
  38. })
  39. test('persistVolumeReview(序4)→ 写卷摘要 + 下卷卷纲', async () => {
  40. const { ctx, root, cleanup } = await tmpRepo()
  41. try {
  42. const r = await persistVolumeReview(ctx, { 卷号: 1, 卷摘要: '第一卷收束。', 下卷卷纲: '# 第2卷\n新地图。' })
  43. assert.equal(r.ok, true)
  44. assert.match(await read(root, '定稿/摘要/卷摘要/01.md'), /收束/)
  45. assert.match(await read(root, '大纲/第02卷.md'), /新地图/)
  46. } finally { await cleanup() }
  47. })
  48. test('persistRepair(序0)→ 仅写失败清单内的文件,内容须能解析', async () => {
  49. const { ctx, root, cleanup } = await tmpRepo()
  50. try {
  51. const target = '定稿/正文/0001-起.md'
  52. await fs.mkdir(path.join(root, '定稿/正文'), { recursive: true })
  53. await fs.writeFile(path.join(root, target), '---\n坏: yaml: :\n---\n正文', 'utf8')
  54. const good = '---\n章号: 1\n标题: 起\n---\n正文'
  55. const r = await persistRepair(ctx, { repairs: [{ file: target, content: good }] }, { allowedFiles: [target] })
  56. assert.equal(r.ok, true)
  57. assert.deepEqual(r.written, [target])
  58. assert.match(await read(root, target), /章号: 1/)
  59. } finally { await cleanup() }
  60. })
  61. test('persistRepair:拒绝写不在失败清单内的文件(安全网,防 AI 任意写)', async () => {
  62. const { ctx, root, cleanup } = await tmpRepo()
  63. try {
  64. const r = await persistRepair(
  65. ctx,
  66. { repairs: [{ file: '定稿/正文/9999-注入.md', content: '---\n章号: 9\n---\nx' }] },
  67. { allowedFiles: ['定稿/正文/0001-起.md'] }
  68. )
  69. assert.equal(r.ok, false)
  70. await assert.rejects(() => read(root, '定稿/正文/9999-注入.md'))
  71. } finally { await cleanup() }
  72. })
  73. test('persistRepair:修复内容仍解析失败 → ok=false 不写', async () => {
  74. const { ctx, root, cleanup } = await tmpRepo()
  75. try {
  76. const target = '定稿/正文/0001-起.md'
  77. const r = await persistRepair(
  78. ctx,
  79. { repairs: [{ file: target, content: '---\n仍坏: : :\n---\nx' }] },
  80. { allowedFiles: [target] }
  81. )
  82. assert.equal(r.ok, false)
  83. } finally { await cleanup() }
  84. })