git-health.test.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import path from 'node:path'
  4. import os from 'node:os'
  5. import { promises as fs } from 'node:fs'
  6. import { execFile } from 'node:child_process'
  7. import { promisify } from 'node:util'
  8. import { checkGitHealth } from '../../src/state-machine/git-health.js'
  9. const execFileAsync = promisify(execFile)
  10. // 造一个健康的 git 书仓库
  11. async function makeGitRepo() {
  12. const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-gh-'))
  13. const git = (args) => execFileAsync('git', args, { cwd: root })
  14. await git(['init', '-q'])
  15. await git(['config', 'user.email', 't@example.com'])
  16. await git(['config', 'user.name', 'test'])
  17. await fs.mkdir(path.join(root, '定稿', '正文'), { recursive: true })
  18. await fs.writeFile(path.join(root, '定稿', '正文', '0001-开局.md'), '---\n章号: 1\n---\n正文', 'utf8')
  19. await fs.writeFile(path.join(root, 'book.yaml'), '书名: 测\n', 'utf8')
  20. await fs.writeFile(path.join(root, '.gitignore'), '.cache/\n工作区/\n', 'utf8')
  21. await git(['add', '-A'])
  22. await git(['commit', '-q', '-m', 'init'])
  23. return { root, git }
  24. }
  25. test('git健康:陈旧锁文件自动删 + 救援记录', async () => {
  26. const { root } = await makeGitRepo()
  27. try {
  28. const lock = path.join(root, '.git', 'index.lock')
  29. await fs.writeFile(lock, '', 'utf8')
  30. const old = new Date(Date.now() - 3600_000)
  31. await fs.utimes(lock, old, old)
  32. const r = await checkGitHealth({ repoPath: root })
  33. assert.equal(r.ok, true)
  34. assert.ok(r.fixed.some((m) => m.includes('锁文件')))
  35. await assert.rejects(() => fs.access(lock))
  36. } finally {
  37. await fs.rm(root, { recursive: true, force: true })
  38. }
  39. })
  40. test('git健康:网盘冲突副本归档不删', async () => {
  41. const { root } = await makeGitRepo()
  42. try {
  43. const dupe = path.join(root, '定稿', '正文', '0001-开局 (1).md')
  44. await fs.writeFile(dupe, '副本内容', 'utf8')
  45. const r = await checkGitHealth({ repoPath: root })
  46. assert.ok(r.fixed.some((m) => m.includes('网盘')))
  47. await assert.rejects(() => fs.access(dupe))
  48. const archived = path.join(root, '工作区', '.救援', '网盘副本', '0001-开局 (1).md')
  49. assert.equal(await fs.readFile(archived, 'utf8'), '副本内容')
  50. } finally {
  51. await fs.rm(root, { recursive: true, force: true })
  52. }
  53. })
  54. test('git健康:半提交(暂存残留)自动 stash 可恢复', async () => {
  55. const { root, git } = await makeGitRepo()
  56. try {
  57. await fs.writeFile(path.join(root, '定稿', '正文', '0002-初遇.md'), '---\n章号: 2\n---\n新章', 'utf8')
  58. await git(['add', '-A'])
  59. const r = await checkGitHealth({ repoPath: root })
  60. assert.ok(r.fixed.some((m) => m.includes('暂存') || m.includes('stash')))
  61. const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
  62. cwd: root,
  63. encoding: 'utf8',
  64. })
  65. assert.equal(stdout.trim(), '')
  66. const { stdout: stashList } = await execFileAsync('git', ['stash', 'list'], {
  67. cwd: root,
  68. encoding: 'utf8',
  69. })
  70. assert.ok(stashList.includes('救援'))
  71. } finally {
  72. await fs.rm(root, { recursive: true, force: true })
  73. }
  74. })
  75. test('git健康:合并冲突先备份再中止', async () => {
  76. const { root, git } = await makeGitRepo()
  77. try {
  78. await git(['checkout', '-q', '-b', 'other'])
  79. await fs.writeFile(path.join(root, 'book.yaml'), '书名: 分支A\n', 'utf8')
  80. await git(['commit', '-q', '-am', 'A'])
  81. await git(['checkout', '-q', '-'])
  82. await fs.writeFile(path.join(root, 'book.yaml'), '书名: 分支B\n', 'utf8')
  83. await git(['commit', '-q', '-am', 'B'])
  84. try {
  85. await git(['merge', 'other'])
  86. } catch {
  87. // 预期冲突
  88. }
  89. const r = await checkGitHealth({ repoPath: root })
  90. assert.ok(r.fixed.some((m) => m.includes('合并')))
  91. await assert.rejects(() => fs.access(path.join(root, '.git', 'MERGE_HEAD')))
  92. } finally {
  93. await fs.rm(root, { recursive: true, force: true })
  94. }
  95. })
  96. test('git健康:.git 损坏只指引不乱动(零英文堆栈)', async () => {
  97. const { root } = await makeGitRepo()
  98. try {
  99. await fs.writeFile(path.join(root, '.git', 'HEAD'), '损坏内容不是 ref', 'utf8')
  100. const r = await checkGitHealth({ repoPath: root })
  101. assert.equal(r.ok, true)
  102. assert.ok(r.guidance.some((m) => m.includes('损坏') || m.includes('备份')))
  103. assert.ok(r.guidance.every((m) => !/Error|fatal|\bstack\b/i.test(m)))
  104. } finally {
  105. await fs.rm(root, { recursive: true, force: true })
  106. }
  107. })