1
0

units.test.js 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  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 { fileURLToPath } from 'node:url'
  7. import { detectHosts, parseHostsOverride } from '../../src/installer/detect.js'
  8. import { classifyFile, sha256, readManifest, writeManifest } from '../../src/installer/manifest.js'
  9. import { mergeClaudeSettings, SESSION_HOOK_COMMAND } from '../../src/installer/shells.js'
  10. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  11. const REGISTRY = JSON.parse(
  12. await fs.readFile(path.join(__dirname, '../../adapters/registry.json'), 'utf8')
  13. )
  14. async function tmpDir(prefix = 'wnw-inst-') {
  15. const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix))
  16. return { root, cleanup: () => fs.rm(root, { recursive: true, force: true }) }
  17. }
  18. test('detectHosts:PATH 上有探测名才命中(win32 认 PATHEXT)', async () => {
  19. const { root, cleanup } = await tmpDir('wnw-path-')
  20. try {
  21. // 造一个假 claude 可执行
  22. const ext = process.platform === 'win32' ? '.cmd' : ''
  23. await fs.writeFile(path.join(root, `claude${ext}`), '#!/bin/sh\n', 'utf8')
  24. const env = { PATH: root, PATHEXT: '.COM;.EXE;.BAT;.CMD' }
  25. const hits = await detectHosts(REGISTRY, { env })
  26. assert.deepEqual(hits, ['claude-code'])
  27. // 空 PATH → 全不命中
  28. assert.deepEqual(await detectHosts(REGISTRY, { env: { PATH: '' } }), [])
  29. } finally {
  30. await cleanup()
  31. }
  32. })
  33. test('parseHostsOverride:未知宿主人话报错并列可用项', () => {
  34. const bad = parseHostsOverride('claude-code,不存在', REGISTRY)
  35. assert.equal(bad.ok, false)
  36. assert.ok(bad.error.includes('不存在') && bad.error.includes('codex'))
  37. const good = parseHostsOverride('codex, cursor', REGISTRY)
  38. assert.deepEqual(good.hosts, ['codex', 'cursor'])
  39. })
  40. test('manifest:三态判定 + 读写往返', async () => {
  41. const { root, cleanup } = await tmpDir()
  42. try {
  43. const m = { version: '7.0.0-alpha', files: { 'a.md': sha256('旧内容') } }
  44. await writeManifest(root, m)
  45. const back = await readManifest(root)
  46. assert.equal(back.files['a.md'], sha256('旧内容'))
  47. assert.equal(classifyFile('新.md', null, back), 'new')
  48. assert.equal(classifyFile('a.md', null, back), 'missing')
  49. assert.equal(classifyFile('a.md', sha256('旧内容'), back), 'unchanged')
  50. assert.equal(classifyFile('a.md', sha256('用户改过'), back), 'user-modified')
  51. assert.equal(classifyFile('a.md', sha256('x'), null), 'new')
  52. } finally {
  53. await cleanup()
  54. }
  55. })
  56. test('mergeClaudeSettings:新建/保留用户配置/幂等/坏 JSON 不动', () => {
  57. // 新建
  58. const a = mergeClaudeSettings(null, SESSION_HOOK_COMMAND)
  59. assert.equal(a.changed, true)
  60. const parsedA = JSON.parse(a.content)
  61. assert.equal(parsedA.hooks.SessionStart[0].hooks[0].command, SESSION_HOOK_COMMAND)
  62. // 保留用户已有 hooks 与其他配置
  63. const user = JSON.stringify({
  64. permissions: { allow: ['Bash'] },
  65. hooks: { SessionStart: [{ hooks: [{ type: 'command', command: 'echo 自己的' }] }] },
  66. })
  67. const b = mergeClaudeSettings(user, SESSION_HOOK_COMMAND)
  68. assert.equal(b.changed, true)
  69. const parsedB = JSON.parse(b.content)
  70. assert.deepEqual(parsedB.permissions.allow, ['Bash'])
  71. assert.equal(parsedB.hooks.SessionStart.length, 2)
  72. // 幂等:再合并不重复
  73. const c = mergeClaudeSettings(b.content, SESSION_HOOK_COMMAND)
  74. assert.equal(c.changed, false)
  75. assert.equal(JSON.parse(c.content).hooks.SessionStart.length, 2)
  76. // 坏 JSON → 不动原文,报 error
  77. const d = mergeClaudeSettings('{坏的', SESSION_HOOK_COMMAND)
  78. assert.equal(d.changed, false)
  79. assert.ok(d.error)
  80. assert.equal(d.content, '{坏的')
  81. })