@yivan-lab/pretty-please 1.4.0 → 1.5.1
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.
- package/README.md +32 -4
- package/bin/pls.tsx +153 -35
- package/dist/bin/pls.js +126 -23
- package/dist/package.json +10 -2
- package/dist/src/__integration__/command-generation.test.d.ts +5 -0
- package/dist/src/__integration__/command-generation.test.js +508 -0
- package/dist/src/__integration__/error-recovery.test.d.ts +5 -0
- package/dist/src/__integration__/error-recovery.test.js +511 -0
- package/dist/src/__integration__/shell-hook-workflow.test.d.ts +5 -0
- package/dist/src/__integration__/shell-hook-workflow.test.js +375 -0
- package/dist/src/__tests__/alias.test.d.ts +5 -0
- package/dist/src/__tests__/alias.test.js +421 -0
- package/dist/src/__tests__/chat-history.test.d.ts +5 -0
- package/dist/src/__tests__/chat-history.test.js +372 -0
- package/dist/src/__tests__/config.test.d.ts +5 -0
- package/dist/src/__tests__/config.test.js +822 -0
- package/dist/src/__tests__/history.test.d.ts +5 -0
- package/dist/src/__tests__/history.test.js +439 -0
- package/dist/src/__tests__/remote-history.test.d.ts +5 -0
- package/dist/src/__tests__/remote-history.test.js +641 -0
- package/dist/src/__tests__/remote.test.d.ts +5 -0
- package/dist/src/__tests__/remote.test.js +689 -0
- package/dist/src/__tests__/shell-hook-install.test.d.ts +5 -0
- package/dist/src/__tests__/shell-hook-install.test.js +413 -0
- package/dist/src/__tests__/shell-hook-remote.test.d.ts +5 -0
- package/dist/src/__tests__/shell-hook-remote.test.js +507 -0
- package/dist/src/__tests__/shell-hook.test.d.ts +5 -0
- package/dist/src/__tests__/shell-hook.test.js +440 -0
- package/dist/src/__tests__/sysinfo.test.d.ts +5 -0
- package/dist/src/__tests__/sysinfo.test.js +572 -0
- package/dist/src/__tests__/system-history.test.d.ts +5 -0
- package/dist/src/__tests__/system-history.test.js +457 -0
- package/dist/src/components/Chat.js +9 -28
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +30 -2
- package/dist/src/mastra-chat.js +6 -3
- package/dist/src/multi-step.js +6 -3
- package/dist/src/project-context.d.ts +22 -0
- package/dist/src/project-context.js +168 -0
- package/dist/src/prompts.d.ts +4 -4
- package/dist/src/prompts.js +23 -6
- package/dist/src/shell-hook.d.ts +13 -0
- package/dist/src/shell-hook.js +163 -33
- package/dist/src/sysinfo.d.ts +38 -9
- package/dist/src/sysinfo.js +245 -21
- package/dist/src/system-history.d.ts +5 -0
- package/dist/src/system-history.js +64 -18
- package/dist/src/ui/__tests__/theme.test.d.ts +5 -0
- package/dist/src/ui/__tests__/theme.test.js +688 -0
- package/dist/src/upgrade.js +3 -0
- package/dist/src/user-preferences.d.ts +44 -0
- package/dist/src/user-preferences.js +147 -0
- package/dist/src/utils/__tests__/platform-capabilities.test.d.ts +5 -0
- package/dist/src/utils/__tests__/platform-capabilities.test.js +214 -0
- package/dist/src/utils/__tests__/platform-exec.test.d.ts +5 -0
- package/dist/src/utils/__tests__/platform-exec.test.js +212 -0
- package/dist/src/utils/__tests__/platform-shell.test.d.ts +5 -0
- package/dist/src/utils/__tests__/platform-shell.test.js +300 -0
- package/dist/src/utils/__tests__/platform.test.d.ts +5 -0
- package/dist/src/utils/__tests__/platform.test.js +137 -0
- package/dist/src/utils/platform.d.ts +88 -0
- package/dist/src/utils/platform.js +331 -0
- package/package.json +10 -2
- package/src/__integration__/command-generation.test.ts +602 -0
- package/src/__integration__/error-recovery.test.ts +620 -0
- package/src/__integration__/shell-hook-workflow.test.ts +457 -0
- package/src/__tests__/alias.test.ts +545 -0
- package/src/__tests__/chat-history.test.ts +462 -0
- package/src/__tests__/config.test.ts +1043 -0
- package/src/__tests__/history.test.ts +538 -0
- package/src/__tests__/remote-history.test.ts +791 -0
- package/src/__tests__/remote.test.ts +866 -0
- package/src/__tests__/shell-hook-install.test.ts +510 -0
- package/src/__tests__/shell-hook-remote.test.ts +679 -0
- package/src/__tests__/shell-hook.test.ts +564 -0
- package/src/__tests__/sysinfo.test.ts +718 -0
- package/src/__tests__/system-history.test.ts +608 -0
- package/src/components/Chat.tsx +10 -37
- package/src/config.ts +29 -2
- package/src/mastra-chat.ts +8 -3
- package/src/multi-step.ts +7 -2
- package/src/project-context.ts +191 -0
- package/src/prompts.ts +26 -5
- package/src/shell-hook.ts +179 -33
- package/src/sysinfo.ts +326 -25
- package/src/system-history.ts +67 -14
- package/src/ui/__tests__/theme.test.ts +869 -0
- package/src/upgrade.ts +5 -0
- package/src/user-preferences.ts +178 -0
- package/src/utils/__tests__/platform-capabilities.test.ts +265 -0
- package/src/utils/__tests__/platform-exec.test.ts +278 -0
- package/src/utils/__tests__/platform-shell.test.ts +353 -0
- package/src/utils/__tests__/platform.test.ts +170 -0
- package/src/utils/platform.ts +431 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令历史记录模块测试
|
|
3
|
+
* 测试历史记录的读写、格式化等功能
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
6
|
+
import { plsHistory } from '../../tests/fixtures/history';
|
|
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
|
+
homedir: vi.fn(() => '/home/testuser'),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
// Mock config 模块
|
|
23
|
+
vi.mock('../config.js', () => ({
|
|
24
|
+
getConfig: vi.fn(() => ({
|
|
25
|
+
commandHistoryLimit: 10,
|
|
26
|
+
})),
|
|
27
|
+
}));
|
|
28
|
+
import fs from 'fs';
|
|
29
|
+
import os from 'os';
|
|
30
|
+
import { getConfig } from '../config.js';
|
|
31
|
+
// 获取 mock 函数引用
|
|
32
|
+
const mockFs = vi.mocked(fs);
|
|
33
|
+
const mockOs = vi.mocked(os);
|
|
34
|
+
const mockGetConfig = vi.mocked(getConfig);
|
|
35
|
+
// 模块状态重置辅助
|
|
36
|
+
async function resetHistoryModule() {
|
|
37
|
+
vi.resetModules();
|
|
38
|
+
return await import('../history.js');
|
|
39
|
+
}
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
mockOs.homedir.mockReturnValue('/home/testuser');
|
|
43
|
+
mockGetConfig.mockReturnValue({
|
|
44
|
+
commandHistoryLimit: 10,
|
|
45
|
+
});
|
|
46
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
47
|
+
mockFs.writeFileSync.mockImplementation(() => { });
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// getHistory 测试
|
|
54
|
+
// ============================================================================
|
|
55
|
+
describe('getHistory', () => {
|
|
56
|
+
it('应该返回历史记录数组', async () => {
|
|
57
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
58
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(plsHistory));
|
|
59
|
+
const { getHistory } = await resetHistoryModule();
|
|
60
|
+
const history = getHistory();
|
|
61
|
+
expect(Array.isArray(history)).toBe(true);
|
|
62
|
+
expect(history.length).toBe(plsHistory.length);
|
|
63
|
+
});
|
|
64
|
+
it('历史文件不存在时应该返回空数组', async () => {
|
|
65
|
+
mockFs.existsSync.mockImplementation((path) => {
|
|
66
|
+
if (path.includes('history.json'))
|
|
67
|
+
return false;
|
|
68
|
+
return true; // 目录存在
|
|
69
|
+
});
|
|
70
|
+
const { getHistory } = await resetHistoryModule();
|
|
71
|
+
const history = getHistory();
|
|
72
|
+
expect(history).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
it('JSON 损坏时应该返回空数组', async () => {
|
|
75
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
76
|
+
mockFs.readFileSync.mockReturnValue('{invalid json');
|
|
77
|
+
const { getHistory } = await resetHistoryModule();
|
|
78
|
+
const history = getHistory();
|
|
79
|
+
expect(history).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
it('应该包含正确的历史记录字段', async () => {
|
|
82
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
83
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(plsHistory));
|
|
84
|
+
const { getHistory } = await resetHistoryModule();
|
|
85
|
+
const history = getHistory();
|
|
86
|
+
expect(history[0].userPrompt).toBe('安装 git');
|
|
87
|
+
expect(history[0].command).toBe('brew install git');
|
|
88
|
+
expect(history[0].executed).toBe(true);
|
|
89
|
+
expect(history[0].exitCode).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
it('应该创建配置目录(如果不存在)', async () => {
|
|
92
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
93
|
+
const { getHistory } = await resetHistoryModule();
|
|
94
|
+
getHistory();
|
|
95
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(expect.stringContaining('.please'), { recursive: true });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// addHistory 测试
|
|
100
|
+
// ============================================================================
|
|
101
|
+
describe('addHistory', () => {
|
|
102
|
+
it('应该添加新记录到历史', async () => {
|
|
103
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
104
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]));
|
|
105
|
+
let writtenContent = '';
|
|
106
|
+
mockFs.writeFileSync.mockImplementation((path, content) => {
|
|
107
|
+
writtenContent = content;
|
|
108
|
+
});
|
|
109
|
+
const { addHistory } = await resetHistoryModule();
|
|
110
|
+
addHistory({
|
|
111
|
+
userPrompt: '测试命令',
|
|
112
|
+
command: 'echo test',
|
|
113
|
+
executed: true,
|
|
114
|
+
exitCode: 0,
|
|
115
|
+
});
|
|
116
|
+
const saved = JSON.parse(writtenContent);
|
|
117
|
+
expect(saved.length).toBe(1);
|
|
118
|
+
expect(saved[0].userPrompt).toBe('测试命令');
|
|
119
|
+
expect(saved[0].command).toBe('echo test');
|
|
120
|
+
});
|
|
121
|
+
it('应该添加时间戳', async () => {
|
|
122
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
123
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]));
|
|
124
|
+
let writtenContent = '';
|
|
125
|
+
mockFs.writeFileSync.mockImplementation((path, content) => {
|
|
126
|
+
writtenContent = content;
|
|
127
|
+
});
|
|
128
|
+
const beforeTime = new Date().toISOString();
|
|
129
|
+
const { addHistory } = await resetHistoryModule();
|
|
130
|
+
addHistory({
|
|
131
|
+
userPrompt: '测试',
|
|
132
|
+
command: 'test',
|
|
133
|
+
executed: true,
|
|
134
|
+
exitCode: 0,
|
|
135
|
+
});
|
|
136
|
+
const afterTime = new Date().toISOString();
|
|
137
|
+
const saved = JSON.parse(writtenContent);
|
|
138
|
+
expect(saved[0].timestamp).toBeDefined();
|
|
139
|
+
expect(saved[0].timestamp >= beforeTime).toBe(true);
|
|
140
|
+
expect(saved[0].timestamp <= afterTime).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it('应该将新记录添加到开头', async () => {
|
|
143
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
144
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
145
|
+
{ userPrompt: '旧命令', command: 'old', executed: true, exitCode: 0 },
|
|
146
|
+
]));
|
|
147
|
+
let writtenContent = '';
|
|
148
|
+
mockFs.writeFileSync.mockImplementation((path, content) => {
|
|
149
|
+
writtenContent = content;
|
|
150
|
+
});
|
|
151
|
+
const { addHistory } = await resetHistoryModule();
|
|
152
|
+
addHistory({
|
|
153
|
+
userPrompt: '新命令',
|
|
154
|
+
command: 'new',
|
|
155
|
+
executed: true,
|
|
156
|
+
exitCode: 0,
|
|
157
|
+
});
|
|
158
|
+
const saved = JSON.parse(writtenContent);
|
|
159
|
+
expect(saved[0].userPrompt).toBe('新命令');
|
|
160
|
+
expect(saved[1].userPrompt).toBe('旧命令');
|
|
161
|
+
});
|
|
162
|
+
it('应该限制历史记录条数(commandHistoryLimit)', async () => {
|
|
163
|
+
mockGetConfig.mockReturnValue({ commandHistoryLimit: 3 });
|
|
164
|
+
// 已有 3 条记录
|
|
165
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
166
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
167
|
+
{ userPrompt: '1', command: 'c1', executed: true, exitCode: 0 },
|
|
168
|
+
{ userPrompt: '2', command: 'c2', executed: true, exitCode: 0 },
|
|
169
|
+
{ userPrompt: '3', command: 'c3', executed: true, exitCode: 0 },
|
|
170
|
+
]));
|
|
171
|
+
let writtenContent = '';
|
|
172
|
+
mockFs.writeFileSync.mockImplementation((path, content) => {
|
|
173
|
+
writtenContent = content;
|
|
174
|
+
});
|
|
175
|
+
const { addHistory } = await resetHistoryModule();
|
|
176
|
+
addHistory({
|
|
177
|
+
userPrompt: '新命令',
|
|
178
|
+
command: 'new',
|
|
179
|
+
executed: true,
|
|
180
|
+
exitCode: 0,
|
|
181
|
+
});
|
|
182
|
+
const saved = JSON.parse(writtenContent);
|
|
183
|
+
expect(saved.length).toBe(3); // 限制为 3 条
|
|
184
|
+
expect(saved[0].userPrompt).toBe('新命令');
|
|
185
|
+
expect(saved[2].userPrompt).toBe('2'); // 最旧的被删除
|
|
186
|
+
});
|
|
187
|
+
it('应该截断过长的输出', async () => {
|
|
188
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
189
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]));
|
|
190
|
+
let writtenContent = '';
|
|
191
|
+
mockFs.writeFileSync.mockImplementation((path, content) => {
|
|
192
|
+
writtenContent = content;
|
|
193
|
+
});
|
|
194
|
+
const longOutput = 'a'.repeat(1000); // 超过 500 字符
|
|
195
|
+
const { addHistory } = await resetHistoryModule();
|
|
196
|
+
addHistory({
|
|
197
|
+
userPrompt: '测试',
|
|
198
|
+
command: 'test',
|
|
199
|
+
executed: true,
|
|
200
|
+
exitCode: 0,
|
|
201
|
+
output: longOutput,
|
|
202
|
+
});
|
|
203
|
+
const saved = JSON.parse(writtenContent);
|
|
204
|
+
expect(saved[0].output.length).toBeLessThan(longOutput.length);
|
|
205
|
+
expect(saved[0].output).toContain('...(截断)');
|
|
206
|
+
});
|
|
207
|
+
it('应该记录 userModified 标记', async () => {
|
|
208
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
209
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]));
|
|
210
|
+
let writtenContent = '';
|
|
211
|
+
mockFs.writeFileSync.mockImplementation((path, content) => {
|
|
212
|
+
writtenContent = content;
|
|
213
|
+
});
|
|
214
|
+
const { addHistory } = await resetHistoryModule();
|
|
215
|
+
addHistory({
|
|
216
|
+
userPrompt: '查看目录',
|
|
217
|
+
aiGeneratedCommand: 'ls -la',
|
|
218
|
+
command: 'eza -la',
|
|
219
|
+
userModified: true,
|
|
220
|
+
executed: true,
|
|
221
|
+
exitCode: 0,
|
|
222
|
+
});
|
|
223
|
+
const saved = JSON.parse(writtenContent);
|
|
224
|
+
expect(saved[0].userModified).toBe(true);
|
|
225
|
+
expect(saved[0].aiGeneratedCommand).toBe('ls -la');
|
|
226
|
+
expect(saved[0].command).toBe('eza -la');
|
|
227
|
+
});
|
|
228
|
+
it('应该记录 builtin 原因', async () => {
|
|
229
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
230
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]));
|
|
231
|
+
let writtenContent = '';
|
|
232
|
+
mockFs.writeFileSync.mockImplementation((path, content) => {
|
|
233
|
+
writtenContent = content;
|
|
234
|
+
});
|
|
235
|
+
const { addHistory } = await resetHistoryModule();
|
|
236
|
+
addHistory({
|
|
237
|
+
userPrompt: '删除文件',
|
|
238
|
+
command: 'rm -rf *',
|
|
239
|
+
executed: false,
|
|
240
|
+
exitCode: null,
|
|
241
|
+
reason: 'builtin',
|
|
242
|
+
});
|
|
243
|
+
const saved = JSON.parse(writtenContent);
|
|
244
|
+
expect(saved[0].executed).toBe(false);
|
|
245
|
+
expect(saved[0].reason).toBe('builtin');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// clearHistory 测试
|
|
250
|
+
// ============================================================================
|
|
251
|
+
describe('clearHistory', () => {
|
|
252
|
+
it('应该清空历史记录', async () => {
|
|
253
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
254
|
+
let writtenContent = '';
|
|
255
|
+
mockFs.writeFileSync.mockImplementation((path, content) => {
|
|
256
|
+
writtenContent = content;
|
|
257
|
+
});
|
|
258
|
+
const { clearHistory } = await resetHistoryModule();
|
|
259
|
+
clearHistory();
|
|
260
|
+
const saved = JSON.parse(writtenContent);
|
|
261
|
+
expect(saved).toEqual([]);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// formatHistoryForAI 测试
|
|
266
|
+
// ============================================================================
|
|
267
|
+
describe('formatHistoryForAI', () => {
|
|
268
|
+
it('应该格式化历史记录为字符串', async () => {
|
|
269
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
270
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
271
|
+
{
|
|
272
|
+
userPrompt: '安装 git',
|
|
273
|
+
command: 'brew install git',
|
|
274
|
+
executed: true,
|
|
275
|
+
exitCode: 0,
|
|
276
|
+
timestamp: new Date().toISOString(),
|
|
277
|
+
},
|
|
278
|
+
]));
|
|
279
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
280
|
+
const formatted = formatHistoryForAI();
|
|
281
|
+
expect(formatted).toContain('安装 git');
|
|
282
|
+
expect(formatted).toContain('brew install git');
|
|
283
|
+
expect(formatted).toContain('✓');
|
|
284
|
+
});
|
|
285
|
+
it('空历史应该返回空字符串', async () => {
|
|
286
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
287
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]));
|
|
288
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
289
|
+
const formatted = formatHistoryForAI();
|
|
290
|
+
expect(formatted).toBe('');
|
|
291
|
+
});
|
|
292
|
+
it('失败命令应该显示退出码', async () => {
|
|
293
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
294
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
295
|
+
{
|
|
296
|
+
userPrompt: '测试',
|
|
297
|
+
command: 'false',
|
|
298
|
+
executed: true,
|
|
299
|
+
exitCode: 1,
|
|
300
|
+
timestamp: new Date().toISOString(),
|
|
301
|
+
},
|
|
302
|
+
]));
|
|
303
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
304
|
+
const formatted = formatHistoryForAI();
|
|
305
|
+
expect(formatted).toContain('✗');
|
|
306
|
+
expect(formatted).toContain('退出码:1');
|
|
307
|
+
});
|
|
308
|
+
it('builtin 命令应该标记未执行', async () => {
|
|
309
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
310
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
311
|
+
{
|
|
312
|
+
userPrompt: '删除文件',
|
|
313
|
+
command: 'rm -rf *',
|
|
314
|
+
executed: false,
|
|
315
|
+
exitCode: null,
|
|
316
|
+
reason: 'builtin',
|
|
317
|
+
timestamp: new Date().toISOString(),
|
|
318
|
+
},
|
|
319
|
+
]));
|
|
320
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
321
|
+
const formatted = formatHistoryForAI();
|
|
322
|
+
expect(formatted).toContain('builtin');
|
|
323
|
+
expect(formatted).toContain('未执行');
|
|
324
|
+
});
|
|
325
|
+
it('用户修改的命令应该显示 AI 生成和用户修改', async () => {
|
|
326
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
327
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
328
|
+
{
|
|
329
|
+
userPrompt: '查看目录',
|
|
330
|
+
aiGeneratedCommand: 'ls -la',
|
|
331
|
+
command: 'eza -la',
|
|
332
|
+
userModified: true,
|
|
333
|
+
executed: true,
|
|
334
|
+
exitCode: 0,
|
|
335
|
+
timestamp: new Date().toISOString(),
|
|
336
|
+
},
|
|
337
|
+
]));
|
|
338
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
339
|
+
const formatted = formatHistoryForAI();
|
|
340
|
+
expect(formatted).toContain('AI 生成');
|
|
341
|
+
expect(formatted).toContain('ls -la');
|
|
342
|
+
expect(formatted).toContain('用户修改');
|
|
343
|
+
expect(formatted).toContain('eza -la');
|
|
344
|
+
});
|
|
345
|
+
it('用户取消的命令应该正确标记', async () => {
|
|
346
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
347
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
348
|
+
{
|
|
349
|
+
userPrompt: '测试',
|
|
350
|
+
command: 'test',
|
|
351
|
+
executed: false,
|
|
352
|
+
exitCode: null,
|
|
353
|
+
timestamp: new Date().toISOString(),
|
|
354
|
+
},
|
|
355
|
+
]));
|
|
356
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
357
|
+
const formatted = formatHistoryForAI();
|
|
358
|
+
expect(formatted).toContain('用户取消');
|
|
359
|
+
});
|
|
360
|
+
it('应该包含标题', async () => {
|
|
361
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
362
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
363
|
+
{
|
|
364
|
+
userPrompt: '测试',
|
|
365
|
+
command: 'test',
|
|
366
|
+
executed: true,
|
|
367
|
+
exitCode: 0,
|
|
368
|
+
timestamp: new Date().toISOString(),
|
|
369
|
+
},
|
|
370
|
+
]));
|
|
371
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
372
|
+
const formatted = formatHistoryForAI();
|
|
373
|
+
expect(formatted).toContain('最近通过 pls 执行的命令');
|
|
374
|
+
});
|
|
375
|
+
it('失败命令应该附加输出摘要', async () => {
|
|
376
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
377
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
378
|
+
{
|
|
379
|
+
userPrompt: '测试',
|
|
380
|
+
command: 'false',
|
|
381
|
+
executed: true,
|
|
382
|
+
exitCode: 1,
|
|
383
|
+
output: 'Error: command failed\nsome details',
|
|
384
|
+
timestamp: new Date().toISOString(),
|
|
385
|
+
},
|
|
386
|
+
]));
|
|
387
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
388
|
+
const formatted = formatHistoryForAI();
|
|
389
|
+
expect(formatted).toContain('输出:');
|
|
390
|
+
expect(formatted).toContain('Error: command failed');
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
// ============================================================================
|
|
394
|
+
// getHistoryFilePath 测试
|
|
395
|
+
// ============================================================================
|
|
396
|
+
describe('getHistoryFilePath', () => {
|
|
397
|
+
it('应该返回正确的历史文件路径', async () => {
|
|
398
|
+
mockOs.homedir.mockReturnValue('/home/testuser');
|
|
399
|
+
const { getHistoryFilePath } = await resetHistoryModule();
|
|
400
|
+
const path = getHistoryFilePath();
|
|
401
|
+
expect(path).toContain('.please');
|
|
402
|
+
expect(path).toContain('history.json');
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
// ============================================================================
|
|
406
|
+
// 时间显示测试
|
|
407
|
+
// ============================================================================
|
|
408
|
+
describe('时间显示', () => {
|
|
409
|
+
it('刚刚执行的命令应该显示"刚刚"', async () => {
|
|
410
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
411
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
412
|
+
{
|
|
413
|
+
userPrompt: '测试',
|
|
414
|
+
command: 'test',
|
|
415
|
+
executed: true,
|
|
416
|
+
exitCode: 0,
|
|
417
|
+
timestamp: new Date().toISOString(),
|
|
418
|
+
},
|
|
419
|
+
]));
|
|
420
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
421
|
+
const formatted = formatHistoryForAI();
|
|
422
|
+
expect(formatted).toContain('刚刚');
|
|
423
|
+
});
|
|
424
|
+
it('没有时间戳的记录应该显示"未知"', async () => {
|
|
425
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
426
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([
|
|
427
|
+
{
|
|
428
|
+
userPrompt: '测试',
|
|
429
|
+
command: 'test',
|
|
430
|
+
executed: true,
|
|
431
|
+
exitCode: 0,
|
|
432
|
+
// 没有 timestamp
|
|
433
|
+
},
|
|
434
|
+
]));
|
|
435
|
+
const { formatHistoryForAI } = await resetHistoryModule();
|
|
436
|
+
const formatted = formatHistoryForAI();
|
|
437
|
+
expect(formatted).toContain('未知');
|
|
438
|
+
});
|
|
439
|
+
});
|