materials-preview.spec.ts 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. import { describe, expect, it } from 'vitest'
  2. import * as fs from 'node:fs'
  3. import * as os from 'node:os'
  4. import * as path from 'node:path'
  5. import { createNovelTools } from '../src/novel-tools'
  6. import { nativeWriteStub } from './fixtures/native-write-stub'
  7. /**
  8. * 真机缺陷回归(20章连写第1章前):novel_assemble_materials 模式=预览在备料前置
  9. * 未就绪时返回 ok:false,但 材料清单哈希/合计字数/补充预览 三个可选键缺席——宿主
  10. * 工具边界拒绝 undefined 值,整个结果被报成内部错误「value is not lossless JSON」。
  11. * 修复:预览与保存失败分支一律回填空值,返回值必须无损 JSON 往返。
  12. */
  13. function makeBook(): { ws: string; bookRoot: string } {
  14. const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'webnovel-preview-'))
  15. const bookRoot = path.join(ws, '探书')
  16. fs.mkdirSync(path.join(bookRoot, '作品契约'), { recursive: true })
  17. fs.writeFileSync(path.join(bookRoot, '作品契约', '契约.md'), '---\n书id: probe-preview\n---\n正文\n', 'utf-8')
  18. return { ws, bookRoot }
  19. }
  20. function lossless(value: unknown): boolean {
  21. try {
  22. return JSON.stringify(value) === JSON.stringify(JSON.parse(JSON.stringify(value)))
  23. } catch {
  24. return false
  25. }
  26. }
  27. describe('材料预览返回的无损 JSON 边界', () => {
  28. it('前置未就绪时预览返回 ok:false 且不含 undefined 值', async () => {
  29. const { ws, bookRoot } = makeBook()
  30. const tools = createNovelTools({ nativeWrite: nativeWriteStub, workspaceRoot: () => ws, bookRootOfBookId: () => bookRoot })
  31. const tool = tools.find((t) => t.name === 'novel_assemble_materials')!
  32. const preview = await tool.execute({ bookId: 'probe-preview', 卷: 1, 章: 1, 章名: '探章', 模式: '预览' }) as Record<string, unknown>
  33. expect(preview.ok).toBe(false)
  34. expect(String(preview.gaps)).toContain('细纲')
  35. expect(preview.材料清单哈希).toBe('')
  36. expect(preview.合计字数).toBe(0)
  37. expect(preview.补充预览).toEqual([])
  38. expect(lossless(preview), JSON.stringify(preview)).toBe(true)
  39. for (const value of Object.values(preview)) expect(value).not.toBeUndefined()
  40. const saved = await tool.execute({ bookId: 'probe-preview', 卷: 1, 章: 1, 章名: '探章' }) as Record<string, unknown>
  41. expect(saved.ok).toBe(false)
  42. expect(saved.材料清单哈希).toBe('')
  43. expect(lossless(saved), JSON.stringify(saved)).toBe(true)
  44. fs.rmSync(ws, { recursive: true, force: true, maxRetries: 5 })
  45. })
  46. })