@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,507 @@
1
+ /**
2
+ * 远程 Shell Hook 测试
3
+ * 测试远程服务器上的 Hook 安装/卸载、状态检测等功能
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ // Mock chalk
7
+ vi.mock('chalk', () => ({
8
+ default: {
9
+ hex: vi.fn(() => (s) => s),
10
+ gray: vi.fn((s) => s),
11
+ bold: vi.fn((s) => s),
12
+ },
13
+ }));
14
+ // Mock theme 模块
15
+ vi.mock('../ui/theme.js', () => ({
16
+ getCurrentTheme: vi.fn(() => ({
17
+ primary: '#007acc',
18
+ secondary: '#6c757d',
19
+ success: '#4caf50',
20
+ error: '#f44336',
21
+ warning: '#ff9800',
22
+ })),
23
+ }));
24
+ import { detectRemoteShell, checkRemoteHookInstalled, installRemoteShellHook, uninstallRemoteShellHook, getRemoteHookStatus, getRemoteShellConfigPath, generateRemoteHookScript, } from '../shell-hook.js';
25
+ // 创建 mock SSH 执行函数的工厂
26
+ function createMockSshExec(responses) {
27
+ return vi.fn(async (cmd) => {
28
+ // 尝试精确匹配
29
+ if (responses[cmd]) {
30
+ return responses[cmd];
31
+ }
32
+ // 尝试部分匹配
33
+ for (const [pattern, response] of Object.entries(responses)) {
34
+ if (cmd.includes(pattern)) {
35
+ return response;
36
+ }
37
+ }
38
+ // 默认返回成功
39
+ return { stdout: '', exitCode: 0 };
40
+ });
41
+ }
42
+ // 创建一个会抛出错误的 mock SSH 执行函数
43
+ function createErrorSshExec(error) {
44
+ return vi.fn(async () => {
45
+ throw error;
46
+ });
47
+ }
48
+ beforeEach(() => {
49
+ vi.clearAllMocks();
50
+ });
51
+ afterEach(() => {
52
+ vi.restoreAllMocks();
53
+ });
54
+ // ============================================================================
55
+ // detectRemoteShell 测试
56
+ // ============================================================================
57
+ describe('detectRemoteShell', () => {
58
+ it('应该正确检测到 zsh', async () => {
59
+ const mockSshExec = createMockSshExec({
60
+ 'basename "$SHELL"': { stdout: 'zsh\n', exitCode: 0 },
61
+ });
62
+ const shell = await detectRemoteShell(mockSshExec);
63
+ expect(shell).toBe('zsh');
64
+ expect(mockSshExec).toHaveBeenCalledWith('basename "$SHELL"');
65
+ });
66
+ it('应该正确检测到 bash', async () => {
67
+ const mockSshExec = createMockSshExec({
68
+ 'basename "$SHELL"': { stdout: 'bash\n', exitCode: 0 },
69
+ });
70
+ const shell = await detectRemoteShell(mockSshExec);
71
+ expect(shell).toBe('bash');
72
+ });
73
+ it('应该去除 stdout 中的空白字符', async () => {
74
+ const mockSshExec = createMockSshExec({
75
+ 'basename "$SHELL"': { stdout: ' zsh \n', exitCode: 0 },
76
+ });
77
+ const shell = await detectRemoteShell(mockSshExec);
78
+ expect(shell).toBe('zsh');
79
+ });
80
+ it('命令失败时应该返回默认的 bash', async () => {
81
+ const mockSshExec = createMockSshExec({
82
+ 'basename "$SHELL"': { stdout: '', exitCode: 1 },
83
+ });
84
+ const shell = await detectRemoteShell(mockSshExec);
85
+ expect(shell).toBe('bash');
86
+ });
87
+ it('未知 shell 应该返回默认的 bash', async () => {
88
+ const mockSshExec = createMockSshExec({
89
+ 'basename "$SHELL"': { stdout: 'fish\n', exitCode: 0 },
90
+ });
91
+ const shell = await detectRemoteShell(mockSshExec);
92
+ expect(shell).toBe('bash');
93
+ });
94
+ it('SSH 执行抛出错误时应该返回默认的 bash', async () => {
95
+ const mockSshExec = createErrorSshExec(new Error('Connection refused'));
96
+ const shell = await detectRemoteShell(mockSshExec);
97
+ expect(shell).toBe('bash');
98
+ });
99
+ it('空 stdout 应该返回默认的 bash', async () => {
100
+ const mockSshExec = createMockSshExec({
101
+ 'basename "$SHELL"': { stdout: '', exitCode: 0 },
102
+ });
103
+ const shell = await detectRemoteShell(mockSshExec);
104
+ expect(shell).toBe('bash');
105
+ });
106
+ it('应该处理带路径的 shell 名称', async () => {
107
+ // 虽然 basename 命令应该只返回文件名,但测试边界情况
108
+ const mockSshExec = createMockSshExec({
109
+ 'basename "$SHELL"': { stdout: '/bin/zsh\n', exitCode: 0 },
110
+ });
111
+ const shell = await detectRemoteShell(mockSshExec);
112
+ // /bin/zsh 不等于 'zsh',所以返回默认 bash
113
+ expect(shell).toBe('bash');
114
+ });
115
+ });
116
+ // ============================================================================
117
+ // checkRemoteHookInstalled 测试
118
+ // ============================================================================
119
+ describe('checkRemoteHookInstalled', () => {
120
+ it('已安装时应该返回 true', async () => {
121
+ const mockSshExec = createMockSshExec({
122
+ 'grep': { stdout: 'installed\n', exitCode: 0 },
123
+ });
124
+ const installed = await checkRemoteHookInstalled(mockSshExec, '~/.zshrc');
125
+ expect(installed).toBe(true);
126
+ });
127
+ it('未安装时应该返回 false', async () => {
128
+ const mockSshExec = createMockSshExec({
129
+ 'grep': { stdout: 'not_installed\n', exitCode: 0 },
130
+ });
131
+ const installed = await checkRemoteHookInstalled(mockSshExec, '~/.zshrc');
132
+ expect(installed).toBe(false);
133
+ });
134
+ it('命令失败时应该返回 false', async () => {
135
+ const mockSshExec = createMockSshExec({
136
+ 'grep': { stdout: '', exitCode: 1 },
137
+ });
138
+ const installed = await checkRemoteHookInstalled(mockSshExec, '~/.zshrc');
139
+ expect(installed).toBe(false);
140
+ });
141
+ it('SSH 执行抛出错误时应该返回 false', async () => {
142
+ const mockSshExec = createErrorSshExec(new Error('Connection refused'));
143
+ const installed = await checkRemoteHookInstalled(mockSshExec, '~/.zshrc');
144
+ expect(installed).toBe(false);
145
+ });
146
+ it('应该检查正确的配置文件路径', async () => {
147
+ const mockSshExec = vi.fn().mockResolvedValue({ stdout: 'not_installed\n', exitCode: 0 });
148
+ await checkRemoteHookInstalled(mockSshExec, '~/.bashrc');
149
+ expect(mockSshExec).toHaveBeenCalledWith(expect.stringContaining('~/.bashrc'));
150
+ });
151
+ it('应该检查 Hook 开始标记', async () => {
152
+ const mockSshExec = vi.fn().mockResolvedValue({ stdout: 'not_installed\n', exitCode: 0 });
153
+ await checkRemoteHookInstalled(mockSshExec, '~/.zshrc');
154
+ expect(mockSshExec).toHaveBeenCalledWith(expect.stringContaining('pretty-please shell hook'));
155
+ });
156
+ });
157
+ // ============================================================================
158
+ // installRemoteShellHook 测试
159
+ // ============================================================================
160
+ describe('installRemoteShellHook', () => {
161
+ it('zsh 首次安装应该成功', async () => {
162
+ const mockSshExec = vi.fn()
163
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 }) // checkRemoteHookInstalled
164
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 }) // backup
165
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 }) // install
166
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 }); // mkdir
167
+ const result = await installRemoteShellHook(mockSshExec, 'zsh');
168
+ expect(result.success).toBe(true);
169
+ expect(result.message).toContain('已安装');
170
+ expect(result.message).toContain('.zshrc');
171
+ });
172
+ it('bash 首次安装应该成功', async () => {
173
+ const mockSshExec = vi.fn()
174
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 })
175
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 })
176
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 })
177
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 });
178
+ const result = await installRemoteShellHook(mockSshExec, 'bash');
179
+ expect(result.success).toBe(true);
180
+ expect(result.message).toContain('已安装');
181
+ expect(result.message).toContain('.bashrc');
182
+ });
183
+ it('已安装时应该跳过并返回成功', async () => {
184
+ const mockSshExec = vi.fn()
185
+ .mockResolvedValueOnce({ stdout: 'installed\n', exitCode: 0 });
186
+ const result = await installRemoteShellHook(mockSshExec, 'zsh');
187
+ expect(result.success).toBe(true);
188
+ expect(result.message).toContain('已安装');
189
+ expect(result.message).toContain('跳过');
190
+ });
191
+ it('不支持的 shell 类型应该返回失败', async () => {
192
+ const mockSshExec = vi.fn();
193
+ const result = await installRemoteShellHook(mockSshExec, 'powershell');
194
+ expect(result.success).toBe(false);
195
+ expect(result.message).toContain('不支持');
196
+ });
197
+ it('unknown shell 类型应该返回失败', async () => {
198
+ const mockSshExec = vi.fn();
199
+ const result = await installRemoteShellHook(mockSshExec, 'unknown');
200
+ expect(result.success).toBe(false);
201
+ expect(result.message).toContain('不支持');
202
+ });
203
+ it('安装命令失败应该返回失败', async () => {
204
+ const mockSshExec = vi.fn()
205
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 }) // check
206
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 }) // backup
207
+ .mockResolvedValueOnce({ stdout: 'Permission denied', exitCode: 1 }); // install fails
208
+ const result = await installRemoteShellHook(mockSshExec, 'zsh');
209
+ expect(result.success).toBe(false);
210
+ expect(result.message).toContain('安装失败');
211
+ });
212
+ it('SSH 执行抛出错误应该返回失败', async () => {
213
+ const mockSshExec = vi.fn()
214
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 })
215
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 })
216
+ .mockRejectedValueOnce(new Error('Network error'));
217
+ const result = await installRemoteShellHook(mockSshExec, 'zsh');
218
+ expect(result.success).toBe(false);
219
+ expect(result.message).toContain('安装失败');
220
+ expect(result.message).toContain('Network error');
221
+ });
222
+ it('备份失败不应该阻止安装', async () => {
223
+ const mockSshExec = vi.fn()
224
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 })
225
+ .mockRejectedValueOnce(new Error('Backup failed')) // backup fails
226
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 }) // install
227
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 }); // mkdir
228
+ const result = await installRemoteShellHook(mockSshExec, 'zsh');
229
+ expect(result.success).toBe(true);
230
+ });
231
+ it('应该创建 ~/.please 目录', async () => {
232
+ const mockSshExec = vi.fn()
233
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 })
234
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 })
235
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 })
236
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 });
237
+ await installRemoteShellHook(mockSshExec, 'zsh');
238
+ expect(mockSshExec).toHaveBeenCalledWith('mkdir -p ~/.please');
239
+ });
240
+ it('应该备份原配置文件', async () => {
241
+ const mockSshExec = vi.fn()
242
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 })
243
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 })
244
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 })
245
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 });
246
+ await installRemoteShellHook(mockSshExec, 'zsh');
247
+ expect(mockSshExec).toHaveBeenCalledWith(expect.stringContaining('.pls-backup'));
248
+ });
249
+ });
250
+ // ============================================================================
251
+ // uninstallRemoteShellHook 测试
252
+ // ============================================================================
253
+ describe('uninstallRemoteShellHook', () => {
254
+ it('已安装时应该成功卸载', async () => {
255
+ const mockSshExec = vi.fn()
256
+ .mockResolvedValueOnce({ stdout: 'installed\n', exitCode: 0 }) // check
257
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 }); // sed command
258
+ const result = await uninstallRemoteShellHook(mockSshExec, 'zsh');
259
+ expect(result.success).toBe(true);
260
+ expect(result.message).toContain('已卸载');
261
+ });
262
+ it('未安装时应该跳过并返回成功', async () => {
263
+ const mockSshExec = vi.fn()
264
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 });
265
+ const result = await uninstallRemoteShellHook(mockSshExec, 'zsh');
266
+ expect(result.success).toBe(true);
267
+ expect(result.message).toContain('未安装');
268
+ expect(result.message).toContain('跳过');
269
+ });
270
+ it('bash 卸载应该成功', async () => {
271
+ const mockSshExec = vi.fn()
272
+ .mockResolvedValueOnce({ stdout: 'installed\n', exitCode: 0 })
273
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 });
274
+ const result = await uninstallRemoteShellHook(mockSshExec, 'bash');
275
+ expect(result.success).toBe(true);
276
+ expect(result.message).toContain('已卸载');
277
+ });
278
+ it('sed 命令失败应该返回失败', async () => {
279
+ const mockSshExec = vi.fn()
280
+ .mockResolvedValueOnce({ stdout: 'installed\n', exitCode: 0 })
281
+ .mockResolvedValueOnce({ stdout: 'Permission denied', exitCode: 1 });
282
+ const result = await uninstallRemoteShellHook(mockSshExec, 'zsh');
283
+ expect(result.success).toBe(false);
284
+ expect(result.message).toContain('卸载失败');
285
+ });
286
+ it('SSH 执行抛出错误应该返回失败', async () => {
287
+ const mockSshExec = vi.fn()
288
+ .mockResolvedValueOnce({ stdout: 'installed\n', exitCode: 0 })
289
+ .mockRejectedValueOnce(new Error('Connection lost'));
290
+ const result = await uninstallRemoteShellHook(mockSshExec, 'zsh');
291
+ expect(result.success).toBe(false);
292
+ expect(result.message).toContain('卸载失败');
293
+ expect(result.message).toContain('Connection lost');
294
+ });
295
+ it('应该使用 sed 删除 hook 代码块', async () => {
296
+ const mockSshExec = vi.fn()
297
+ .mockResolvedValueOnce({ stdout: 'installed\n', exitCode: 0 })
298
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 });
299
+ await uninstallRemoteShellHook(mockSshExec, 'zsh');
300
+ // 验证第二次调用包含 sed 命令
301
+ const sedCall = mockSshExec.mock.calls[1][0];
302
+ expect(sedCall).toContain('sed');
303
+ expect(sedCall).toContain('pretty-please shell hook');
304
+ });
305
+ it('应该处理正确的配置文件路径', async () => {
306
+ const mockSshExec = vi.fn()
307
+ .mockResolvedValueOnce({ stdout: 'installed\n', exitCode: 0 })
308
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 });
309
+ await uninstallRemoteShellHook(mockSshExec, 'bash');
310
+ const sedCall = mockSshExec.mock.calls[1][0];
311
+ expect(sedCall).toContain('.bashrc');
312
+ });
313
+ });
314
+ // ============================================================================
315
+ // getRemoteHookStatus 测试
316
+ // ============================================================================
317
+ describe('getRemoteHookStatus', () => {
318
+ it('应该返回完整的状态信息', async () => {
319
+ const mockSshExec = vi.fn()
320
+ .mockResolvedValueOnce({ stdout: 'zsh\n', exitCode: 0 }) // detectRemoteShell
321
+ .mockResolvedValueOnce({ stdout: 'installed\n', exitCode: 0 }); // checkRemoteHookInstalled
322
+ const status = await getRemoteHookStatus(mockSshExec);
323
+ expect(status.installed).toBe(true);
324
+ expect(status.shellType).toBe('zsh');
325
+ expect(status.configPath).toBe('~/.zshrc');
326
+ });
327
+ it('bash 未安装状态应该正确返回', async () => {
328
+ const mockSshExec = vi.fn()
329
+ .mockResolvedValueOnce({ stdout: 'bash\n', exitCode: 0 })
330
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 });
331
+ const status = await getRemoteHookStatus(mockSshExec);
332
+ expect(status.installed).toBe(false);
333
+ expect(status.shellType).toBe('bash');
334
+ expect(status.configPath).toBe('~/.bashrc');
335
+ });
336
+ it('检测失败时应该使用默认 bash', async () => {
337
+ const mockSshExec = vi.fn()
338
+ .mockResolvedValueOnce({ stdout: '', exitCode: 1 }) // detect fails
339
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 });
340
+ const status = await getRemoteHookStatus(mockSshExec);
341
+ expect(status.shellType).toBe('bash');
342
+ expect(status.configPath).toBe('~/.bashrc');
343
+ });
344
+ it('检查安装状态失败应该返回 false', async () => {
345
+ const mockSshExec = vi.fn()
346
+ .mockResolvedValueOnce({ stdout: 'zsh\n', exitCode: 0 })
347
+ .mockRejectedValueOnce(new Error('Connection error'));
348
+ const status = await getRemoteHookStatus(mockSshExec);
349
+ expect(status.installed).toBe(false);
350
+ expect(status.shellType).toBe('zsh');
351
+ });
352
+ });
353
+ // ============================================================================
354
+ // getRemoteShellConfigPath 测试 (补充)
355
+ // ============================================================================
356
+ describe('getRemoteShellConfigPath', () => {
357
+ it('zsh 应该返回 ~/.zshrc', () => {
358
+ expect(getRemoteShellConfigPath('zsh')).toBe('~/.zshrc');
359
+ });
360
+ it('bash 应该返回 ~/.bashrc', () => {
361
+ expect(getRemoteShellConfigPath('bash')).toBe('~/.bashrc');
362
+ });
363
+ it('powershell 应该返回默认 ~/.bashrc', () => {
364
+ expect(getRemoteShellConfigPath('powershell')).toBe('~/.bashrc');
365
+ });
366
+ it('unknown 应该返回默认 ~/.bashrc', () => {
367
+ expect(getRemoteShellConfigPath('unknown')).toBe('~/.bashrc');
368
+ });
369
+ });
370
+ // ============================================================================
371
+ // generateRemoteHookScript 测试 (补充)
372
+ // ============================================================================
373
+ describe('generateRemoteHookScript', () => {
374
+ it('zsh 应该生成包含 preexec 和 precmd 的脚本', () => {
375
+ const script = generateRemoteHookScript('zsh');
376
+ expect(script).not.toBeNull();
377
+ expect(script).toContain('__pls_preexec');
378
+ expect(script).toContain('__pls_precmd');
379
+ expect(script).toContain('add-zsh-hook');
380
+ });
381
+ it('bash 应该生成包含 PROMPT_COMMAND 的脚本', () => {
382
+ const script = generateRemoteHookScript('bash');
383
+ expect(script).not.toBeNull();
384
+ expect(script).toContain('PROMPT_COMMAND');
385
+ expect(script).toContain('__pls_prompt_command');
386
+ });
387
+ it('powershell 应该返回 null', () => {
388
+ const script = generateRemoteHookScript('powershell');
389
+ expect(script).toBeNull();
390
+ });
391
+ it('unknown 应该返回 null', () => {
392
+ const script = generateRemoteHookScript('unknown');
393
+ expect(script).toBeNull();
394
+ });
395
+ it('zsh 脚本应该包含 Hook 标记', () => {
396
+ const script = generateRemoteHookScript('zsh');
397
+ expect(script).toContain('>>> pretty-please shell hook >>>');
398
+ expect(script).toContain('<<< pretty-please shell hook <<<');
399
+ });
400
+ it('bash 脚本应该包含 Hook 标记', () => {
401
+ const script = generateRemoteHookScript('bash');
402
+ expect(script).toContain('>>> pretty-please shell hook >>>');
403
+ expect(script).toContain('<<< pretty-please shell hook <<<');
404
+ });
405
+ it('zsh 脚本应该使用 ~/.please 目录', () => {
406
+ const script = generateRemoteHookScript('zsh');
407
+ expect(script).toContain('~/.please');
408
+ expect(script).toContain('shell_history.jsonl');
409
+ });
410
+ it('bash 脚本应该使用 ~/.please 目录', () => {
411
+ const script = generateRemoteHookScript('bash');
412
+ expect(script).toContain('~/.please');
413
+ expect(script).toContain('shell_history.jsonl');
414
+ });
415
+ it('zsh 脚本应该记录命令、退出码和时间戳', () => {
416
+ const script = generateRemoteHookScript('zsh');
417
+ expect(script).toContain('exit_code');
418
+ expect(script).toContain('timestamp');
419
+ });
420
+ it('bash 脚本应该记录命令、退出码和时间戳', () => {
421
+ const script = generateRemoteHookScript('bash');
422
+ expect(script).toContain('exit_code');
423
+ expect(script).toContain('timestamp');
424
+ });
425
+ });
426
+ // ============================================================================
427
+ // 集成场景测试
428
+ // ============================================================================
429
+ describe('远程 Hook 集成场景', () => {
430
+ it('完整安装流程:检测 → 安装 → 验证', async () => {
431
+ const callLog = [];
432
+ const mockSshExec = vi.fn(async (cmd) => {
433
+ callLog.push(cmd);
434
+ if (cmd.includes('basename')) {
435
+ return { stdout: 'zsh\n', exitCode: 0 };
436
+ }
437
+ if (cmd.includes('grep') && callLog.length <= 3) {
438
+ return { stdout: 'not_installed\n', exitCode: 0 };
439
+ }
440
+ if (cmd.includes('grep')) {
441
+ return { stdout: 'installed\n', exitCode: 0 };
442
+ }
443
+ return { stdout: '', exitCode: 0 };
444
+ });
445
+ // 1. 检测 shell
446
+ const shell = await detectRemoteShell(mockSshExec);
447
+ expect(shell).toBe('zsh');
448
+ // 2. 安装 hook
449
+ const installResult = await installRemoteShellHook(mockSshExec, shell);
450
+ expect(installResult.success).toBe(true);
451
+ // 3. 验证状态
452
+ const status = await getRemoteHookStatus(mockSshExec);
453
+ expect(status.installed).toBe(true);
454
+ expect(status.shellType).toBe('zsh');
455
+ });
456
+ it('完整卸载流程:检测 → 卸载 → 验证', async () => {
457
+ const callCount = { check: 0 };
458
+ const mockSshExec = vi.fn(async (cmd) => {
459
+ if (cmd.includes('basename')) {
460
+ return { stdout: 'bash\n', exitCode: 0 };
461
+ }
462
+ if (cmd.includes('grep')) {
463
+ callCount.check++;
464
+ // 第一次检查(卸载时)返回已安装,第二次(验证时)返回未安装
465
+ if (callCount.check <= 1) {
466
+ return { stdout: 'installed\n', exitCode: 0 };
467
+ }
468
+ return { stdout: 'not_installed\n', exitCode: 0 };
469
+ }
470
+ return { stdout: '', exitCode: 0 };
471
+ });
472
+ // 1. 获取当前状态
473
+ const beforeStatus = await getRemoteHookStatus(mockSshExec);
474
+ expect(beforeStatus.installed).toBe(true);
475
+ // 2. 卸载 hook
476
+ const uninstallResult = await uninstallRemoteShellHook(mockSshExec, 'bash');
477
+ expect(uninstallResult.success).toBe(true);
478
+ // 3. 验证状态
479
+ const afterStatus = await getRemoteHookStatus(mockSshExec);
480
+ expect(afterStatus.installed).toBe(false);
481
+ });
482
+ it('网络错误恢复场景', async () => {
483
+ let callCount = 0;
484
+ const mockSshExec = vi.fn(async () => {
485
+ callCount++;
486
+ if (callCount === 1) {
487
+ throw new Error('ETIMEDOUT');
488
+ }
489
+ return { stdout: 'bash\n', exitCode: 0 };
490
+ });
491
+ // 第一次调用失败
492
+ const shell1 = await detectRemoteShell(mockSshExec);
493
+ expect(shell1).toBe('bash'); // 默认值
494
+ // 第二次调用成功
495
+ const shell2 = await detectRemoteShell(mockSshExec);
496
+ expect(shell2).toBe('bash');
497
+ });
498
+ it('权限错误场景', async () => {
499
+ const mockSshExec = vi.fn()
500
+ .mockResolvedValueOnce({ stdout: 'not_installed\n', exitCode: 0 })
501
+ .mockResolvedValueOnce({ stdout: '', exitCode: 0 }) // backup
502
+ .mockResolvedValueOnce({ stdout: 'Permission denied: ~/.zshrc', exitCode: 1 });
503
+ const result = await installRemoteShellHook(mockSshExec, 'zsh');
504
+ expect(result.success).toBe(false);
505
+ expect(result.message).toContain('安装失败');
506
+ });
507
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shell Hook 管理模块测试
3
+ * 测试 Hook 脚本生成、安装/卸载、历史记录读写等功能
4
+ */
5
+ export {};