memory-catalog.spec.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import { afterAll, 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 { removeSync } from '../src/repo/remove'
  6. import {
  7. readAuthorMemoryCatalog,
  8. readBookMemoryCatalog,
  9. renderMemoryCatalog,
  10. writeAuthorMemory,
  11. writeBookMemory,
  12. } from '../src/index'
  13. const roots: string[] = []
  14. function mkDir(prefix: string): string {
  15. const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix))
  16. roots.push(dir)
  17. return dir
  18. }
  19. afterAll(() => { for (const root of roots) removeSync(root) })
  20. describe('记忆目录快照(15A)', () => {
  21. it('目录被文件占用是读取错误,不报告没有记忆', () => {
  22. const book = mkDir('webnovel-catalog-file-')
  23. fs.writeFileSync(path.join(book, '本书记忆'), '不是目录')
  24. expect(readBookMemoryCatalog(book)).toMatchObject({ 状态: '读取错误', 条目: [] })
  25. })
  26. it('目录junction、条目硬链接及索引链接不跨归属读取', () => {
  27. const outside = mkDir('webnovel-catalog-outside-')
  28. writeBookMemory(outside, { 类: '决策', 名称: '外部条目', 正文: '外部正文', 来源: '对谈', 裁决记录: '批准' })
  29. const book = mkDir('webnovel-catalog-link-')
  30. const link = path.join(book, '本书记忆')
  31. fs.symlinkSync(path.join(outside, '本书记忆'), link, process.platform === 'win32' ? 'junction' : 'dir')
  32. expect(readBookMemoryCatalog(book)).toMatchObject({ 状态: '读取错误', 条目: [] })
  33. fs.unlinkSync(link)
  34. fs.mkdirSync(link)
  35. fs.linkSync(path.join(outside, '本书记忆', '外部条目.md'), path.join(link, '外部条目.md'))
  36. expect(readBookMemoryCatalog(book)).toMatchObject({ 状态: '读取错误', 条目: [] })
  37. fs.unlinkSync(path.join(link, '外部条目.md'))
  38. fs.linkSync(path.join(outside, '本书记忆', '索引.md'), path.join(link, '索引.md'))
  39. expect(readBookMemoryCatalog(book).问题.join(';')).toContain('读取索引失败')
  40. const workspace = mkDir('webnovel-catalog-parent-link-')
  41. fs.symlinkSync(outside, path.join(workspace, '书房'), process.platform === 'win32' ? 'junction' : 'dir')
  42. expect(readAuthorMemoryCatalog(workspace)).toMatchObject({ 状态: '读取错误', 条目: [] })
  43. })
  44. it('旧格式文件出现在索引里时不误报为不存在', () => {
  45. const book = mkDir('webnovel-catalog-legacy-')
  46. fs.mkdirSync(path.join(book, '本书记忆'))
  47. fs.writeFileSync(path.join(book, '本书记忆/文风.md'), '# 旧文风\n旧正文')
  48. fs.writeFileSync(path.join(book, '本书记忆/索引.md'), '- [文风](文风.md)\n')
  49. expect(readBookMemoryCatalog(book).状态).toBe('正常')
  50. })
  51. it('没有记忆:状态无记忆,索引位置仍给出', () => {
  52. const ws = mkDir('webnovel-catalog-')
  53. const c = readAuthorMemoryCatalog(ws)
  54. expect(c.状态).toBe('无记忆')
  55. expect(c.条目).toHaveLength(0)
  56. expect(c.索引位置).toBe('书房/作者记忆/索引.md')
  57. const text = renderMemoryCatalog(c, '会话开始时')
  58. expect(text).toContain('【作者记忆目录】会话开始时')
  59. expect(text).toContain(path.join(ws, '书房', '作者记忆', '索引.md'))
  60. expect(text).toContain('目前没有作者记忆')
  61. })
  62. it('作者层:条目名称、描述、类、来源、文件位置进目录,正文不进', () => {
  63. const ws = mkDir('webnovel-catalog-')
  64. expect(writeAuthorMemory(ws, { 名称: '短句节奏', 描述: '作者偏好短句收束', 类: '文风', 标签: ['星辰'], 来源: '对谈', 正文: '秘密正文不可外泄' }).ok).toBe(true)
  65. const c = readAuthorMemoryCatalog(ws)
  66. expect(c.状态).toBe('正常')
  67. expect(c.条目).toEqual([{ 名称: '短句节奏', 描述: '作者偏好短句收束', 类: '文风', 来源: '对谈', 全文位置: '书房/作者记忆/短句节奏.md' }])
  68. const text = renderMemoryCatalog(c, '会话开始时')
  69. expect(text).toContain('- 短句节奏 — 作者偏好短句收束|类:文风|来源:对谈|文件:书房/作者记忆/短句节奏.md')
  70. expect(text).not.toContain('秘密正文')
  71. })
  72. it('本书层:缺描述如实标注,旧格式文件整文件定位,正文不进', () => {
  73. const book = mkDir('webnovel-catalog-book-')
  74. expect(writeBookMemory(book, { 类: '决策', 名称: '主角不下山', 正文: '第三章前不离开山门', 来源: '定稿/卷01/0001-开篇.md', 裁决记录: '作者批准' }).ok).toBe(true)
  75. fs.writeFileSync(path.join(book, '本书记忆', '文风.md'), '## 条目\n- 名称: 旧条\n', 'utf-8')
  76. const c = readBookMemoryCatalog(book)
  77. expect(c.归属).toBe('本书')
  78. expect(c.状态).toBe('正常')
  79. expect(c.条目).toEqual([
  80. { 名称: '主角不下山', 类: '决策', 来源: '定稿/卷01/0001-开篇.md', 全文位置: '本书记忆/主角不下山.md' },
  81. { 名称: '文风', 类: '文风', 全文位置: '本书记忆/文风.md', 格式: '旧' },
  82. ])
  83. const text = renderMemoryCatalog(c, '选书时')
  84. expect(text).toContain('- 主角不下山 — (缺描述)|类:决策')
  85. expect(text).toContain('- 文风(旧格式文件,一类一文件)|文件:本书记忆/文风.md')
  86. expect(text).not.toContain('第三章前不离开山门')
  87. })
  88. it('索引失效:新增/删除/改名后未重建索引,状态标失效并列出差异,旧条目仍可读', () => {
  89. const ws = mkDir('webnovel-catalog-')
  90. writeAuthorMemory(ws, { 名称: '甲', 描述: '甲描述', 类: '决策', 标签: ['x'], 来源: '对谈', 正文: 'a' })
  91. const dir = path.join(ws, '书房', '作者记忆')
  92. fs.renameSync(path.join(dir, '甲.md'), path.join(dir, '乙.md'))
  93. const c = readAuthorMemoryCatalog(ws)
  94. expect(c.状态).toBe('索引失效')
  95. expect(c.问题.join(';')).toContain('索引未收录:乙.md')
  96. expect(c.问题.join(';')).toContain('索引指向不存在的文件:甲.md')
  97. expect(c.条目.map((e) => e.全文位置)).toEqual(['书房/作者记忆/乙.md'])
  98. expect(renderMemoryCatalog(c, '会话开始时')).toContain('索引失效:')
  99. fs.unlinkSync(path.join(dir, '索引.md'))
  100. const noIndex = readAuthorMemoryCatalog(ws)
  101. expect(noIndex.状态).toBe('索引失效')
  102. expect(noIndex.问题.join(';')).toContain('索引文件不存在')
  103. })
  104. it('读取错误:条目文件坏了明确报出,其余条目照列,不伪造描述', () => {
  105. const ws = mkDir('webnovel-catalog-')
  106. writeAuthorMemory(ws, { 名称: '好条', 描述: '好描述', 类: '决策', 标签: ['x'], 来源: '对谈', 正文: 'a' })
  107. const dir = path.join(ws, '书房', '作者记忆')
  108. fs.writeFileSync(path.join(dir, '坏条.md'), '---\n名称: [\n---\n正文', 'utf-8')
  109. const c = readAuthorMemoryCatalog(ws)
  110. expect(c.状态).toBe('读取错误')
  111. expect(c.问题.join(';')).toContain('解析失败:书房/作者记忆/坏条.md')
  112. expect(c.条目.map((e) => e.名称)).toEqual(['好条'])
  113. const text = renderMemoryCatalog(c, '会话开始时')
  114. expect(text).toContain('读取错误:')
  115. expect(text).toContain('- 好条 — 好描述')
  116. })
  117. })