1
0

session.test.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  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. readBooksRegistry,
  8. scanRebuildBooks,
  9. assembleSessionContext,
  10. writeBooksRegistry,
  11. } from '../../src/session/index.js'
  12. async function tmpWorkdir() {
  13. const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-wd-'))
  14. return { root, cleanup: () => fs.rm(root, { recursive: true, force: true }) }
  15. }
  16. async function writeRegistry(root, lines) {
  17. await fs.mkdir(path.join(root, '.webnovel'), { recursive: true })
  18. await fs.writeFile(path.join(root, '.webnovel', 'books.jsonl'), lines.join('\n') + '\n', 'utf8')
  19. }
  20. async function makeBookDir(root, name) {
  21. await fs.mkdir(path.join(root, name), { recursive: true })
  22. await fs.writeFile(path.join(root, name, 'book.yaml'), `spec_version: "7.0"\n书名: ${name}\n`, 'utf8')
  23. }
  24. test('readBooksRegistry:解析合法行,损坏行跳过并计数', async () => {
  25. const { root, cleanup } = await tmpWorkdir()
  26. try {
  27. await writeRegistry(root, [
  28. JSON.stringify({ 书名: '剑起青云', 目录: '剑起青云', 当前: true }),
  29. '{坏的 json',
  30. JSON.stringify({ 书名: '星海', 目录: '星海', 当前: false }),
  31. ])
  32. const r = await readBooksRegistry(root)
  33. assert.equal(r.ok, true)
  34. assert.equal(r.books.length, 2)
  35. assert.equal(r.corrupt, 1)
  36. } finally { await cleanup() }
  37. })
  38. test('readBooksRegistry:缺文件 → missing=true 不抛', async () => {
  39. const { root, cleanup } = await tmpWorkdir()
  40. try {
  41. const r = await readBooksRegistry(root)
  42. assert.equal(r.missing, true)
  43. assert.equal(r.books.length, 0)
  44. } finally { await cleanup() }
  45. })
  46. test('scanRebuildBooks:扫含 book.yaml 子目录重建,需作者选当前书', async () => {
  47. const { root, cleanup } = await tmpWorkdir()
  48. try {
  49. await makeBookDir(root, '剑起青云')
  50. await makeBookDir(root, '星海')
  51. const r = await scanRebuildBooks(root)
  52. assert.equal(r.ok, true)
  53. assert.equal(r.books.length, 2)
  54. assert.ok(r.books.some((b) => b.书名 === '剑起青云'))
  55. assert.equal(r.needsAuthorPick, true)
  56. } finally { await cleanup() }
  57. })
  58. test('assembleSessionContext:有登记 → 注入含当前书与本数', async () => {
  59. const { root, cleanup } = await tmpWorkdir()
  60. try {
  61. await writeRegistry(root, [
  62. JSON.stringify({ 书名: '剑起青云', 目录: '剑起青云', 当前: true }),
  63. JSON.stringify({ 书名: '星海', 目录: '星海', 当前: false }),
  64. ])
  65. const r = await assembleSessionContext(root)
  66. assert.equal(r.ok, true)
  67. assert.match(r.text, /剑起青云/)
  68. assert.match(r.text, /2 本/)
  69. assert.equal(r.current.书名, '剑起青云')
  70. } finally { await cleanup() }
  71. })
  72. test('assembleSessionContext:登记缺失 → 扫描重建并标记', async () => {
  73. const { root, cleanup } = await tmpWorkdir()
  74. try {
  75. await makeBookDir(root, '剑起青云')
  76. const r = await assembleSessionContext(root)
  77. assert.equal(r.ok, true)
  78. assert.equal(r.rebuilt, true)
  79. assert.match(r.text, /剑起青云/)
  80. } finally { await cleanup() }
  81. })
  82. test('assembleSessionContext(P1-4):部分损坏 → 丢坏行回写,下读 corrupt=0', async () => {
  83. const { root, cleanup } = await tmpWorkdir()
  84. try {
  85. await writeRegistry(root, [
  86. JSON.stringify({ 书名: '剑起青云', 目录: '剑起青云', 当前: true }),
  87. '{坏的 json',
  88. JSON.stringify({ 书名: '星海', 目录: '星海', 当前: false }),
  89. ])
  90. const r = await assembleSessionContext(root)
  91. assert.equal(r.ok, true)
  92. assert.equal(r.current.书名, '剑起青云')
  93. // 回写后坏行已丢
  94. const reread = await readBooksRegistry(root)
  95. assert.equal(reread.corrupt, 0, '自愈回写应丢掉坏行')
  96. assert.equal(reread.books.length, 2)
  97. } finally { await cleanup() }
  98. })
  99. test('assembleSessionContext(P1-4):登记缺失重建后回写,下读不再 missing', async () => {
  100. const { root, cleanup } = await tmpWorkdir()
  101. try {
  102. await makeBookDir(root, '剑起青云')
  103. await makeBookDir(root, '星海')
  104. const r = await assembleSessionContext(root)
  105. assert.equal(r.rebuilt, true)
  106. assert.equal(r.books.length, 2)
  107. // 回写后下个会话直接读登记,不必再扫
  108. const reread = await readBooksRegistry(root)
  109. assert.equal(reread.missing, false)
  110. assert.equal(reread.books.length, 2)
  111. } finally { await cleanup() }
  112. })
  113. test('writeBooksRegistry:写 JSONL 逐行,可被 readBooksRegistry 读回', async () => {
  114. const { root, cleanup } = await tmpWorkdir()
  115. try {
  116. await writeBooksRegistry(root, [
  117. { 书名: 'A', 目录: 'A', 当前: true },
  118. { 书名: 'B', 目录: 'B', 当前: false },
  119. ])
  120. const r = await readBooksRegistry(root)
  121. assert.equal(r.books.length, 2)
  122. assert.equal(r.corrupt, 0)
  123. } finally { await cleanup() }
  124. })
  125. test('无 hook 等价:hook 入口与状态机入口调同一函数 → 注入文本逐字一致', async () => {
  126. const { root, cleanup } = await tmpWorkdir()
  127. try {
  128. await writeRegistry(root, [JSON.stringify({ 书名: '剑起青云', 目录: '剑起青云', 当前: true })])
  129. const hookText = (await assembleSessionContext(root)).text // Claude Code SessionStart hook
  130. const smText = (await assembleSessionContext(root)).text // 无 hook 宿主由状态机入口调
  131. assert.equal(hookText, smText)
  132. } finally { await cleanup() }
  133. })