1
0

volume-summary-cli.spec.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import { afterAll, describe, expect, it, vi } from 'vitest'
  2. import * as fs from 'node:fs'
  3. import * as path from 'node:path'
  4. import * as os from 'node:os'
  5. import { paths, seedMinDesign, serializeDocument, writeFileAtomic } from '@webnovel/core'
  6. import { volumeSummaryCli } from '../src/scripts/volume-summary-cli'
  7. const roots: string[] = []
  8. function mkBook(): string {
  9. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webnovel-volsum-cli-'))
  10. roots.push(dir)
  11. seedMinDesign(dir)
  12. return dir
  13. }
  14. afterAll(() => { for (const r of roots) { try { fs.rmSync(r, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }) } catch { /* Windows 句柄延迟释放 */ } } })
  15. function finalize(root: string, chapters: number, opts: { 缺摘要?: number[] } = {}): void {
  16. writeFileAtomic(root, paths.卷纲(1), '# 卷纲\n\n卷01|1 案/3 章\n\n## 叙事结构 〔已确认〕\n\nx\n\n## 卷末兑现 〔已确认〕\n\n- 结案。\n')
  17. for (const name of ['故事线', '人物弧线', '承诺', '时间线'] as const) {
  18. writeFileAtomic(root, paths.账本(name), `# ${name}\n`)
  19. }
  20. for (let n = 1; n <= chapters; n += 1) {
  21. const fin = path.join(root, `定稿/卷01/${String(n).padStart(4, '0')}-第${n}案.md`)
  22. fs.mkdirSync(path.dirname(fin), { recursive: true })
  23. fs.writeFileSync(fin, serializeDocument({ 状态: '已定稿' }, '正文'), 'utf-8')
  24. if (opts.缺摘要?.includes(n)) continue
  25. const sum = path.join(root, paths.章摘要(1, n, `第${n}案`))
  26. fs.mkdirSync(path.dirname(sum), { recursive: true })
  27. fs.writeFileSync(sum, `# 章摘要\n\n第${n}案摘要。`, 'utf-8')
  28. }
  29. }
  30. function captureRun(argv: string[]): { code: number; out: string } {
  31. const lines: string[] = []
  32. const logSpy = vi.spyOn(console, 'log').mockImplementation((msg?: unknown) => { lines.push(String(msg)) })
  33. const errSpy = vi.spyOn(console, 'error').mockImplementation((msg?: unknown) => { lines.push(String(msg)) })
  34. try {
  35. const code = volumeSummaryCli(argv)
  36. return { code, out: lines.join('\n') }
  37. } finally {
  38. logSpy.mockRestore()
  39. errSpy.mockRestore()
  40. }
  41. }
  42. describe('卷摘要候选薄入口退出码(F21-2)', () => {
  43. it('账本缺失/损坏 → exit 1(前置不满足);正常与如实报缺 → exit 0', () => {
  44. // 正常:账本在线索有卷内条目
  45. const okBook = mkBook()
  46. finalize(okBook, 3)
  47. writeFileAtomic(okBook, paths.账本('线索'), '# 线索\n\n## 铜灯钩\n状态:已收\n埋设点:第0001章\n\n### 正文\n划痕。\n')
  48. const okRun = captureRun(['--book', okBook, '--卷', '1'])
  49. expect(okRun.code).toBe(0)
  50. expect(JSON.parse(okRun.out).ok).toBe(true)
  51. // 如实报缺:缺第2章摘要(账本在线索空) → exit 0,ok:false
  52. const lackBook = mkBook()
  53. finalize(lackBook, 3, { 缺摘要: [2] })
  54. writeFileAtomic(lackBook, paths.账本('线索'), '# 线索\n')
  55. const lackRun = captureRun(['--book', lackBook, '--卷', '1'])
  56. expect(lackRun.code).toBe(0)
  57. expect(JSON.parse(lackRun.out).ok).toBe(false)
  58. // 账本缺失:不创建线索账本 → exit 1
  59. const missingBook = mkBook()
  60. finalize(missingBook, 3)
  61. const missingRun = captureRun(['--book', missingBook, '--卷', '1'])
  62. expect(missingRun.code).toBe(1)
  63. expect(JSON.parse(missingRun.out).ok).toBe(false)
  64. // 账本损坏:重复字段 → exit 1
  65. const badBook = mkBook()
  66. finalize(badBook, 3)
  67. writeFileAtomic(badBook, paths.账本('线索'), '# 线索\n\n## 坏\n状态:已埋\n状态:已示\n')
  68. const badRun = captureRun(['--book', badBook, '--卷', '1'])
  69. expect(badRun.code).toBe(1)
  70. expect(JSON.parse(badRun.out).ok).toBe(false)
  71. })
  72. })