@yivan-lab/pretty-please 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +250 -620
  2. package/bin/pls.tsx +178 -40
  3. package/dist/bin/pls.js +149 -27
  4. package/dist/package.json +10 -2
  5. package/dist/src/__integration__/command-generation.test.d.ts +5 -0
  6. package/dist/src/__integration__/command-generation.test.js +508 -0
  7. package/dist/src/__integration__/error-recovery.test.d.ts +5 -0
  8. package/dist/src/__integration__/error-recovery.test.js +511 -0
  9. package/dist/src/__integration__/shell-hook-workflow.test.d.ts +5 -0
  10. package/dist/src/__integration__/shell-hook-workflow.test.js +375 -0
  11. package/dist/src/__tests__/alias.test.d.ts +5 -0
  12. package/dist/src/__tests__/alias.test.js +421 -0
  13. package/dist/src/__tests__/chat-history.test.d.ts +5 -0
  14. package/dist/src/__tests__/chat-history.test.js +372 -0
  15. package/dist/src/__tests__/config.test.d.ts +5 -0
  16. package/dist/src/__tests__/config.test.js +822 -0
  17. package/dist/src/__tests__/history.test.d.ts +5 -0
  18. package/dist/src/__tests__/history.test.js +439 -0
  19. package/dist/src/__tests__/remote-history.test.d.ts +5 -0
  20. package/dist/src/__tests__/remote-history.test.js +641 -0
  21. package/dist/src/__tests__/remote.test.d.ts +5 -0
  22. package/dist/src/__tests__/remote.test.js +689 -0
  23. package/dist/src/__tests__/shell-hook-install.test.d.ts +5 -0
  24. package/dist/src/__tests__/shell-hook-install.test.js +413 -0
  25. package/dist/src/__tests__/shell-hook-remote.test.d.ts +5 -0
  26. package/dist/src/__tests__/shell-hook-remote.test.js +507 -0
  27. package/dist/src/__tests__/shell-hook.test.d.ts +5 -0
  28. package/dist/src/__tests__/shell-hook.test.js +440 -0
  29. package/dist/src/__tests__/sysinfo.test.d.ts +5 -0
  30. package/dist/src/__tests__/sysinfo.test.js +572 -0
  31. package/dist/src/__tests__/system-history.test.d.ts +5 -0
  32. package/dist/src/__tests__/system-history.test.js +457 -0
  33. package/dist/src/components/Chat.js +9 -28
  34. package/dist/src/config.d.ts +2 -0
  35. package/dist/src/config.js +30 -2
  36. package/dist/src/mastra-chat.js +10 -6
  37. package/dist/src/multi-step.js +10 -8
  38. package/dist/src/project-context.d.ts +22 -0
  39. package/dist/src/project-context.js +168 -0
  40. package/dist/src/prompts.d.ts +4 -4
  41. package/dist/src/prompts.js +23 -6
  42. package/dist/src/shell-hook.d.ts +32 -0
  43. package/dist/src/shell-hook.js +226 -33
  44. package/dist/src/sysinfo.d.ts +38 -9
  45. package/dist/src/sysinfo.js +245 -21
  46. package/dist/src/system-history.d.ts +18 -0
  47. package/dist/src/system-history.js +151 -0
  48. package/dist/src/ui/__tests__/theme.test.d.ts +5 -0
  49. package/dist/src/ui/__tests__/theme.test.js +688 -0
  50. package/dist/src/upgrade.js +3 -0
  51. package/dist/src/user-preferences.d.ts +44 -0
  52. package/dist/src/user-preferences.js +147 -0
  53. package/dist/src/utils/__tests__/platform-capabilities.test.d.ts +5 -0
  54. package/dist/src/utils/__tests__/platform-capabilities.test.js +214 -0
  55. package/dist/src/utils/__tests__/platform-exec.test.d.ts +5 -0
  56. package/dist/src/utils/__tests__/platform-exec.test.js +212 -0
  57. package/dist/src/utils/__tests__/platform-shell.test.d.ts +5 -0
  58. package/dist/src/utils/__tests__/platform-shell.test.js +300 -0
  59. package/dist/src/utils/__tests__/platform.test.d.ts +5 -0
  60. package/dist/src/utils/__tests__/platform.test.js +137 -0
  61. package/dist/src/utils/platform.d.ts +88 -0
  62. package/dist/src/utils/platform.js +331 -0
  63. package/package.json +10 -2
  64. package/src/__integration__/command-generation.test.ts +602 -0
  65. package/src/__integration__/error-recovery.test.ts +620 -0
  66. package/src/__integration__/shell-hook-workflow.test.ts +457 -0
  67. package/src/__tests__/alias.test.ts +545 -0
  68. package/src/__tests__/chat-history.test.ts +462 -0
  69. package/src/__tests__/config.test.ts +1043 -0
  70. package/src/__tests__/history.test.ts +538 -0
  71. package/src/__tests__/remote-history.test.ts +791 -0
  72. package/src/__tests__/remote.test.ts +866 -0
  73. package/src/__tests__/shell-hook-install.test.ts +510 -0
  74. package/src/__tests__/shell-hook-remote.test.ts +679 -0
  75. package/src/__tests__/shell-hook.test.ts +564 -0
  76. package/src/__tests__/sysinfo.test.ts +718 -0
  77. package/src/__tests__/system-history.test.ts +608 -0
  78. package/src/components/Chat.tsx +10 -37
  79. package/src/config.ts +29 -2
  80. package/src/mastra-chat.ts +12 -5
  81. package/src/multi-step.ts +11 -5
  82. package/src/project-context.ts +191 -0
  83. package/src/prompts.ts +26 -5
  84. package/src/shell-hook.ts +254 -32
  85. package/src/sysinfo.ts +326 -25
  86. package/src/system-history.ts +170 -0
  87. package/src/ui/__tests__/theme.test.ts +869 -0
  88. package/src/upgrade.ts +5 -0
  89. package/src/user-preferences.ts +178 -0
  90. package/src/utils/__tests__/platform-capabilities.test.ts +265 -0
  91. package/src/utils/__tests__/platform-exec.test.ts +278 -0
  92. package/src/utils/__tests__/platform-shell.test.ts +353 -0
  93. package/src/utils/__tests__/platform.test.ts +170 -0
  94. package/src/utils/platform.ts +431 -0
