@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,602 @@
1
+ /**
2
+ * 命令生成工作流集成测试
3
+ * 测试用户输入 → AI生成 → 确认 → 执行 → 成功/失败 的完整流程
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
7
+
8
+ // Mock child_process 模块
9
+ vi.mock('child_process', () => ({
10
+ exec: vi.fn(),
11
+ execSync: vi.fn(),
12
+ spawn: vi.fn(),
13
+ }))
14
+
15
+ // Mock fs 模块
16
+ vi.mock('fs', () => ({
17
+ default: {
18
+ existsSync: vi.fn(),
19
+ readFileSync: vi.fn(),
20
+ writeFileSync: vi.fn(),
21
+ appendFileSync: vi.fn(),
22
+ mkdirSync: vi.fn(),
23
+ unlinkSync: vi.fn(),
24
+ statSync: vi.fn(),
25
+ },
26
+ }))
27
+
28
+ // Mock os 模块
29
+ vi.mock('os', () => ({
30
+ default: {
31
+ homedir: vi.fn(() => '/home/testuser'),
32
+ platform: vi.fn(() => 'linux'),
33
+ type: vi.fn(() => 'Linux'),
34
+ release: vi.fn(() => '5.4.0'),
35
+ arch: vi.fn(() => 'x64'),
36
+ userInfo: vi.fn(() => ({ username: 'testuser' })),
37
+ hostname: vi.fn(() => 'testhost'),
38
+ },
39
+ }))
40
+
41
+ // Mock config 模块
42
+ const mockConfig = {
43
+ aliases: {},
44
+ commandHistoryLimit: 50,
45
+ shellHistoryLimit: 20,
46
+ shellHook: false,
47
+ }
48
+
49
+ vi.mock('../config.js', () => ({
50
+ getConfig: vi.fn(() => mockConfig),
51
+ saveConfig: vi.fn(),
52
+ CONFIG_DIR: '/home/testuser/.please',
53
+ }))
54
+
55
+ // Mock remote 模块
56
+ vi.mock('../remote.js', () => ({
57
+ sshExec: vi.fn(),
58
+ getRemote: vi.fn(),
59
+ getRemotes: vi.fn(() => ({})),
60
+ testRemoteConnection: vi.fn(),
61
+ collectRemoteSysInfo: vi.fn(),
62
+ }))
63
+
64
+ // Mock theme 模块
65
+ vi.mock('../ui/theme.js', () => ({
66
+ getCurrentTheme: vi.fn(() => ({
67
+ primary: '#007acc',
68
+ secondary: '#6c757d',
69
+ success: '#4caf50',
70
+ error: '#f44336',
71
+ warning: '#ff9800',
72
+ text: { muted: '#666666' },
73
+ })),
74
+ }))
75
+
76
+ // Mock chalk
77
+ vi.mock('chalk', () => ({
78
+ default: {
79
+ bold: vi.fn((s: string) => s),
80
+ gray: vi.fn((s: string) => s),
81
+ dim: vi.fn((s: string) => s),
82
+ hex: vi.fn(() => (s: string) => s),
83
+ green: vi.fn((s: string) => s),
84
+ red: vi.fn((s: string) => s),
85
+ yellow: vi.fn((s: string) => s),
86
+ },
87
+ }))
88
+
89
+ import fs from 'fs'
90
+ import os from 'os'
91
+ import { getConfig, saveConfig } from '../config.js'
92
+ import { sshExec, getRemote, testRemoteConnection } from '../remote.js'
93
+
94
+ const mockFs = vi.mocked(fs)
95
+ const mockOs = vi.mocked(os)
96
+ const mockGetConfig = vi.mocked(getConfig)
97
+ const mockSaveConfig = vi.mocked(saveConfig)
98
+ const mockSshExec = vi.mocked(sshExec)
99
+ const mockGetRemote = vi.mocked(getRemote)
100
+ const mockTestRemoteConnection = vi.mocked(testRemoteConnection)
101
+
102
+ // 重置模块辅助函数
103
+ async function resetModules() {
104
+ vi.resetModules()
105
+ return {
106
+ alias: await import('../alias.js'),
107
+ history: await import('../history.js'),
108
+ remoteHistory: await import('../remote-history.js'),
109
+ }
110
+ }
111
+
112
+ beforeEach(() => {
113
+ vi.clearAllMocks()
114
+ mockOs.homedir.mockReturnValue('/home/testuser')
115
+ mockOs.platform.mockReturnValue('linux')
116
+ mockFs.mkdirSync.mockImplementation(() => undefined)
117
+ mockFs.writeFileSync.mockImplementation(() => {})
118
+ mockFs.existsSync.mockReturnValue(true)
119
+ mockFs.readFileSync.mockReturnValue(JSON.stringify([]))
120
+ mockSaveConfig.mockImplementation(() => {})
121
+
122
+ // 重置配置
123
+ Object.assign(mockConfig, {
124
+ aliases: {},
125
+ commandHistoryLimit: 50,
126
+ shellHistoryLimit: 20,
127
+ shellHook: false,
128
+ })
129
+ })
130
+
131
+ afterEach(() => {
132
+ vi.restoreAllMocks()
133
+ })
134
+
135
+ // ============================================================================
136
+ // 基础命令生成流程测试
137
+ // ============================================================================
138
+
139
+ describe('基础命令生成流程', () => {
140
+ it('用户输入 → 历史记录 → 成功执行', async () => {
141
+ let savedHistory: any[] = []
142
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
143
+ if (path.toString().includes('history.json')) {
144
+ savedHistory = JSON.parse(content)
145
+ }
146
+ })
147
+
148
+ const { history } = await resetModules()
149
+
150
+ // 模拟完整流程: 用户输入 "检查磁盘" → AI 生成 "df -h" → 执行成功
151
+ history.addHistory({
152
+ userPrompt: '检查磁盘',
153
+ command: 'df -h',
154
+ executed: true,
155
+ exitCode: 0,
156
+ output: 'Filesystem Size Used Avail Use%',
157
+ })
158
+
159
+ expect(savedHistory.length).toBe(1)
160
+ expect(savedHistory[0].userPrompt).toBe('检查磁盘')
161
+ expect(savedHistory[0].command).toBe('df -h')
162
+ expect(savedHistory[0].executed).toBe(true)
163
+ expect(savedHistory[0].exitCode).toBe(0)
164
+ expect(savedHistory[0].timestamp).toBeDefined()
165
+ })
166
+
167
+ it('命令执行失败应该记录退出码', async () => {
168
+ let savedHistory: any[] = []
169
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
170
+ if (path.toString().includes('history.json')) {
171
+ savedHistory = JSON.parse(content)
172
+ }
173
+ })
174
+
175
+ const { history } = await resetModules()
176
+
177
+ // 模拟执行失败
178
+ history.addHistory({
179
+ userPrompt: '查找文件',
180
+ command: 'find /nonexistent -name "*.txt"',
181
+ executed: true,
182
+ exitCode: 1,
183
+ output: 'find: /nonexistent: No such file or directory',
184
+ })
185
+
186
+ expect(savedHistory[0].exitCode).toBe(1)
187
+ expect(savedHistory[0].output).toContain('No such file or directory')
188
+ })
189
+
190
+ it('用户拒绝执行应该记录为未执行', async () => {
191
+ let savedHistory: any[] = []
192
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
193
+ if (path.toString().includes('history.json')) {
194
+ savedHistory = JSON.parse(content)
195
+ }
196
+ })
197
+
198
+ const { history } = await resetModules()
199
+
200
+ // 用户拒绝执行
201
+ history.addHistory({
202
+ userPrompt: '删除所有文件',
203
+ command: 'rm -rf *',
204
+ executed: false,
205
+ exitCode: null,
206
+ output: '',
207
+ reason: 'user_rejected',
208
+ })
209
+
210
+ expect(savedHistory[0].executed).toBe(false)
211
+ expect(savedHistory[0].exitCode).toBeNull()
212
+ expect(savedHistory[0].reason).toBe('user_rejected')
213
+ })
214
+ })
215
+
216
+ // ============================================================================
217
+ // 用户编辑流程测试
218
+ // ============================================================================
219
+
220
+ describe('用户编辑命令流程', () => {
221
+ it('用户修改命令应该记录 AI 生成和最终命令', async () => {
222
+ let savedHistory: any[] = []
223
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
224
+ if (path.toString().includes('history.json')) {
225
+ savedHistory = JSON.parse(content)
226
+ }
227
+ })
228
+
229
+ const { history } = await resetModules()
230
+
231
+ // AI 生成 "df -h",用户修改为 "df -h /home"
232
+ history.addHistory({
233
+ userPrompt: '检查磁盘',
234
+ command: 'df -h /home',
235
+ aiGeneratedCommand: 'df -h',
236
+ userModified: true,
237
+ executed: true,
238
+ exitCode: 0,
239
+ output: '',
240
+ })
241
+
242
+ expect(savedHistory[0].userModified).toBe(true)
243
+ expect(savedHistory[0].aiGeneratedCommand).toBe('df -h')
244
+ expect(savedHistory[0].command).toBe('df -h /home')
245
+ })
246
+
247
+ it('格式化历史应该区分 AI 生成和用户修改', async () => {
248
+ mockFs.readFileSync.mockReturnValue(JSON.stringify([
249
+ {
250
+ userPrompt: '检查磁盘',
251
+ command: 'df -h /home',
252
+ aiGeneratedCommand: 'df -h',
253
+ userModified: true,
254
+ executed: true,
255
+ exitCode: 0,
256
+ output: '',
257
+ timestamp: '2024-01-01T10:00:00.000Z',
258
+ },
259
+ ]))
260
+
261
+ const { history } = await resetModules()
262
+ const formatted = history.formatHistoryForAI()
263
+
264
+ expect(formatted).toContain('AI 生成')
265
+ expect(formatted).toContain('df -h')
266
+ expect(formatted).toContain('用户修改')
267
+ expect(formatted).toContain('df -h /home')
268
+ })
269
+ })
270
+
271
+ // ============================================================================
272
+ // 别名解析流程测试
273
+ // ============================================================================
274
+
275
+ describe('别名解析流程', () => {
276
+ it('应该解析简单别名', async () => {
277
+ mockConfig.aliases = {
278
+ disk: { prompt: '检查磁盘空间', description: '磁盘检查' },
279
+ }
280
+
281
+ const { alias } = await resetModules()
282
+ const result = alias.resolveAlias('@disk')
283
+
284
+ expect(result.resolved).toBe(true)
285
+ expect(result.prompt).toBe('检查磁盘空间')
286
+ expect(result.aliasName).toBe('disk')
287
+ })
288
+
289
+ it('应该支持模板参数替换', async () => {
290
+ mockConfig.aliases = {
291
+ deploy: { prompt: '部署 {{env}} 环境到 {{server}}' },
292
+ }
293
+
294
+ const { alias } = await resetModules()
295
+ const result = alias.resolveAlias('@deploy env=production server=web1')
296
+
297
+ expect(result.resolved).toBe(true)
298
+ expect(result.prompt).toBe('部署 production 环境到 web1')
299
+ })
300
+
301
+ it('应该使用默认参数值', async () => {
302
+ mockConfig.aliases = {
303
+ deploy: { prompt: '部署 {{env:staging}} 环境' },
304
+ }
305
+
306
+ const { alias } = await resetModules()
307
+ const result = alias.resolveAlias('@deploy')
308
+
309
+ expect(result.prompt).toBe('部署 staging 环境')
310
+ })
311
+
312
+ it('缺少必填参数应该抛出错误', async () => {
313
+ mockConfig.aliases = {
314
+ deploy: { prompt: '部署 {{env}} 环境' },
315
+ }
316
+
317
+ const { alias } = await resetModules()
318
+
319
+ expect(() => alias.resolveAlias('@deploy'))
320
+ .toThrow('缺少必填参数: env')
321
+ })
322
+
323
+ it('额外参数应该追加到 prompt', async () => {
324
+ mockConfig.aliases = {
325
+ list: { prompt: '列出文件' },
326
+ }
327
+
328
+ const { alias } = await resetModules()
329
+ const result = alias.resolveAlias('@list -la /home')
330
+
331
+ expect(result.prompt).toBe('列出文件 -la /home')
332
+ })
333
+ })
334
+
335
+ // ============================================================================
336
+ // 远程执行流程测试
337
+ // ============================================================================
338
+
339
+ describe('远程执行流程', () => {
340
+ it('远程执行成功应该记录到远程历史', async () => {
341
+ mockGetRemote.mockReturnValue({
342
+ host: '192.168.1.100',
343
+ user: 'root',
344
+ port: 22,
345
+ } as any)
346
+ mockSshExec.mockResolvedValue({
347
+ stdout: 'output',
348
+ stderr: '',
349
+ exitCode: 0,
350
+ output: 'output',
351
+ })
352
+
353
+ let savedHistory: any[] = []
354
+ mockFs.existsSync.mockReturnValue(false) // 历史文件不存在
355
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
356
+ savedHistory = JSON.parse(content)
357
+ })
358
+
359
+ const { remoteHistory } = await resetModules()
360
+
361
+ remoteHistory.addRemoteHistory('server1', {
362
+ userPrompt: '检查磁盘',
363
+ command: 'df -h',
364
+ executed: true,
365
+ exitCode: 0,
366
+ output: 'output',
367
+ })
368
+
369
+ expect(savedHistory.length).toBe(1)
370
+ expect(savedHistory[0].command).toBe('df -h')
371
+ expect(savedHistory[0].exitCode).toBe(0)
372
+ })
373
+
374
+ it('远程执行失败应该记录错误信息', async () => {
375
+ mockGetRemote.mockReturnValue({
376
+ host: '192.168.1.100',
377
+ user: 'root',
378
+ port: 22,
379
+ } as any)
380
+
381
+ let savedHistory: any[] = []
382
+ mockFs.existsSync.mockReturnValue(false)
383
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
384
+ savedHistory = JSON.parse(content)
385
+ })
386
+
387
+ const { remoteHistory } = await resetModules()
388
+
389
+ remoteHistory.addRemoteHistory('server1', {
390
+ userPrompt: '检查服务状态',
391
+ command: 'systemctl status nginx',
392
+ executed: true,
393
+ exitCode: 3,
394
+ output: 'nginx.service - A high performance web server\n Active: inactive (dead)',
395
+ })
396
+
397
+ expect(savedHistory[0].exitCode).toBe(3)
398
+ })
399
+
400
+ it('格式化远程历史应该包含服务器信息', async () => {
401
+ mockGetRemote.mockReturnValue({
402
+ host: '192.168.1.100',
403
+ user: 'root',
404
+ port: 22,
405
+ } as any)
406
+
407
+ mockFs.readFileSync.mockReturnValue(JSON.stringify([
408
+ {
409
+ userPrompt: '检查磁盘',
410
+ command: 'df -h',
411
+ executed: true,
412
+ exitCode: 0,
413
+ output: '',
414
+ timestamp: '2024-01-01T10:00:00.000Z',
415
+ },
416
+ ]))
417
+
418
+ const { remoteHistory } = await resetModules()
419
+ const formatted = remoteHistory.formatRemoteHistoryForAI('server1')
420
+
421
+ expect(formatted).toContain('df -h')
422
+ expect(formatted).toContain('✓')
423
+ })
424
+ })
425
+
426
+ // ============================================================================
427
+ // 多步骤命令流程测试
428
+ // ============================================================================
429
+
430
+ describe('多步骤命令流程', () => {
431
+ it('连续命令应该全部记录', async () => {
432
+ let savedHistory: any[] = []
433
+ mockFs.existsSync.mockReturnValue(true)
434
+ // 使用 getter 函数来动态返回当前 savedHistory
435
+ mockFs.readFileSync.mockImplementation(() => JSON.stringify(savedHistory))
436
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
437
+ savedHistory = JSON.parse(content)
438
+ })
439
+
440
+ const { history } = await resetModules()
441
+
442
+ // 步骤 1: git status
443
+ history.addHistory({
444
+ userPrompt: '查看 git 状态',
445
+ command: 'git status',
446
+ executed: true,
447
+ exitCode: 0,
448
+ output: 'On branch main',
449
+ })
450
+
451
+ // 步骤 2: git add
452
+ history.addHistory({
453
+ userPrompt: '添加所有文件',
454
+ command: 'git add .',
455
+ executed: true,
456
+ exitCode: 0,
457
+ output: '',
458
+ })
459
+
460
+ // 步骤 3: git commit
461
+ history.addHistory({
462
+ userPrompt: '提交更改',
463
+ command: 'git commit -m "update"',
464
+ executed: true,
465
+ exitCode: 0,
466
+ output: '[main abc1234] update',
467
+ })
468
+
469
+ expect(savedHistory.length).toBe(3)
470
+ // 检查是否包含所有命令
471
+ const commands = savedHistory.map((h: any) => h.command)
472
+ expect(commands).toContain('git status')
473
+ expect(commands).toContain('git add .')
474
+ expect(commands).toContain('git commit -m "update"')
475
+ })
476
+
477
+ it('成功和失败命令都应该记录', async () => {
478
+ let savedHistory: any[] = []
479
+ mockFs.existsSync.mockReturnValue(true)
480
+ mockFs.readFileSync.mockImplementation(() => JSON.stringify(savedHistory))
481
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
482
+ savedHistory = JSON.parse(content)
483
+ })
484
+
485
+ const { history } = await resetModules()
486
+
487
+ // 步骤 1: 成功
488
+ history.addHistory({
489
+ userPrompt: '编译项目',
490
+ command: 'npm run build',
491
+ executed: true,
492
+ exitCode: 0,
493
+ output: 'Build successful',
494
+ })
495
+
496
+ // 步骤 2: 失败
497
+ history.addHistory({
498
+ userPrompt: '运行测试',
499
+ command: 'npm test',
500
+ executed: true,
501
+ exitCode: 1,
502
+ output: 'Test failed: 2 assertions failed',
503
+ })
504
+
505
+ expect(savedHistory.length).toBe(2)
506
+ // addHistory 使用 unshift,所以最新的在 index 0
507
+ expect(savedHistory[0].exitCode).toBe(1) // npm test (最新)
508
+ expect(savedHistory[1].exitCode).toBe(0) // npm run build
509
+ })
510
+ })
511
+
512
+ // ============================================================================
513
+ // 历史数量限制测试
514
+ // ============================================================================
515
+
516
+ describe('历史数量限制', () => {
517
+ it('应该遵守 commandHistoryLimit 配置', async () => {
518
+ mockConfig.commandHistoryLimit = 3
519
+
520
+ // 已有 3 条历史(实际存储顺序:newest first)
521
+ let existingHistory = [
522
+ { userPrompt: '1', command: 'c1', executed: true, exitCode: 0, output: '', timestamp: '2024-01-01' },
523
+ { userPrompt: '2', command: 'c2', executed: true, exitCode: 0, output: '', timestamp: '2024-01-02' },
524
+ { userPrompt: '3', command: 'c3', executed: true, exitCode: 0, output: '', timestamp: '2024-01-03' },
525
+ ]
526
+
527
+ let savedHistory: any[] = []
528
+ mockFs.existsSync.mockReturnValue(true)
529
+ mockFs.readFileSync.mockImplementation(() => JSON.stringify(existingHistory))
530
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
531
+ savedHistory = JSON.parse(content)
532
+ existingHistory = savedHistory // 更新以便后续读取
533
+ })
534
+
535
+ const { history } = await resetModules()
536
+
537
+ // 添加第 4 条
538
+ history.addHistory({
539
+ userPrompt: '4',
540
+ command: 'c4',
541
+ executed: true,
542
+ exitCode: 0,
543
+ output: '',
544
+ })
545
+
546
+ // addHistory 使用 unshift 添加到开头,然后 truncate 从末尾删除
547
+ // [c1, c2, c3] → unshift c4 → [c4, c1, c2, c3] → truncate → [c4, c1, c2]
548
+ // 所以 c3 被删除,不是 c1
549
+ expect(savedHistory.length).toBe(3)
550
+ expect(savedHistory[0].command).toBe('c4') // 最新的在开头
551
+ expect(savedHistory[2].command).toBe('c2') // c3 被删除
552
+ })
553
+ })
554
+
555
+ // ============================================================================
556
+ // builtin 命令处理测试
557
+ // ============================================================================
558
+
559
+ describe('builtin 命令处理', () => {
560
+ it('builtin 命令应该标记为未执行', async () => {
561
+ let savedHistory: any[] = []
562
+ mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
563
+ if (path.toString().includes('history.json')) {
564
+ savedHistory = JSON.parse(content)
565
+ }
566
+ })
567
+
568
+ const { history } = await resetModules()
569
+
570
+ history.addHistory({
571
+ userPrompt: '删除危险文件',
572
+ command: 'rm -rf /',
573
+ executed: false,
574
+ exitCode: null,
575
+ output: '',
576
+ reason: 'builtin',
577
+ })
578
+
579
+ expect(savedHistory[0].executed).toBe(false)
580
+ expect(savedHistory[0].reason).toBe('builtin')
581
+ })
582
+
583
+ it('格式化历史应该显示 builtin 标记', async () => {
584
+ mockFs.readFileSync.mockReturnValue(JSON.stringify([
585
+ {
586
+ userPrompt: '删除文件',
587
+ command: 'rm -rf /',
588
+ executed: false,
589
+ exitCode: null,
590
+ output: '',
591
+ reason: 'builtin',
592
+ timestamp: '2024-01-01T10:00:00.000Z',
593
+ },
594
+ ]))
595
+
596
+ const { history } = await resetModules()
597
+ const formatted = history.formatHistoryForAI()
598
+
599
+ expect(formatted).toContain('builtin')
600
+ expect(formatted).toContain('未执行')
601
+ })
602
+ })