@yivan-lab/pretty-please 1.4.0 → 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 +30 -2
  2. package/bin/pls.tsx +153 -35
  3. package/dist/bin/pls.js +126 -23
  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 +6 -3
  37. package/dist/src/multi-step.js +6 -3
  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 +13 -0
  43. package/dist/src/shell-hook.js +163 -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 +5 -0
  47. package/dist/src/system-history.js +64 -18
  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 +8 -3
  81. package/src/multi-step.ts +7 -2
  82. package/src/project-context.ts +191 -0
  83. package/src/prompts.ts +26 -5
  84. package/src/shell-hook.ts +179 -33
  85. package/src/sysinfo.ts +326 -25
  86. package/src/system-history.ts +67 -14
  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,608 @@
1
+ /**
2
+ * 系统 Shell 历史读取模块测试
3
+ * 测试各种 Shell 历史格式解析和系统历史读取功能
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
7
+ import {
8
+ zshExtendedHistory,
9
+ zshSimpleHistory,
10
+ bashHistory,
11
+ powerShellHistory,
12
+ } from '../../tests/fixtures/shell-history'
13
+
14
+ // Mock fs 模块
15
+ vi.mock('fs', () => ({
16
+ default: {
17
+ existsSync: vi.fn(),
18
+ readFileSync: vi.fn(),
19
+ },
20
+ }))
21
+
22
+ // Mock config 模块
23
+ vi.mock('../config.js', () => ({
24
+ getConfig: vi.fn(() => ({
25
+ shellHistoryLimit: 10,
26
+ })),
27
+ }))
28
+
29
+ // Mock platform 模块
30
+ vi.mock('../utils/platform.js', () => ({
31
+ detectShell: vi.fn(),
32
+ getShellCapabilities: vi.fn(),
33
+ }))
34
+
35
+ import fs from 'fs'
36
+ import { getConfig } from '../config.js'
37
+ import { detectShell, getShellCapabilities } from '../utils/platform.js'
38
+
39
+ // 获取 mock 函数引用
40
+ const mockFs = vi.mocked(fs)
41
+ const mockDetectShell = vi.mocked(detectShell)
42
+ const mockGetShellCapabilities = vi.mocked(getShellCapabilities)
43
+ const mockGetConfig = vi.mocked(getConfig)
44
+
45
+ beforeEach(() => {
46
+ vi.clearAllMocks()
47
+ // 默认配置
48
+ mockGetConfig.mockReturnValue({
49
+ shellHistoryLimit: 10,
50
+ } as any)
51
+ })
52
+
53
+ afterEach(() => {
54
+ vi.restoreAllMocks()
55
+ })
56
+
57
+ // ============================================================================
58
+ // Zsh 历史解析测试
59
+ // ============================================================================
60
+
61
+ describe('Zsh 历史解析', () => {
62
+ beforeEach(() => {
63
+ mockDetectShell.mockReturnValue('zsh')
64
+ mockGetShellCapabilities.mockReturnValue({
65
+ supportsHistory: true,
66
+ historyPath: '/home/user/.zsh_history',
67
+ supportsHook: true,
68
+ hookType: 'zsh',
69
+ } as any)
70
+ mockFs.existsSync.mockReturnValue(true)
71
+ })
72
+
73
+ it('应该解析扩展格式(: timestamp:duration;command)', async () => {
74
+ mockFs.readFileSync.mockReturnValue(': 1700000000:0;ls -la')
75
+
76
+ const { getSystemShellHistory } = await import('../system-history.js')
77
+ const history = getSystemShellHistory()
78
+
79
+ expect(history).toHaveLength(1)
80
+ expect(history[0].cmd).toBe('ls -la')
81
+ expect(history[0].exit).toBe(0)
82
+ // 时间戳 1700000000 = 2023-11-14T22:13:20.000Z
83
+ expect(history[0].time).toContain('2023-11-14')
84
+ })
85
+
86
+ it('应该解析简单格式(纯命令)', async () => {
87
+ mockFs.readFileSync.mockReturnValue('git status')
88
+
89
+ const { getSystemShellHistory } = await import('../system-history.js')
90
+ const history = getSystemShellHistory()
91
+
92
+ expect(history).toHaveLength(1)
93
+ expect(history[0].cmd).toBe('git status')
94
+ expect(history[0].exit).toBe(0)
95
+ })
96
+
97
+ it('应该处理多行扩展格式历史', async () => {
98
+ mockFs.readFileSync.mockReturnValue(zshExtendedHistory)
99
+
100
+ const { getSystemShellHistory } = await import('../system-history.js')
101
+ const history = getSystemShellHistory()
102
+
103
+ expect(history).toHaveLength(5)
104
+ expect(history[0].cmd).toBe('ls -la')
105
+ expect(history[1].cmd).toBe('git status')
106
+ expect(history[4].cmd).toBe('cd ~/projects')
107
+ })
108
+
109
+ it('应该处理多行简单格式历史', async () => {
110
+ mockFs.readFileSync.mockReturnValue(zshSimpleHistory)
111
+
112
+ const { getSystemShellHistory } = await import('../system-history.js')
113
+ const history = getSystemShellHistory()
114
+
115
+ expect(history).toHaveLength(5)
116
+ expect(history[0].cmd).toBe('ls -la')
117
+ expect(history[4].cmd).toBe('cd ~/projects')
118
+ })
119
+
120
+ it('退出码应该默认为 0(系统历史无退出码)', async () => {
121
+ mockFs.readFileSync.mockReturnValue(': 1700000000:0;failed-command')
122
+
123
+ const { getSystemShellHistory } = await import('../system-history.js')
124
+ const history = getSystemShellHistory()
125
+
126
+ expect(history[0].exit).toBe(0)
127
+ })
128
+
129
+ it('空行应该被过滤', async () => {
130
+ mockFs.readFileSync.mockReturnValue('ls -la\n\n\ngit status\n\n')
131
+
132
+ const { getSystemShellHistory } = await import('../system-history.js')
133
+ const history = getSystemShellHistory()
134
+
135
+ expect(history).toHaveLength(2)
136
+ })
137
+
138
+ it('应该正确转换时间戳为 ISO 8601 格式', async () => {
139
+ // 时间戳 1700000000 对应 2023-11-14T22:13:20.000Z
140
+ mockFs.readFileSync.mockReturnValue(': 1700000000:0;test')
141
+
142
+ const { getSystemShellHistory } = await import('../system-history.js')
143
+ const history = getSystemShellHistory()
144
+
145
+ expect(history[0].time).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
146
+ })
147
+
148
+ it('duration 字段应该被忽略', async () => {
149
+ // duration 值不同,但结果应该一样
150
+ mockFs.readFileSync.mockReturnValue(': 1700000000:999;test')
151
+
152
+ const { getSystemShellHistory } = await import('../system-history.js')
153
+ const history = getSystemShellHistory()
154
+
155
+ expect(history[0].cmd).toBe('test')
156
+ })
157
+
158
+ it('应该保留命令中的特殊字符', async () => {
159
+ mockFs.readFileSync.mockReturnValue(': 1700000000:0;echo "hello $USER" && ls')
160
+
161
+ const { getSystemShellHistory } = await import('../system-history.js')
162
+ const history = getSystemShellHistory()
163
+
164
+ expect(history[0].cmd).toBe('echo "hello $USER" && ls')
165
+ })
166
+
167
+ it('简单格式的时间应该使用当前时间', async () => {
168
+ const beforeTime = new Date().toISOString()
169
+ mockFs.readFileSync.mockReturnValue('simple-command')
170
+
171
+ const { getSystemShellHistory } = await import('../system-history.js')
172
+ const history = getSystemShellHistory()
173
+ const afterTime = new Date().toISOString()
174
+
175
+ // 时间应该在测试执行期间
176
+ expect(history[0].time >= beforeTime).toBe(true)
177
+ expect(history[0].time <= afterTime).toBe(true)
178
+ })
179
+ })
180
+
181
+ // ============================================================================
182
+ // Bash 历史解析测试
183
+ // ============================================================================
184
+
185
+ describe('Bash 历史解析', () => {
186
+ beforeEach(() => {
187
+ mockDetectShell.mockReturnValue('bash')
188
+ mockGetShellCapabilities.mockReturnValue({
189
+ supportsHistory: true,
190
+ historyPath: '/home/user/.bash_history',
191
+ supportsHook: true,
192
+ hookType: 'bash',
193
+ } as any)
194
+ mockFs.existsSync.mockReturnValue(true)
195
+ })
196
+
197
+ it('应该解析纯文本命令', async () => {
198
+ mockFs.readFileSync.mockReturnValue('ls -la')
199
+
200
+ const { getSystemShellHistory } = await import('../system-history.js')
201
+ const history = getSystemShellHistory()
202
+
203
+ expect(history).toHaveLength(1)
204
+ expect(history[0].cmd).toBe('ls -la')
205
+ })
206
+
207
+ it('应该处理多行历史', async () => {
208
+ mockFs.readFileSync.mockReturnValue(bashHistory)
209
+
210
+ const { getSystemShellHistory } = await import('../system-history.js')
211
+ const history = getSystemShellHistory()
212
+
213
+ expect(history).toHaveLength(5)
214
+ expect(history[0].cmd).toBe('ls -la')
215
+ expect(history[2].cmd).toBe('npm install')
216
+ })
217
+
218
+ it('退出码应该默认为 0', async () => {
219
+ mockFs.readFileSync.mockReturnValue('any-command')
220
+
221
+ const { getSystemShellHistory } = await import('../system-history.js')
222
+ const history = getSystemShellHistory()
223
+
224
+ expect(history[0].exit).toBe(0)
225
+ })
226
+
227
+ it('时间应该使用当前时间', async () => {
228
+ const beforeTime = new Date().toISOString()
229
+ mockFs.readFileSync.mockReturnValue('test-command')
230
+
231
+ const { getSystemShellHistory } = await import('../system-history.js')
232
+ const history = getSystemShellHistory()
233
+
234
+ expect(history[0].time >= beforeTime).toBe(true)
235
+ })
236
+
237
+ it('空行应该被过滤', async () => {
238
+ mockFs.readFileSync.mockReturnValue('cmd1\n\ncmd2\n\n\ncmd3')
239
+
240
+ const { getSystemShellHistory } = await import('../system-history.js')
241
+ const history = getSystemShellHistory()
242
+
243
+ expect(history).toHaveLength(3)
244
+ })
245
+
246
+ it('应该去除首尾空格', async () => {
247
+ mockFs.readFileSync.mockReturnValue(' ls -la ')
248
+
249
+ const { getSystemShellHistory } = await import('../system-history.js')
250
+ const history = getSystemShellHistory()
251
+
252
+ expect(history[0].cmd).toBe('ls -la')
253
+ })
254
+
255
+ it('应该保留命令中的特殊字符', async () => {
256
+ mockFs.readFileSync.mockReturnValue('grep "pattern" file | awk \'{print $1}\'')
257
+
258
+ const { getSystemShellHistory } = await import('../system-history.js')
259
+ const history = getSystemShellHistory()
260
+
261
+ expect(history[0].cmd).toBe('grep "pattern" file | awk \'{print $1}\'')
262
+ })
263
+ })
264
+
265
+ // ============================================================================
266
+ // Fish 历史解析测试
267
+ // ============================================================================
268
+
269
+ describe('Fish 历史解析', () => {
270
+ beforeEach(() => {
271
+ mockDetectShell.mockReturnValue('fish')
272
+ mockGetShellCapabilities.mockReturnValue({
273
+ supportsHistory: true,
274
+ historyPath: '/home/user/.local/share/fish/fish_history',
275
+ supportsHook: false,
276
+ hookType: null,
277
+ } as any)
278
+ mockFs.existsSync.mockReturnValue(true)
279
+ })
280
+
281
+ it('应该解析 YAML-like 格式(- cmd: ...)', async () => {
282
+ mockFs.readFileSync.mockReturnValue('- cmd: ls -la')
283
+
284
+ const { getSystemShellHistory } = await import('../system-history.js')
285
+ const history = getSystemShellHistory()
286
+
287
+ expect(history).toHaveLength(1)
288
+ expect(history[0].cmd).toBe('ls -la')
289
+ })
290
+
291
+ it('非 cmd 行应该被过滤', async () => {
292
+ mockFs.readFileSync.mockReturnValue('- cmd: ls -la\n when: 1700000000\n- cmd: git status')
293
+
294
+ const { getSystemShellHistory } = await import('../system-history.js')
295
+ const history = getSystemShellHistory()
296
+
297
+ // 只有 cmd 行被解析
298
+ expect(history).toHaveLength(2)
299
+ expect(history[0].cmd).toBe('ls -la')
300
+ expect(history[1].cmd).toBe('git status')
301
+ })
302
+
303
+ it('退出码应该默认为 0', async () => {
304
+ mockFs.readFileSync.mockReturnValue('- cmd: failed-cmd')
305
+
306
+ const { getSystemShellHistory } = await import('../system-history.js')
307
+ const history = getSystemShellHistory()
308
+
309
+ expect(history[0].exit).toBe(0)
310
+ })
311
+
312
+ it('时间应该使用当前时间', async () => {
313
+ const beforeTime = new Date().toISOString()
314
+ mockFs.readFileSync.mockReturnValue('- cmd: test')
315
+
316
+ const { getSystemShellHistory } = await import('../system-history.js')
317
+ const history = getSystemShellHistory()
318
+
319
+ expect(history[0].time >= beforeTime).toBe(true)
320
+ })
321
+
322
+ it('格式错误的行应该被过滤', async () => {
323
+ mockFs.readFileSync.mockReturnValue('- cmd: valid\ninvalid line\n- cmd: also-valid')
324
+
325
+ const { getSystemShellHistory } = await import('../system-history.js')
326
+ const history = getSystemShellHistory()
327
+
328
+ expect(history).toHaveLength(2)
329
+ })
330
+
331
+ it('应该保留命令中的特殊字符', async () => {
332
+ mockFs.readFileSync.mockReturnValue('- cmd: echo "hello $USER"')
333
+
334
+ const { getSystemShellHistory } = await import('../system-history.js')
335
+ const history = getSystemShellHistory()
336
+
337
+ expect(history[0].cmd).toBe('echo "hello $USER"')
338
+ })
339
+ })
340
+
341
+ // ============================================================================
342
+ // PowerShell 历史解析测试
343
+ // ============================================================================
344
+
345
+ describe('PowerShell 历史解析', () => {
346
+ beforeEach(() => {
347
+ mockDetectShell.mockReturnValue('powershell7')
348
+ mockGetShellCapabilities.mockReturnValue({
349
+ supportsHistory: true,
350
+ historyPath: 'C:\\Users\\test\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt',
351
+ supportsHook: true,
352
+ hookType: 'powershell',
353
+ } as any)
354
+ mockFs.existsSync.mockReturnValue(true)
355
+ })
356
+
357
+ it('应该解析纯文本命令', async () => {
358
+ mockFs.readFileSync.mockReturnValue('Get-ChildItem')
359
+
360
+ const { getSystemShellHistory } = await import('../system-history.js')
361
+ const history = getSystemShellHistory()
362
+
363
+ expect(history).toHaveLength(1)
364
+ expect(history[0].cmd).toBe('Get-ChildItem')
365
+ })
366
+
367
+ it('应该处理多行历史', async () => {
368
+ mockFs.readFileSync.mockReturnValue(powerShellHistory)
369
+
370
+ const { getSystemShellHistory } = await import('../system-history.js')
371
+ const history = getSystemShellHistory()
372
+
373
+ expect(history).toHaveLength(5)
374
+ expect(history[0].cmd).toBe('Get-ChildItem')
375
+ expect(history[1].cmd).toBe('Get-Process')
376
+ })
377
+
378
+ it('退出码应该默认为 0', async () => {
379
+ mockFs.readFileSync.mockReturnValue('Get-Process')
380
+
381
+ const { getSystemShellHistory } = await import('../system-history.js')
382
+ const history = getSystemShellHistory()
383
+
384
+ expect(history[0].exit).toBe(0)
385
+ })
386
+
387
+ it('时间应该使用当前时间', async () => {
388
+ const beforeTime = new Date().toISOString()
389
+ mockFs.readFileSync.mockReturnValue('Write-Host "test"')
390
+
391
+ const { getSystemShellHistory } = await import('../system-history.js')
392
+ const history = getSystemShellHistory()
393
+
394
+ expect(history[0].time >= beforeTime).toBe(true)
395
+ })
396
+
397
+ it('空行应该被过滤', async () => {
398
+ mockFs.readFileSync.mockReturnValue('cmd1\n\ncmd2')
399
+
400
+ const { getSystemShellHistory } = await import('../system-history.js')
401
+ const history = getSystemShellHistory()
402
+
403
+ expect(history).toHaveLength(2)
404
+ })
405
+
406
+ it('PowerShell 5 也应该正常工作', async () => {
407
+ mockDetectShell.mockReturnValue('powershell5')
408
+ mockFs.readFileSync.mockReturnValue('Get-Service')
409
+
410
+ const { getSystemShellHistory } = await import('../system-history.js')
411
+ const history = getSystemShellHistory()
412
+
413
+ expect(history).toHaveLength(1)
414
+ expect(history[0].cmd).toBe('Get-Service')
415
+ })
416
+ })
417
+
418
+ // ============================================================================
419
+ // 系统历史读取测试
420
+ // ============================================================================
421
+
422
+ describe('getSystemShellHistory', () => {
423
+ it('应该根据 Shell 类型选择正确的解析器', async () => {
424
+ // Zsh
425
+ mockDetectShell.mockReturnValue('zsh')
426
+ mockGetShellCapabilities.mockReturnValue({
427
+ supportsHistory: true,
428
+ historyPath: '/home/user/.zsh_history',
429
+ } as any)
430
+ mockFs.existsSync.mockReturnValue(true)
431
+ mockFs.readFileSync.mockReturnValue(': 1700000000:0;zsh-cmd')
432
+
433
+ const { getSystemShellHistory } = await import('../system-history.js')
434
+ let history = getSystemShellHistory()
435
+ expect(history[0].cmd).toBe('zsh-cmd')
436
+
437
+ // Bash
438
+ mockDetectShell.mockReturnValue('bash')
439
+ mockGetShellCapabilities.mockReturnValue({
440
+ supportsHistory: true,
441
+ historyPath: '/home/user/.bash_history',
442
+ } as any)
443
+ mockFs.readFileSync.mockReturnValue('bash-cmd')
444
+
445
+ history = getSystemShellHistory()
446
+ expect(history[0].cmd).toBe('bash-cmd')
447
+ })
448
+
449
+ it('文件不存在时应该返回空数组', async () => {
450
+ mockDetectShell.mockReturnValue('zsh')
451
+ mockGetShellCapabilities.mockReturnValue({
452
+ supportsHistory: true,
453
+ historyPath: '/nonexistent/.zsh_history',
454
+ } as any)
455
+ mockFs.existsSync.mockReturnValue(false)
456
+
457
+ const { getSystemShellHistory } = await import('../system-history.js')
458
+ const history = getSystemShellHistory()
459
+
460
+ expect(history).toEqual([])
461
+ })
462
+
463
+ it('Shell 不支持历史时应该返回空数组', async () => {
464
+ mockDetectShell.mockReturnValue('cmd')
465
+ mockGetShellCapabilities.mockReturnValue({
466
+ supportsHistory: false,
467
+ historyPath: null,
468
+ } as any)
469
+
470
+ const { getSystemShellHistory } = await import('../system-history.js')
471
+ const history = getSystemShellHistory()
472
+
473
+ expect(history).toEqual([])
474
+ })
475
+
476
+ it('应该只返回最后 N 条(shellHistoryLimit)', async () => {
477
+ mockGetConfig.mockReturnValue({ shellHistoryLimit: 3 } as any)
478
+ mockDetectShell.mockReturnValue('bash')
479
+ mockGetShellCapabilities.mockReturnValue({
480
+ supportsHistory: true,
481
+ historyPath: '/home/user/.bash_history',
482
+ } as any)
483
+ mockFs.existsSync.mockReturnValue(true)
484
+ mockFs.readFileSync.mockReturnValue('cmd1\ncmd2\ncmd3\ncmd4\ncmd5')
485
+
486
+ const { getSystemShellHistory } = await import('../system-history.js')
487
+ const history = getSystemShellHistory()
488
+
489
+ expect(history).toHaveLength(3)
490
+ expect(history[0].cmd).toBe('cmd3')
491
+ expect(history[2].cmd).toBe('cmd5')
492
+ })
493
+
494
+ it('读取失败时应该返回空数组', async () => {
495
+ mockDetectShell.mockReturnValue('zsh')
496
+ mockGetShellCapabilities.mockReturnValue({
497
+ supportsHistory: true,
498
+ historyPath: '/home/user/.zsh_history',
499
+ } as any)
500
+ mockFs.existsSync.mockReturnValue(true)
501
+ mockFs.readFileSync.mockImplementation(() => {
502
+ throw new Error('EACCES: permission denied')
503
+ })
504
+
505
+ const { getSystemShellHistory } = await import('../system-history.js')
506
+ const history = getSystemShellHistory()
507
+
508
+ expect(history).toEqual([])
509
+ })
510
+
511
+ it('unknown Shell 应该返回空数组', async () => {
512
+ mockDetectShell.mockReturnValue('unknown')
513
+ mockGetShellCapabilities.mockReturnValue({
514
+ supportsHistory: false,
515
+ historyPath: null,
516
+ } as any)
517
+
518
+ const { getSystemShellHistory } = await import('../system-history.js')
519
+ const history = getSystemShellHistory()
520
+
521
+ expect(history).toEqual([])
522
+ })
523
+
524
+ it('空文件应该返回空数组', async () => {
525
+ mockDetectShell.mockReturnValue('bash')
526
+ mockGetShellCapabilities.mockReturnValue({
527
+ supportsHistory: true,
528
+ historyPath: '/home/user/.bash_history',
529
+ } as any)
530
+ mockFs.existsSync.mockReturnValue(true)
531
+ mockFs.readFileSync.mockReturnValue('')
532
+
533
+ const { getSystemShellHistory } = await import('../system-history.js')
534
+ const history = getSystemShellHistory()
535
+
536
+ expect(history).toEqual([])
537
+ })
538
+ })
539
+
540
+ // ============================================================================
541
+ // getLastCommandFromSystem 测试
542
+ // ============================================================================
543
+
544
+ describe('getLastCommandFromSystem', () => {
545
+ beforeEach(() => {
546
+ mockDetectShell.mockReturnValue('bash')
547
+ mockGetShellCapabilities.mockReturnValue({
548
+ supportsHistory: true,
549
+ historyPath: '/home/user/.bash_history',
550
+ } as any)
551
+ mockFs.existsSync.mockReturnValue(true)
552
+ })
553
+
554
+ it('应该返回最近一条非 pls 命令', async () => {
555
+ mockFs.readFileSync.mockReturnValue('git status\npls install git\nls -la')
556
+
557
+ const { getLastCommandFromSystem } = await import('../system-history.js')
558
+ const lastCmd = getLastCommandFromSystem()
559
+
560
+ expect(lastCmd).not.toBeNull()
561
+ expect(lastCmd!.cmd).toBe('ls -la')
562
+ })
563
+
564
+ it('应该排除 pls 命令', async () => {
565
+ mockFs.readFileSync.mockReturnValue('git status\npls fix\npls install')
566
+
567
+ const { getLastCommandFromSystem } = await import('../system-history.js')
568
+ const lastCmd = getLastCommandFromSystem()
569
+
570
+ expect(lastCmd!.cmd).toBe('git status')
571
+ })
572
+
573
+ it('应该排除 please 命令', async () => {
574
+ mockFs.readFileSync.mockReturnValue('npm install\nplease help\nplease run')
575
+
576
+ const { getLastCommandFromSystem } = await import('../system-history.js')
577
+ const lastCmd = getLastCommandFromSystem()
578
+
579
+ expect(lastCmd!.cmd).toBe('npm install')
580
+ })
581
+
582
+ it('所有命令都是 pls/please 时应该返回 null', async () => {
583
+ mockFs.readFileSync.mockReturnValue('pls help\npls config\nplease install')
584
+
585
+ const { getLastCommandFromSystem } = await import('../system-history.js')
586
+ const lastCmd = getLastCommandFromSystem()
587
+
588
+ expect(lastCmd).toBeNull()
589
+ })
590
+
591
+ it('历史为空时应该返回 null', async () => {
592
+ mockFs.readFileSync.mockReturnValue('')
593
+
594
+ const { getLastCommandFromSystem } = await import('../system-history.js')
595
+ const lastCmd = getLastCommandFromSystem()
596
+
597
+ expect(lastCmd).toBeNull()
598
+ })
599
+
600
+ it('文件不存在时应该返回 null', async () => {
601
+ mockFs.existsSync.mockReturnValue(false)
602
+
603
+ const { getLastCommandFromSystem } = await import('../system-history.js')
604
+ const lastCmd = getLastCommandFromSystem()
605
+
606
+ expect(lastCmd).toBeNull()
607
+ })
608
+ })
@@ -3,13 +3,8 @@ import { Box, Text } from 'ink'
3
3
  import Spinner from 'ink-spinner'
4
4
  import { MarkdownDisplay } from './MarkdownDisplay.js'
5
5
  import { chatWithMastra } from '../mastra-chat.js'
6
- import { getChatRoundCount, getChatHistory } from '../chat-history.js'
6
+ import { getChatRoundCount } from '../chat-history.js'
7
7
  import { getCurrentTheme } from '../ui/theme.js'
8
- import { formatSystemInfo } from '../sysinfo.js'
9
- import { formatHistoryForAI } from '../history.js'
10
- import { formatShellHistoryForAI, getShellHistory } from '../shell-hook.js'
11
- import { getConfig } from '../config.js'
12
- import { CHAT_SYSTEM_PROMPT, buildChatUserContext } from '../prompts.js'
13
8
 
14
9
  interface ChatProps {
15
10
  prompt: string
@@ -38,34 +33,7 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
38
33
  const [content, setContent] = useState('')
39
34
  const [duration, setDuration] = useState(0)
40
35
  const [roundCount] = useState(getChatRoundCount())
41
-
42
- // Debug 信息:直接在 useState 初始化时计算(同步)
43
- const [debugInfo] = useState<DebugInfo | null>(() => {
44
- if (!debug) return null
45
-
46
- const config = getConfig()
47
- const sysinfo = formatSystemInfo()
48
- const plsHistory = formatHistoryForAI()
49
- const shellHistory = formatShellHistoryForAI()
50
- const shellHookEnabled = config.shellHook && getShellHistory().length > 0
51
- const chatHistory = getChatHistory()
52
-
53
- const userContext = buildChatUserContext(
54
- prompt,
55
- sysinfo,
56
- plsHistory,
57
- shellHistory,
58
- shellHookEnabled
59
- )
60
-
61
- return {
62
- sysinfo,
63
- model: config.model,
64
- systemPrompt: CHAT_SYSTEM_PROMPT,
65
- userContext,
66
- chatHistory,
67
- }
68
- })
36
+ const [debugInfo, setDebugInfo] = useState<DebugInfo | null>(null)
69
37
 
70
38
  useEffect(() => {
71
39
  const startTime = Date.now()
@@ -76,13 +44,18 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
76
44
  setContent((prev) => prev + chunk)
77
45
  }
78
46
 
79
- // 调用 AI
80
- chatWithMastra(prompt, { debug: false, onChunk }) // 不需要 AI 返回 debug
47
+ // 调用 AI(如果需要 debug 信息,则开启)
48
+ chatWithMastra(prompt, { debug, onChunk })
81
49
  .then((result) => {
82
50
  const endTime = Date.now()
83
51
  setDuration(endTime - startTime)
84
52
  setStatus('done')
85
53
 
54
+ // 如果有 debug 信息,保存它
55
+ if (result.debug) {
56
+ setDebugInfo(result.debug)
57
+ }
58
+
86
59
  setTimeout(onComplete, debug ? 500 : 100)
87
60
  })
88
61
  .catch((error: any) => {
@@ -95,7 +68,7 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
95
68
  return (
96
69
  <Box flexDirection="column">
97
70
  {/* 调试信息 - 放在最前面 */}
98
- {debugInfo && (
71
+ {debug && debugInfo && (
99
72
  <Box flexDirection="column" marginBottom={1}>
100
73
  <Text color={theme.accent} bold>━━━ 调试信息 ━━━</Text>
101
74
  <Text color={theme.text.secondary}>模型: {debugInfo.model}</Text>