@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,572 @@
1
+ /**
2
+ * 系统信息检测模块测试
3
+ * 测试命令检测、包管理器检测、缓存机制、系统信息集成等功能
4
+ */
5
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
6
+ import { corruptedSystemCacheJson, } from '../../tests/fixtures/system-cache';
7
+ // Mock fs 模块
8
+ vi.mock('fs', () => ({
9
+ default: {
10
+ existsSync: vi.fn(),
11
+ readFileSync: vi.fn(),
12
+ writeFileSync: vi.fn(),
13
+ mkdirSync: vi.fn(),
14
+ },
15
+ }));
16
+ // Mock os 模块
17
+ vi.mock('os', () => ({
18
+ default: {
19
+ platform: vi.fn(() => 'darwin'),
20
+ arch: vi.fn(() => 'arm64'),
21
+ userInfo: vi.fn(() => ({ username: 'testuser' })),
22
+ },
23
+ }));
24
+ // Mock config 模块
25
+ vi.mock('../config.js', () => ({
26
+ getConfig: vi.fn(() => ({
27
+ systemCacheExpireDays: 7,
28
+ })),
29
+ CONFIG_DIR: '/home/user/.please',
30
+ }));
31
+ // Mock platform 模块
32
+ vi.mock('../utils/platform.js', () => ({
33
+ detectShell: vi.fn(() => 'zsh'),
34
+ getShellCapabilities: vi.fn(() => ({
35
+ displayName: 'Zsh',
36
+ supportsHistory: true,
37
+ supportsHook: true,
38
+ })),
39
+ commandExists: vi.fn(() => false),
40
+ batchCommandExists: vi.fn(() => []),
41
+ isWindows: vi.fn(() => false),
42
+ }));
43
+ // Mock project-context 模块
44
+ vi.mock('../project-context.js', () => ({
45
+ detectProjectContext: vi.fn(() => null),
46
+ formatProjectContext: vi.fn(() => ''),
47
+ }));
48
+ // Mock theme 模块
49
+ vi.mock('../ui/theme.js', () => ({
50
+ getCurrentTheme: vi.fn(() => ({
51
+ primary: '#007acc',
52
+ success: '#4caf50',
53
+ warning: '#ff9800',
54
+ text: {
55
+ muted: '#666666',
56
+ secondary: '#999999',
57
+ },
58
+ })),
59
+ }));
60
+ // Mock chalk
61
+ vi.mock('chalk', () => ({
62
+ default: {
63
+ bold: vi.fn((s) => s),
64
+ hex: vi.fn(() => (s) => s),
65
+ },
66
+ }));
67
+ import fs from 'fs';
68
+ import os from 'os';
69
+ import { getConfig, CONFIG_DIR } from '../config.js';
70
+ import { detectShell, getShellCapabilities, commandExists, batchCommandExists, isWindows, } from '../utils/platform.js';
71
+ import { detectProjectContext } from '../project-context.js';
72
+ // 获取 mock 函数引用
73
+ const mockFs = vi.mocked(fs);
74
+ const mockOs = vi.mocked(os);
75
+ const mockDetectShell = vi.mocked(detectShell);
76
+ const mockGetShellCapabilities = vi.mocked(getShellCapabilities);
77
+ const mockCommandExists = vi.mocked(commandExists);
78
+ const mockBatchCommandExists = vi.mocked(batchCommandExists);
79
+ const mockIsWindows = vi.mocked(isWindows);
80
+ const mockGetConfig = vi.mocked(getConfig);
81
+ const mockDetectProjectContext = vi.mocked(detectProjectContext);
82
+ beforeEach(() => {
83
+ vi.clearAllMocks();
84
+ // 默认配置
85
+ mockGetConfig.mockReturnValue({
86
+ systemCacheExpireDays: 7,
87
+ });
88
+ // 默认 macOS 环境
89
+ mockOs.platform.mockReturnValue('darwin');
90
+ mockOs.arch.mockReturnValue('arm64');
91
+ mockOs.userInfo.mockReturnValue({ username: 'testuser' });
92
+ mockDetectShell.mockReturnValue('zsh');
93
+ mockGetShellCapabilities.mockReturnValue({
94
+ displayName: 'Zsh',
95
+ supportsHistory: true,
96
+ supportsHook: true,
97
+ });
98
+ mockIsWindows.mockReturnValue(false);
99
+ mockCommandExists.mockReturnValue(false);
100
+ mockBatchCommandExists.mockReturnValue([]);
101
+ // 默认配置目录存在
102
+ mockFs.existsSync.mockReturnValue(true);
103
+ });
104
+ afterEach(() => {
105
+ vi.restoreAllMocks();
106
+ });
107
+ // ============================================================================
108
+ // 包管理器检测测试
109
+ // ============================================================================
110
+ describe('包管理器检测', () => {
111
+ describe('Windows 包管理器', () => {
112
+ beforeEach(() => {
113
+ mockIsWindows.mockReturnValue(true);
114
+ });
115
+ it('应该优先检测 winget', async () => {
116
+ mockCommandExists.mockImplementation((cmd) => cmd === 'winget');
117
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
118
+ const info = getStaticSystemInfo();
119
+ expect(info.systemPackageManager).toBe('winget');
120
+ });
121
+ it('winget 不存在时应该降级到 scoop', async () => {
122
+ mockCommandExists.mockImplementation((cmd) => cmd === 'scoop');
123
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
124
+ const info = getStaticSystemInfo();
125
+ expect(info.systemPackageManager).toBe('scoop');
126
+ });
127
+ it('scoop 不存在时应该降级到 choco', async () => {
128
+ mockCommandExists.mockImplementation((cmd) => cmd === 'choco');
129
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
130
+ const info = getStaticSystemInfo();
131
+ expect(info.systemPackageManager).toBe('choco');
132
+ });
133
+ it('都不存在时应该返回 unknown', async () => {
134
+ mockCommandExists.mockReturnValue(false);
135
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
136
+ const info = getStaticSystemInfo();
137
+ expect(info.systemPackageManager).toBe('unknown');
138
+ });
139
+ });
140
+ describe('macOS 包管理器', () => {
141
+ beforeEach(() => {
142
+ mockIsWindows.mockReturnValue(false);
143
+ mockOs.platform.mockReturnValue('darwin');
144
+ });
145
+ it('应该检测到 brew', async () => {
146
+ mockCommandExists.mockImplementation((cmd) => cmd === 'brew');
147
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
148
+ const info = getStaticSystemInfo();
149
+ expect(info.systemPackageManager).toBe('brew');
150
+ });
151
+ });
152
+ describe('Linux 包管理器', () => {
153
+ beforeEach(() => {
154
+ mockIsWindows.mockReturnValue(false);
155
+ mockOs.platform.mockReturnValue('linux');
156
+ });
157
+ it('应该检测 apt-get', async () => {
158
+ mockCommandExists.mockImplementation((cmd) => cmd === 'apt-get');
159
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
160
+ const info = getStaticSystemInfo();
161
+ expect(info.systemPackageManager).toBe('apt');
162
+ });
163
+ it('应该检测 dnf', async () => {
164
+ mockCommandExists.mockImplementation((cmd) => cmd === 'dnf');
165
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
166
+ const info = getStaticSystemInfo();
167
+ expect(info.systemPackageManager).toBe('dnf');
168
+ });
169
+ it('应该检测 pacman', async () => {
170
+ mockCommandExists.mockImplementation((cmd) => cmd === 'pacman');
171
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
172
+ const info = getStaticSystemInfo();
173
+ expect(info.systemPackageManager).toBe('pacman');
174
+ });
175
+ it('应该按优先级返回第一个可用的', async () => {
176
+ // apt-get 和 dnf 都存在,应该返回 apt(优先级更高)
177
+ mockCommandExists.mockImplementation((cmd) => cmd === 'apt-get' || cmd === 'dnf');
178
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
179
+ const info = getStaticSystemInfo();
180
+ // brew 在 apt-get 之前检测
181
+ expect(info.systemPackageManager).toBe('apt');
182
+ });
183
+ });
184
+ });
185
+ // ============================================================================
186
+ // 命令检测测试
187
+ // ============================================================================
188
+ describe('命令检测', () => {
189
+ it('应该使用 batchCommandExists 检测所有命令', async () => {
190
+ mockBatchCommandExists.mockReturnValue(['git', 'npm', 'docker']);
191
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
192
+ const info = getStaticSystemInfo();
193
+ expect(mockBatchCommandExists).toHaveBeenCalled();
194
+ expect(info.availableCommands).toEqual(['git', 'npm', 'docker']);
195
+ });
196
+ it('命令都不可用时应该返回空数组', async () => {
197
+ mockBatchCommandExists.mockReturnValue([]);
198
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
199
+ const info = getStaticSystemInfo();
200
+ expect(info.availableCommands).toEqual([]);
201
+ });
202
+ it('应该返回可用命令列表', async () => {
203
+ mockBatchCommandExists.mockReturnValue(['eza', 'fd', 'rg', 'bat', 'jq']);
204
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
205
+ const info = getStaticSystemInfo();
206
+ expect(info.availableCommands).toContain('eza');
207
+ expect(info.availableCommands).toContain('fd');
208
+ expect(info.availableCommands).toContain('rg');
209
+ });
210
+ });
211
+ // ============================================================================
212
+ // 缓存机制测试
213
+ // ============================================================================
214
+ describe('缓存机制', () => {
215
+ describe('缓存写入', () => {
216
+ it('首次调用应该生成缓存文件', async () => {
217
+ // 缓存文件不存在
218
+ mockFs.existsSync.mockImplementation((path) => {
219
+ if (path === CONFIG_DIR)
220
+ return true;
221
+ return false; // 缓存文件不存在
222
+ });
223
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
224
+ getStaticSystemInfo();
225
+ expect(mockFs.writeFileSync).toHaveBeenCalled();
226
+ });
227
+ it('缓存文件应该包含 version/cachedAt/expiresInDays', async () => {
228
+ mockFs.existsSync.mockImplementation((path) => {
229
+ if (path === CONFIG_DIR)
230
+ return true;
231
+ return false;
232
+ });
233
+ let writtenContent = '';
234
+ mockFs.writeFileSync.mockImplementation((path, content) => {
235
+ writtenContent = content;
236
+ });
237
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
238
+ getStaticSystemInfo();
239
+ const cache = JSON.parse(writtenContent);
240
+ expect(cache.version).toBe(1);
241
+ expect(cache.cachedAt).toBeDefined();
242
+ expect(cache.expiresInDays).toBe(7);
243
+ expect(cache.static).toBeDefined();
244
+ });
245
+ it('缓存文件应该是合法 JSON', async () => {
246
+ mockFs.existsSync.mockImplementation((path) => {
247
+ if (path === CONFIG_DIR)
248
+ return true;
249
+ return false;
250
+ });
251
+ let writtenContent = '';
252
+ mockFs.writeFileSync.mockImplementation((path, content) => {
253
+ writtenContent = content;
254
+ });
255
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
256
+ getStaticSystemInfo();
257
+ expect(() => JSON.parse(writtenContent)).not.toThrow();
258
+ });
259
+ });
260
+ describe('缓存读取', () => {
261
+ it('缓存未过期时应该返回缓存数据', async () => {
262
+ mockFs.existsSync.mockReturnValue(true);
263
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({
264
+ version: 1,
265
+ cachedAt: new Date().toISOString(), // 刚刚创建
266
+ expiresInDays: 7,
267
+ static: {
268
+ os: 'darwin',
269
+ arch: 'arm64',
270
+ shell: 'Zsh',
271
+ user: 'cacheduser',
272
+ systemPackageManager: 'brew',
273
+ availableCommands: ['git', 'cached-cmd'],
274
+ },
275
+ }));
276
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
277
+ const info = getStaticSystemInfo();
278
+ // 应该返回缓存数据
279
+ expect(info.user).toBe('cacheduser');
280
+ expect(info.availableCommands).toContain('cached-cmd');
281
+ // 不应该写入新缓存
282
+ expect(mockFs.writeFileSync).not.toHaveBeenCalled();
283
+ });
284
+ it('缓存过期后应该重新检测', async () => {
285
+ const expiredDate = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
286
+ mockFs.existsSync.mockReturnValue(true);
287
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({
288
+ version: 1,
289
+ cachedAt: expiredDate,
290
+ expiresInDays: 7,
291
+ static: {
292
+ os: 'darwin',
293
+ arch: 'arm64',
294
+ shell: 'Zsh',
295
+ user: 'olduser',
296
+ systemPackageManager: 'brew',
297
+ availableCommands: [],
298
+ },
299
+ }));
300
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
301
+ const info = getStaticSystemInfo();
302
+ // 应该重新检测,返回当前 mock 的用户
303
+ expect(info.user).toBe('testuser');
304
+ // 应该写入新缓存
305
+ expect(mockFs.writeFileSync).toHaveBeenCalled();
306
+ });
307
+ it('缓存文件损坏时应该重新检测', async () => {
308
+ mockFs.existsSync.mockReturnValue(true);
309
+ mockFs.readFileSync.mockReturnValue(corruptedSystemCacheJson);
310
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
311
+ const info = getStaticSystemInfo();
312
+ // 应该重新检测
313
+ expect(info.user).toBe('testuser');
314
+ expect(mockFs.writeFileSync).toHaveBeenCalled();
315
+ });
316
+ it('systemCacheExpireDays 配置应该生效', async () => {
317
+ // 配置为 1 天过期
318
+ mockGetConfig.mockReturnValue({ systemCacheExpireDays: 1 });
319
+ // 缓存 2 天前创建
320
+ const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
321
+ mockFs.existsSync.mockReturnValue(true);
322
+ mockFs.readFileSync.mockReturnValue(JSON.stringify({
323
+ version: 1,
324
+ cachedAt: twoDaysAgo,
325
+ expiresInDays: 1,
326
+ static: {
327
+ os: 'darwin',
328
+ arch: 'arm64',
329
+ shell: 'Zsh',
330
+ user: 'olduser',
331
+ systemPackageManager: 'brew',
332
+ availableCommands: [],
333
+ },
334
+ }));
335
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
336
+ const info = getStaticSystemInfo();
337
+ // 应该过期并重新检测
338
+ expect(info.user).toBe('testuser');
339
+ });
340
+ });
341
+ describe('目录创建', () => {
342
+ it('CONFIG_DIR 不存在时应该自动创建', async () => {
343
+ mockFs.existsSync.mockImplementation((path) => {
344
+ if (path === CONFIG_DIR)
345
+ return false; // 目录不存在
346
+ return false;
347
+ });
348
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
349
+ getStaticSystemInfo();
350
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith(CONFIG_DIR, { recursive: true });
351
+ });
352
+ });
353
+ });
354
+ // ============================================================================
355
+ // 系统信息集成测试
356
+ // ============================================================================
357
+ describe('系统信息集成', () => {
358
+ describe('getStaticSystemInfo', () => {
359
+ beforeEach(() => {
360
+ mockFs.existsSync.mockImplementation((path) => {
361
+ if (path === CONFIG_DIR)
362
+ return true;
363
+ return false; // 缓存文件不存在
364
+ });
365
+ });
366
+ it('应该返回正确的 OS 信息', async () => {
367
+ mockOs.platform.mockReturnValue('darwin');
368
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
369
+ const info = getStaticSystemInfo();
370
+ expect(info.os).toBe('darwin');
371
+ });
372
+ it('应该返回正确的架构信息', async () => {
373
+ mockOs.arch.mockReturnValue('arm64');
374
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
375
+ const info = getStaticSystemInfo();
376
+ expect(info.arch).toBe('arm64');
377
+ });
378
+ it('应该返回正确的 Shell 信息', async () => {
379
+ mockGetShellCapabilities.mockReturnValue({
380
+ displayName: 'Zsh',
381
+ });
382
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
383
+ const info = getStaticSystemInfo();
384
+ expect(info.shell).toBe('Zsh');
385
+ });
386
+ it('应该返回正确的用户信息', async () => {
387
+ mockOs.userInfo.mockReturnValue({ username: 'myuser' });
388
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
389
+ const info = getStaticSystemInfo();
390
+ expect(info.user).toBe('myuser');
391
+ });
392
+ });
393
+ describe('getDynamicSystemInfo', () => {
394
+ it('应该返回当前工作目录', async () => {
395
+ const originalCwd = process.cwd;
396
+ process.cwd = vi.fn(() => '/test/project');
397
+ const { getDynamicSystemInfo } = await import('../sysinfo.js');
398
+ const info = await getDynamicSystemInfo();
399
+ expect(info.cwd).toBe('/test/project');
400
+ process.cwd = originalCwd;
401
+ });
402
+ it('应该包含项目上下文', async () => {
403
+ mockDetectProjectContext.mockResolvedValue({
404
+ types: ['node'],
405
+ packageManager: 'pnpm',
406
+ root: '/test/project',
407
+ });
408
+ const { getDynamicSystemInfo } = await import('../sysinfo.js');
409
+ const info = await getDynamicSystemInfo();
410
+ expect(info.project).not.toBeNull();
411
+ expect(info.project?.types).toContain('node');
412
+ });
413
+ it('项目上下文可以为 null', async () => {
414
+ mockDetectProjectContext.mockResolvedValue(null);
415
+ const { getDynamicSystemInfo } = await import('../sysinfo.js');
416
+ const info = await getDynamicSystemInfo();
417
+ expect(info.project).toBeNull();
418
+ });
419
+ });
420
+ describe('getSystemInfo', () => {
421
+ beforeEach(() => {
422
+ mockFs.existsSync.mockImplementation((path) => {
423
+ if (path === CONFIG_DIR)
424
+ return true;
425
+ return false;
426
+ });
427
+ });
428
+ it('应该合并静态和动态信息', async () => {
429
+ mockDetectProjectContext.mockResolvedValue({
430
+ types: ['node'],
431
+ root: '/test',
432
+ });
433
+ const { getSystemInfo } = await import('../sysinfo.js');
434
+ const info = await getSystemInfo();
435
+ // 静态信息
436
+ expect(info.os).toBeDefined();
437
+ expect(info.arch).toBeDefined();
438
+ expect(info.shell).toBeDefined();
439
+ // 动态信息
440
+ expect(info.cwd).toBeDefined();
441
+ expect(info.project).not.toBeNull();
442
+ });
443
+ });
444
+ });
445
+ // ============================================================================
446
+ // 格式化测试
447
+ // ============================================================================
448
+ describe('formatSystemInfo', () => {
449
+ it('应该包含基础信息', async () => {
450
+ const { formatSystemInfo } = await import('../sysinfo.js');
451
+ const formatted = formatSystemInfo({
452
+ os: 'darwin',
453
+ arch: 'arm64',
454
+ shell: 'Zsh',
455
+ user: 'testuser',
456
+ systemPackageManager: 'brew',
457
+ availableCommands: [],
458
+ cwd: '/test',
459
+ project: null,
460
+ });
461
+ expect(formatted).toContain('darwin');
462
+ expect(formatted).toContain('arm64');
463
+ expect(formatted).toContain('Zsh');
464
+ expect(formatted).toContain('testuser');
465
+ expect(formatted).toContain('brew');
466
+ });
467
+ it('应该正确分类命令', async () => {
468
+ const { formatSystemInfo } = await import('../sysinfo.js');
469
+ const formatted = formatSystemInfo({
470
+ os: 'darwin',
471
+ arch: 'arm64',
472
+ shell: 'Zsh',
473
+ user: 'testuser',
474
+ systemPackageManager: 'brew',
475
+ availableCommands: ['eza', 'fd', 'pnpm', 'docker'],
476
+ cwd: '/test',
477
+ project: null,
478
+ });
479
+ expect(formatted).toContain('现代工具');
480
+ expect(formatted).toContain('eza');
481
+ expect(formatted).toContain('包管理器');
482
+ expect(formatted).toContain('pnpm');
483
+ expect(formatted).toContain('容器工具');
484
+ expect(formatted).toContain('docker');
485
+ });
486
+ it('空命令列表应该不显示工具部分', async () => {
487
+ const { formatSystemInfo } = await import('../sysinfo.js');
488
+ const formatted = formatSystemInfo({
489
+ os: 'darwin',
490
+ arch: 'arm64',
491
+ shell: 'Zsh',
492
+ user: 'testuser',
493
+ systemPackageManager: 'brew',
494
+ availableCommands: [],
495
+ cwd: '/test',
496
+ project: null,
497
+ });
498
+ expect(formatted).not.toContain('现代工具');
499
+ expect(formatted).not.toContain('【用户终端可用工具】');
500
+ });
501
+ });
502
+ // ============================================================================
503
+ // refreshSystemCache 测试
504
+ // ============================================================================
505
+ describe('refreshSystemCache', () => {
506
+ it('应该强制写入新缓存', async () => {
507
+ mockFs.existsSync.mockReturnValue(true);
508
+ const { refreshSystemCache } = await import('../sysinfo.js');
509
+ refreshSystemCache();
510
+ expect(mockFs.writeFileSync).toHaveBeenCalled();
511
+ });
512
+ it('应该创建配置目录(如果不存在)', async () => {
513
+ mockFs.existsSync.mockReturnValue(false);
514
+ const { refreshSystemCache } = await import('../sysinfo.js');
515
+ refreshSystemCache();
516
+ expect(mockFs.mkdirSync).toHaveBeenCalledWith(CONFIG_DIR, { recursive: true });
517
+ });
518
+ it('新缓存应该包含当前时间戳', async () => {
519
+ mockFs.existsSync.mockReturnValue(true);
520
+ let writtenContent = '';
521
+ mockFs.writeFileSync.mockImplementation((path, content) => {
522
+ writtenContent = content;
523
+ });
524
+ const beforeTime = new Date().toISOString();
525
+ const { refreshSystemCache } = await import('../sysinfo.js');
526
+ refreshSystemCache();
527
+ const afterTime = new Date().toISOString();
528
+ const cache = JSON.parse(writtenContent);
529
+ expect(cache.cachedAt >= beforeTime).toBe(true);
530
+ expect(cache.cachedAt <= afterTime).toBe(true);
531
+ });
532
+ });
533
+ // ============================================================================
534
+ // 边界情况测试
535
+ // ============================================================================
536
+ describe('边界情况', () => {
537
+ it('Windows 系统上应该正确检测', async () => {
538
+ mockIsWindows.mockReturnValue(true);
539
+ mockOs.platform.mockReturnValue('win32');
540
+ mockOs.arch.mockReturnValue('x64');
541
+ mockDetectShell.mockReturnValue('powershell7');
542
+ mockGetShellCapabilities.mockReturnValue({
543
+ displayName: 'PowerShell 7+',
544
+ });
545
+ mockFs.existsSync.mockImplementation((path) => {
546
+ if (path === CONFIG_DIR)
547
+ return true;
548
+ return false;
549
+ });
550
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
551
+ const info = getStaticSystemInfo();
552
+ expect(info.os).toBe('win32');
553
+ expect(info.shell).toBe('PowerShell 7+');
554
+ });
555
+ it('Linux 系统上应该正确检测', async () => {
556
+ mockOs.platform.mockReturnValue('linux');
557
+ mockOs.arch.mockReturnValue('x64');
558
+ mockDetectShell.mockReturnValue('bash');
559
+ mockGetShellCapabilities.mockReturnValue({
560
+ displayName: 'Bash',
561
+ });
562
+ mockFs.existsSync.mockImplementation((path) => {
563
+ if (path === CONFIG_DIR)
564
+ return true;
565
+ return false;
566
+ });
567
+ const { getStaticSystemInfo } = await import('../sysinfo.js');
568
+ const info = getStaticSystemInfo();
569
+ expect(info.os).toBe('linux');
570
+ expect(info.shell).toBe('Bash');
571
+ });
572
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 系统 Shell 历史读取模块测试
3
+ * 测试各种 Shell 历史格式解析和系统历史读取功能
4
+ */
5
+ export {};