@@ -0,0 +1,510 @@
1
+ /**
2
+ * Shell Hook 安装/卸载测试
3
+ * 专注测试文件操作逻辑,Mock 掉平台检测
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
7
+ import path from 'path'
8
+
9
+ // Mock fs 模块
10
+ vi.mock('fs', () => ({
11
+ default: {
12
+ existsSync: vi.fn(),
13
+ readFileSync: vi.fn(),
14
+ writeFileSync: vi.fn(),
15
+ appendFileSync: vi.fn(),
16
+ copyFileSync: vi.fn(),
17
+ mkdirSync: vi.fn(),
18
+ unlinkSync: vi.fn(),
19
+ },
20
+ }))
21
+
22
+ // Mock os 模块
23
+ vi.mock('os', () => ({
24
+ default: {
25
+ homedir: vi.fn(() => '/home/testuser'),
26
+ },
27
+ }))
28
+
29
+ // Mock config 模块
30
+ const mockConfig = {
31
+ shellHook: false,
32
+ shellHistoryLimit: 10,
33
+ }
34
+
35
+ vi.mock('../config.js', () => ({
36
+ getConfig: vi.fn(() => mockConfig),
37
+ setConfigValue: vi.fn((key: string, value: any) => {
38
+ ;(mockConfig as any)[key] = value
39
+ return mockConfig
40
+ }),
41
+ CONFIG_DIR: '/home/testuser/.please',
42
+ }))
43
+
44
+ // Mock theme 模块
45
+ vi.mock('../ui/theme.js', () => ({
46
+ getCurrentTheme: vi.fn(() => ({
47
+ primary: '#007acc',
48
+ secondary: '#6c757d',
49
+ success: '#4caf50',
50
+ error: '#f44336',
51
+ warning: '#ff9800',
52
+ })),
53
+ }))
54
+
55
+ // Mock chalk
56
+ vi.mock('chalk', () => ({
57
+ default: {
58
+ hex: vi.fn(() => (s: string) => s),
59
+ gray: vi.fn((s: string) => s),
60
+ },
61
+ }))
62
+
63
+ // Mock platform 模块的 detectShell
64
+ vi.mock('../utils/platform.js', () => ({
65
+ detectShell: vi.fn(() => 'zsh'),
66
+ }))
67
+
68
+ import fs from 'fs'
69
+ import { getConfig, setConfigValue, CONFIG_DIR } from '../config.js'
70
+ import { detectShell as platformDetectShell } from '../utils/platform.js'
71
+
72
+ const mockFs = vi.mocked(fs)
73
+ const mockGetConfig = vi.mocked(getConfig)
74
+ const mockSetConfigValue = vi.mocked(setConfigValue)
75
+ const mockPlatformDetectShell = vi.mocked(platformDetectShell)
76
+
77
+ // Hook 标记
78
+ const HOOK_START_MARKER = '# >>> pretty-please shell hook >>>'
79
+ const HOOK_END_MARKER = '# <<< pretty-please shell hook <<<'
80
+
81
+ // 跨平台路径辅助函数
82
+ const HOME = '/home/testuser'
83
+ const ZSHRC_PATH = path.join(HOME, '.zshrc')
84
+ const ZSHRC_BACKUP_PATH = path.join(HOME, '.zshrc.pls-backup')
85
+ const CONFIG_PATH = path.join(HOME, '.please')
86
+
87
+ // 模拟的 shell 配置文件内容
88
+ const EMPTY_ZSHRC = '# My zshrc\nexport PATH=$PATH:/usr/local/bin\n'
89
+ const ZSHRC_WITH_HOOK = `# My zshrc
90
+ export PATH=$PATH:/usr/local/bin
91
+
92
+ ${HOOK_START_MARKER}
93
+ # Hook content here
94
+ __pls_preexec() { ... }
95
+ ${HOOK_END_MARKER}
96
+ `
97
+
98
+ // 模块重置辅助函数
99
+ async function resetShellHookModule() {
100
+ vi.resetModules()
101
+ // 重新设置 mock
102
+ vi.doMock('fs', () => ({
103
+ default: mockFs,
104
+ }))
105
+ return await import('../shell-hook.js')
106
+ }
107
+
108
+ beforeEach(() => {
109
+ vi.clearAllMocks()
110
+ // 重置 mockConfig
111
+ mockConfig.shellHook = false
112
+ mockConfig.shellHistoryLimit = 10
113
+ // 默认返回 zsh
114
+ mockPlatformDetectShell.mockReturnValue('zsh')
115
+ })
116
+
117
+ afterEach(() => {
118
+ vi.restoreAllMocks()
119
+ })
120
+
121
+ // ============================================================================
122
+ // installShellHook 测试
123
+ // ============================================================================
124
+
125
+ describe('installShellHook', () => {
126
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>
127
+
128
+ beforeEach(() => {
129
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
130
+ })
131
+
132
+ afterEach(() => {
133
+ consoleLogSpy.mockRestore()
134
+ })
135
+
136
+ it('首次安装应该成功', async () => {
137
+ // 配置文件存在但没有 hook
138
+ mockFs.existsSync.mockImplementation((p: any) => {
139
+ const pathStr = p.toString()
140
+ if (pathStr.includes('.zshrc') && !pathStr.includes('backup')) return true
141
+ if (pathStr.includes('.please')) return true
142
+ return false
143
+ })
144
+ mockFs.readFileSync.mockReturnValue(EMPTY_ZSHRC)
145
+
146
+ const { installShellHook } = await resetShellHookModule()
147
+ const result = await installShellHook()
148
+
149
+ expect(result).toBe(true)
150
+ // 应该备份原文件(检查调用了 copyFileSync,不检查具体路径格式)
151
+ expect(mockFs.copyFileSync).toHaveBeenCalled()
152
+ const copyCall = mockFs.copyFileSync.mock.calls[0]
153
+ expect(copyCall[0].toString()).toContain('.zshrc')
154
+ expect(copyCall[1].toString()).toContain('.zshrc.pls-backup')
155
+ // 应该追加 hook 脚本
156
+ expect(mockFs.appendFileSync).toHaveBeenCalled()
157
+ // 应该更新配置
158
+ expect(mockSetConfigValue).toHaveBeenCalledWith('shellHook', true)
159
+ })
160
+
161
+ it('已安装时应该跳过并返回 true', async () => {
162
+ mockFs.existsSync.mockReturnValue(true)
163
+ mockFs.readFileSync.mockReturnValue(ZSHRC_WITH_HOOK)
164
+
165
+ const { installShellHook } = await resetShellHookModule()
166
+ const result = await installShellHook()
167
+
168
+ expect(result).toBe(true)
169
+ // 不应该追加
170
+ expect(mockFs.appendFileSync).not.toHaveBeenCalled()
171
+ // 应该更新配置
172
+ expect(mockSetConfigValue).toHaveBeenCalledWith('shellHook', true)
173
+ // 应该显示警告
174
+ const allLogs = consoleLogSpy.mock.calls.map((c) => c[0]).join('\n')
175
+ expect(allLogs).toContain('已安装')
176
+ })
177
+
178
+ it('不支持的 shell 应该返回 false', async () => {
179
+ mockPlatformDetectShell.mockReturnValue('unknown')
180
+
181
+ const { installShellHook } = await resetShellHookModule()
182
+ const result = await installShellHook()
183
+
184
+ expect(result).toBe(false)
185
+ expect(mockFs.appendFileSync).not.toHaveBeenCalled()
186
+ })
187
+
188
+ it('配置目录不存在时应该创建', async () => {
189
+ mockFs.existsSync.mockImplementation((path: any) => {
190
+ if (path === '/home/testuser/.zshrc') return true
191
+ if (path === '/home/testuser/.please') return false
192
+ return false
193
+ })
194
+ mockFs.readFileSync.mockReturnValue(EMPTY_ZSHRC)
195
+
196
+ const { installShellHook } = await resetShellHookModule()
197
+ await installShellHook()
198
+
199
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith('/home/testuser/.please', {
200
+ recursive: true,
201
+ })
202
+ })
203
+
204
+ it('配置文件不存在时不应该备份', async () => {
205
+ mockFs.existsSync.mockImplementation((path: any) => {
206
+ if (path === '/home/testuser/.zshrc') return false
207
+ if (path === '/home/testuser/.please') return true
208
+ return false
209
+ })
210
+
211
+ const { installShellHook } = await resetShellHookModule()
212
+ await installShellHook()
213
+
214
+ expect(mockFs.copyFileSync).not.toHaveBeenCalled()
215
+ })
216
+ })
217
+
218
+ // ============================================================================
219
+ // uninstallShellHook 测试
220
+ // ============================================================================
221
+
222
+ describe('uninstallShellHook', () => {
223
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>
224
+
225
+ beforeEach(() => {
226
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
227
+ })
228
+
229
+ afterEach(() => {
230
+ consoleLogSpy.mockRestore()
231
+ })
232
+
233
+ it('已安装时应该成功卸载', async () => {
234
+ mockFs.existsSync.mockReturnValue(true)
235
+ mockFs.readFileSync.mockReturnValue(ZSHRC_WITH_HOOK)
236
+
237
+ let writtenContent = ''
238
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
239
+ writtenContent = content
240
+ })
241
+
242
+ const { uninstallShellHook } = await resetShellHookModule()
243
+ const result = uninstallShellHook()
244
+
245
+ expect(result).toBe(true)
246
+ // 应该移除 hook 内容
247
+ expect(writtenContent).not.toContain(HOOK_START_MARKER)
248
+ expect(writtenContent).not.toContain(HOOK_END_MARKER)
249
+ expect(writtenContent).toContain('# My zshrc')
250
+ // 应该更新配置
251
+ expect(mockSetConfigValue).toHaveBeenCalledWith('shellHook', false)
252
+ // 应该删除历史文件
253
+ expect(mockFs.unlinkSync).toHaveBeenCalled()
254
+ })
255
+
256
+ it('未安装时应该返回 true 并更新配置', async () => {
257
+ mockFs.existsSync.mockReturnValue(true)
258
+ mockFs.readFileSync.mockReturnValue(EMPTY_ZSHRC)
259
+
260
+ const { uninstallShellHook } = await resetShellHookModule()
261
+ const result = uninstallShellHook()
262
+
263
+ expect(result).toBe(true)
264
+ // 不应该写入文件
265
+ expect(mockFs.writeFileSync).not.toHaveBeenCalled()
266
+ // 应该更新配置
267
+ expect(mockSetConfigValue).toHaveBeenCalledWith('shellHook', false)
268
+ })
269
+
270
+ it('配置文件不存在时应该返回 true', async () => {
271
+ mockFs.existsSync.mockReturnValue(false)
272
+
273
+ const { uninstallShellHook } = await resetShellHookModule()
274
+ const result = uninstallShellHook()
275
+
276
+ expect(result).toBe(true)
277
+ expect(mockSetConfigValue).toHaveBeenCalledWith('shellHook', false)
278
+ })
279
+
280
+ it('历史文件不存在时不应该报错', async () => {
281
+ mockFs.existsSync.mockImplementation((path: any) => {
282
+ if (path === '/home/testuser/.zshrc') return true
283
+ if (path.includes('shell_history.jsonl')) return false
284
+ return true
285
+ })
286
+ mockFs.readFileSync.mockReturnValue(ZSHRC_WITH_HOOK)
287
+
288
+ const { uninstallShellHook } = await resetShellHookModule()
289
+ const result = uninstallShellHook()
290
+
291
+ expect(result).toBe(true)
292
+ })
293
+ })
294
+
295
+ // ============================================================================
296
+ // clearShellHistory 测试
297
+ // ============================================================================
298
+
299
+ describe('clearShellHistory', () => {
300
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>
301
+
302
+ beforeEach(() => {
303
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
304
+ })
305
+
306
+ afterEach(() => {
307
+ consoleLogSpy.mockRestore()
308
+ })
309
+
310
+ it('历史文件存在时应该删除', async () => {
311
+ mockFs.existsSync.mockReturnValue(true)
312
+
313
+ const { clearShellHistory } = await resetShellHookModule()
314
+ clearShellHistory()
315
+
316
+ expect(mockFs.unlinkSync).toHaveBeenCalled()
317
+ const allLogs = consoleLogSpy.mock.calls.map((c) => c[0]).join('\n')
318
+ expect(allLogs).toContain('已清空')
319
+ })
320
+
321
+ it('历史文件不存在时不应该报错', async () => {
322
+ mockFs.existsSync.mockReturnValue(false)
323
+
324
+ const { clearShellHistory } = await resetShellHookModule()
325
+ clearShellHistory()
326
+
327
+ expect(mockFs.unlinkSync).not.toHaveBeenCalled()
328
+ const allLogs = consoleLogSpy.mock.calls.map((c) => c[0]).join('\n')
329
+ expect(allLogs).toContain('已清空')
330
+ })
331
+ })
332
+
333
+ // ============================================================================
334
+ // getShellHistory 测试 (JSONL 解析)
335
+ // ============================================================================
336
+
337
+ describe('getShellHistory - JSONL 解析', () => {
338
+ it('shellHook 禁用时应该返回空数组', async () => {
339
+ mockConfig.shellHook = false
340
+
341
+ const { getShellHistory } = await resetShellHookModule()
342
+ const history = getShellHistory()
343
+
344
+ expect(history).toEqual([])
345
+ })
346
+
347
+ it('历史文件不存在时应该返回空数组', async () => {
348
+ mockConfig.shellHook = true
349
+ mockFs.existsSync.mockReturnValue(false)
350
+
351
+ const { getShellHistory } = await resetShellHookModule()
352
+ const history = getShellHistory()
353
+
354
+ expect(history).toEqual([])
355
+ })
356
+
357
+ it('应该正确解析 JSONL 格式', async () => {
358
+ mockConfig.shellHook = true
359
+ mockFs.existsSync.mockReturnValue(true)
360
+ mockFs.readFileSync.mockReturnValue(
361
+ '{"cmd":"ls -la","exit":0,"time":"2024-01-01T00:00:00Z"}\n' +
362
+ '{"cmd":"pwd","exit":0,"time":"2024-01-01T00:01:00Z"}\n' +
363
+ '{"cmd":"git status","exit":0,"time":"2024-01-01T00:02:00Z"}\n'
364
+ )
365
+
366
+ const { getShellHistory } = await resetShellHookModule()
367
+ const history = getShellHistory()
368
+
369
+ expect(history).toHaveLength(3)
370
+ expect(history[0].cmd).toBe('ls -la')
371
+ expect(history[1].cmd).toBe('pwd')
372
+ expect(history[2].cmd).toBe('git status')
373
+ })
374
+
375
+ it('应该跳过无效的 JSON 行', async () => {
376
+ mockConfig.shellHook = true
377
+ mockFs.existsSync.mockReturnValue(true)
378
+ mockFs.readFileSync.mockReturnValue(
379
+ '{"cmd":"ls","exit":0,"time":"2024-01-01"}\n' +
380
+ 'invalid json line\n' +
381
+ '{"cmd":"pwd","exit":0,"time":"2024-01-01"}\n'
382
+ )
383
+
384
+ const { getShellHistory } = await resetShellHookModule()
385
+ const history = getShellHistory()
386
+
387
+ expect(history).toHaveLength(2)
388
+ expect(history[0].cmd).toBe('ls')
389
+ expect(history[1].cmd).toBe('pwd')
390
+ })
391
+
392
+ it('应该应用 shellHistoryLimit 限制', async () => {
393
+ mockConfig.shellHook = true
394
+ mockConfig.shellHistoryLimit = 2
395
+ mockFs.existsSync.mockReturnValue(true)
396
+ mockFs.readFileSync.mockReturnValue(
397
+ '{"cmd":"cmd1","exit":0,"time":"2024-01-01"}\n' +
398
+ '{"cmd":"cmd2","exit":0,"time":"2024-01-01"}\n' +
399
+ '{"cmd":"cmd3","exit":0,"time":"2024-01-01"}\n' +
400
+ '{"cmd":"cmd4","exit":0,"time":"2024-01-01"}\n'
401
+ )
402
+
403
+ const { getShellHistory } = await resetShellHookModule()
404
+ const history = getShellHistory()
405
+
406
+ expect(history).toHaveLength(2)
407
+ // 应该返回最后 2 条
408
+ expect(history[0].cmd).toBe('cmd3')
409
+ expect(history[1].cmd).toBe('cmd4')
410
+ })
411
+
412
+ it('空文件应该返回空数组', async () => {
413
+ mockConfig.shellHook = true
414
+ mockFs.existsSync.mockReturnValue(true)
415
+ mockFs.readFileSync.mockReturnValue('')
416
+
417
+ const { getShellHistory } = await resetShellHookModule()
418
+ const history = getShellHistory()
419
+
420
+ expect(history).toEqual([])
421
+ })
422
+
423
+ it('只有空行的文件应该返回空数组', async () => {
424
+ mockConfig.shellHook = true
425
+ mockFs.existsSync.mockReturnValue(true)
426
+ mockFs.readFileSync.mockReturnValue('\n\n \n\n')
427
+
428
+ const { getShellHistory } = await resetShellHookModule()
429
+ const history = getShellHistory()
430
+
431
+ expect(history).toEqual([])
432
+ })
433
+
434
+ it('读取文件失败时应该返回空数组', async () => {
435
+ mockConfig.shellHook = true
436
+ mockFs.existsSync.mockReturnValue(true)
437
+ mockFs.readFileSync.mockImplementation(() => {
438
+ throw new Error('EACCES: permission denied')
439
+ })
440
+
441
+ const { getShellHistory } = await resetShellHookModule()
442
+ const history = getShellHistory()
443
+
444
+ expect(history).toEqual([])
445
+ })
446
+ })
447
+
448
+ // ============================================================================
449
+ // reinstallShellHook 测试
450
+ // ============================================================================
451
+
452
+ describe('reinstallShellHook', () => {
453
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>
454
+
455
+ beforeEach(() => {
456
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
457
+ })
458
+
459
+ afterEach(() => {
460
+ consoleLogSpy.mockRestore()
461
+ })
462
+
463
+ it('应该先卸载再安装', async () => {
464
+ // shellHook 必须为 true 才会执行重装
465
+ mockConfig.shellHook = true
466
+
467
+ // 第一次调用(卸载时)返回有 hook 的内容
468
+ // 第二次调用(安装时)返回无 hook 的内容
469
+ let callCount = 0
470
+ mockFs.existsSync.mockReturnValue(true)
471
+ mockFs.readFileSync.mockImplementation(() => {
472
+ callCount++
473
+ if (callCount === 1) return ZSHRC_WITH_HOOK // 卸载时读取
474
+ return EMPTY_ZSHRC // 安装时读取
475
+ })
476
+
477
+ const { reinstallShellHook } = await resetShellHookModule()
478
+ const result = await reinstallShellHook()
479
+
480
+ expect(result).toBe(true)
481
+ // 应该先写入(卸载),再追加(安装)
482
+ expect(mockFs.writeFileSync).toHaveBeenCalled()
483
+ expect(mockFs.appendFileSync).toHaveBeenCalled()
484
+ })
485
+
486
+ it('shellHook 禁用时应该返回 false', async () => {
487
+ mockConfig.shellHook = false
488
+
489
+ const { reinstallShellHook } = await resetShellHookModule()
490
+ const result = await reinstallShellHook()
491
+
492
+ expect(result).toBe(false)
493
+ expect(mockFs.writeFileSync).not.toHaveBeenCalled()
494
+ expect(mockFs.appendFileSync).not.toHaveBeenCalled()
495
+ })
496
+
497
+ it('silent 模式应该不输出日志', async () => {
498
+ mockConfig.shellHook = true
499
+ mockFs.existsSync.mockReturnValue(true)
500
+ mockFs.readFileSync.mockReturnValue(EMPTY_ZSHRC)
501
+
502
+ const { reinstallShellHook } = await resetShellHookModule()
503
+ await reinstallShellHook({ silent: true })
504
+
505
+ // silent 模式下,installShellHook 内部的日志仍然会输出
506
+ // 但 reinstallShellHook 本身不会输出额外日志
507
+ // 这个测试主要验证 silent 参数被正确传递
508
+ expect(true).toBe(true)
509
+ })
510
+ })