@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,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 错误恢复集成测试
|
|
3
|
+
* 测试各种错误场景下的恢复机制:文件损坏、权限错误、网络超时、SSH断连等
|
|
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
|
+
rmSync: vi.fn(),
|
|
25
|
+
statSync: vi.fn(),
|
|
26
|
+
copyFileSync: vi.fn(),
|
|
27
|
+
},
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
// Mock os 模块
|
|
31
|
+
vi.mock('os', () => ({
|
|
32
|
+
default: {
|
|
33
|
+
homedir: vi.fn(() => '/home/testuser'),
|
|
34
|
+
platform: vi.fn(() => 'linux'),
|
|
35
|
+
type: vi.fn(() => 'Linux'),
|
|
36
|
+
release: vi.fn(() => '5.4.0'),
|
|
37
|
+
arch: vi.fn(() => 'x64'),
|
|
38
|
+
userInfo: vi.fn(() => ({ username: 'testuser' })),
|
|
39
|
+
hostname: vi.fn(() => 'testhost'),
|
|
40
|
+
},
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
// Mock config 模块
|
|
44
|
+
const mockConfig = {
|
|
45
|
+
aliases: {},
|
|
46
|
+
commandHistoryLimit: 50,
|
|
47
|
+
shellHistoryLimit: 20,
|
|
48
|
+
shellHook: false,
|
|
49
|
+
remotes: {},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
vi.mock('../config.js', () => ({
|
|
53
|
+
getConfig: vi.fn(() => mockConfig),
|
|
54
|
+
saveConfig: vi.fn(),
|
|
55
|
+
CONFIG_DIR: '/home/testuser/.please',
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
// Mock remote 模块
|
|
59
|
+
vi.mock('../remote.js', () => ({
|
|
60
|
+
sshExec: vi.fn(),
|
|
61
|
+
getRemote: vi.fn(),
|
|
62
|
+
getRemotes: vi.fn(() => ({})),
|
|
63
|
+
testRemoteConnection: vi.fn(),
|
|
64
|
+
collectRemoteSysInfo: vi.fn(),
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
// Mock theme 模块
|
|
68
|
+
vi.mock('../ui/theme.js', () => ({
|
|
69
|
+
getCurrentTheme: vi.fn(() => ({
|
|
70
|
+
primary: '#007acc',
|
|
71
|
+
secondary: '#6c757d',
|
|
72
|
+
success: '#4caf50',
|
|
73
|
+
error: '#f44336',
|
|
74
|
+
warning: '#ff9800',
|
|
75
|
+
text: { muted: '#666666' },
|
|
76
|
+
})),
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
// Mock chalk
|
|
80
|
+
vi.mock('chalk', () => ({
|
|
81
|
+
default: {
|
|
82
|
+
bold: vi.fn((s: string) => s),
|
|
83
|
+
gray: vi.fn((s: string) => s),
|
|
84
|
+
dim: vi.fn((s: string) => s),
|
|
85
|
+
hex: vi.fn(() => (s: string) => s),
|
|
86
|
+
green: vi.fn((s: string) => s),
|
|
87
|
+
red: vi.fn((s: string) => s),
|
|
88
|
+
yellow: vi.fn((s: string) => s),
|
|
89
|
+
},
|
|
90
|
+
}))
|
|
91
|
+
|
|
92
|
+
import fs from 'fs'
|
|
93
|
+
import os from 'os'
|
|
94
|
+
import { spawn } from 'child_process'
|
|
95
|
+
import { getConfig, saveConfig } from '../config.js'
|
|
96
|
+
import { sshExec, getRemote, testRemoteConnection } from '../remote.js'
|
|
97
|
+
|
|
98
|
+
const mockFs = vi.mocked(fs)
|
|
99
|
+
const mockOs = vi.mocked(os)
|
|
100
|
+
const mockSpawn = vi.mocked(spawn)
|
|
101
|
+
const mockGetConfig = vi.mocked(getConfig)
|
|
102
|
+
const mockSaveConfig = vi.mocked(saveConfig)
|
|
103
|
+
const mockSshExec = vi.mocked(sshExec)
|
|
104
|
+
const mockGetRemote = vi.mocked(getRemote)
|
|
105
|
+
const mockTestRemoteConnection = vi.mocked(testRemoteConnection)
|
|
106
|
+
|
|
107
|
+
// 创建 mock child process
|
|
108
|
+
function createMockChildProcess(options: {
|
|
109
|
+
stdout?: string
|
|
110
|
+
stderr?: string
|
|
111
|
+
exitCode?: number
|
|
112
|
+
error?: Error
|
|
113
|
+
}) {
|
|
114
|
+
const stdoutCallbacks: ((data: Buffer) => void)[] = []
|
|
115
|
+
const stderrCallbacks: ((data: Buffer) => void)[] = []
|
|
116
|
+
const closeCallbacks: ((code: number) => void)[] = []
|
|
117
|
+
const errorCallbacks: ((err: Error) => void)[] = []
|
|
118
|
+
|
|
119
|
+
const mockChild = {
|
|
120
|
+
stdout: {
|
|
121
|
+
on: vi.fn((event: string, cb: (data: Buffer) => void) => {
|
|
122
|
+
if (event === 'data') stdoutCallbacks.push(cb)
|
|
123
|
+
}),
|
|
124
|
+
},
|
|
125
|
+
stderr: {
|
|
126
|
+
on: vi.fn((event: string, cb: (data: Buffer) => void) => {
|
|
127
|
+
if (event === 'data') stderrCallbacks.push(cb)
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
stdin: {
|
|
131
|
+
write: vi.fn(),
|
|
132
|
+
end: vi.fn(),
|
|
133
|
+
},
|
|
134
|
+
on: vi.fn((event: string, cb: any) => {
|
|
135
|
+
if (event === 'close') closeCallbacks.push(cb)
|
|
136
|
+
if (event === 'error') errorCallbacks.push(cb)
|
|
137
|
+
}),
|
|
138
|
+
kill: vi.fn(),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 模拟异步执行
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
if (options.error) {
|
|
144
|
+
errorCallbacks.forEach(cb => cb(options.error!))
|
|
145
|
+
} else {
|
|
146
|
+
if (options.stdout) {
|
|
147
|
+
stdoutCallbacks.forEach(cb => cb(Buffer.from(options.stdout!)))
|
|
148
|
+
}
|
|
149
|
+
if (options.stderr) {
|
|
150
|
+
stderrCallbacks.forEach(cb => cb(Buffer.from(options.stderr!)))
|
|
151
|
+
}
|
|
152
|
+
closeCallbacks.forEach(cb => cb(options.exitCode ?? 0))
|
|
153
|
+
}
|
|
154
|
+
}, 10)
|
|
155
|
+
|
|
156
|
+
return mockChild
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 重置模块辅助函数
|
|
160
|
+
async function resetModules() {
|
|
161
|
+
vi.resetModules()
|
|
162
|
+
return {
|
|
163
|
+
config: await import('../config.js'),
|
|
164
|
+
history: await import('../history.js'),
|
|
165
|
+
shellHook: await import('../shell-hook.js'),
|
|
166
|
+
remoteHistory: await import('../remote-history.js'),
|
|
167
|
+
chatHistory: await import('../chat-history.js'),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
beforeEach(() => {
|
|
172
|
+
vi.clearAllMocks()
|
|
173
|
+
mockOs.homedir.mockReturnValue('/home/testuser')
|
|
174
|
+
mockOs.platform.mockReturnValue('linux')
|
|
175
|
+
mockFs.mkdirSync.mockImplementation(() => undefined)
|
|
176
|
+
mockFs.writeFileSync.mockImplementation(() => {})
|
|
177
|
+
mockSaveConfig.mockImplementation(() => {})
|
|
178
|
+
|
|
179
|
+
// 重置配置
|
|
180
|
+
Object.assign(mockConfig, {
|
|
181
|
+
aliases: {},
|
|
182
|
+
commandHistoryLimit: 50,
|
|
183
|
+
shellHistoryLimit: 20,
|
|
184
|
+
shellHook: false,
|
|
185
|
+
remotes: {},
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
afterEach(() => {
|
|
190
|
+
vi.restoreAllMocks()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// 配置文件损坏恢复测试
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
describe('配置文件损坏恢复', () => {
|
|
198
|
+
it('JSON 损坏时应该返回默认配置', async () => {
|
|
199
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
200
|
+
mockFs.readFileSync.mockReturnValue('{invalid json content')
|
|
201
|
+
|
|
202
|
+
const { history } = await resetModules()
|
|
203
|
+
const historyData = history.getHistory()
|
|
204
|
+
|
|
205
|
+
// 应该返回空数组而不是抛出错误
|
|
206
|
+
expect(historyData).toEqual([])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('历史文件部分损坏时应该返回有效部分', async () => {
|
|
210
|
+
mockConfig.shellHook = true
|
|
211
|
+
const partiallyCorruptedJsonl = `{"cmd":"valid1","exit":0,"time":"2024-01-01"}
|
|
212
|
+
{invalid line
|
|
213
|
+
{"cmd":"valid2","exit":0,"time":"2024-01-01"}`
|
|
214
|
+
|
|
215
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
216
|
+
mockFs.readFileSync.mockReturnValue(partiallyCorruptedJsonl)
|
|
217
|
+
|
|
218
|
+
const { shellHook } = await resetModules()
|
|
219
|
+
const history = shellHook.getShellHistory()
|
|
220
|
+
|
|
221
|
+
// 应该返回有效的记录,跳过损坏的行
|
|
222
|
+
expect(history.length).toBe(2)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('聊天历史损坏时应该返回空数组', async () => {
|
|
226
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
227
|
+
mockFs.readFileSync.mockReturnValue('not valid json')
|
|
228
|
+
|
|
229
|
+
const { chatHistory } = await resetModules()
|
|
230
|
+
const history = chatHistory.getChatHistory()
|
|
231
|
+
|
|
232
|
+
expect(history).toEqual([])
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('远程历史损坏时应该返回空数组', async () => {
|
|
236
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
237
|
+
mockFs.readFileSync.mockReturnValue('{broken')
|
|
238
|
+
|
|
239
|
+
const { remoteHistory } = await resetModules()
|
|
240
|
+
const history = remoteHistory.getRemoteHistory('server1')
|
|
241
|
+
|
|
242
|
+
expect(history).toEqual([])
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// 权限错误处理测试
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
describe('权限错误处理', () => {
|
|
251
|
+
it('历史文件无读取权限时应该返回空数组', async () => {
|
|
252
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
253
|
+
mockFs.readFileSync.mockImplementation(() => {
|
|
254
|
+
const error = new Error('EACCES: permission denied') as NodeJS.ErrnoException
|
|
255
|
+
error.code = 'EACCES'
|
|
256
|
+
throw error
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const { history } = await resetModules()
|
|
260
|
+
const historyData = history.getHistory()
|
|
261
|
+
|
|
262
|
+
expect(historyData).toEqual([])
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('配置目录无写入权限时应该抛出错误', async () => {
|
|
266
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
267
|
+
mockFs.mkdirSync.mockImplementation(() => {
|
|
268
|
+
const error = new Error('EACCES: permission denied') as NodeJS.ErrnoException
|
|
269
|
+
error.code = 'EACCES'
|
|
270
|
+
throw error
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const { history } = await resetModules()
|
|
274
|
+
|
|
275
|
+
// 尝试添加历史记录时应该处理权限错误
|
|
276
|
+
expect(() => history.addHistory({
|
|
277
|
+
userPrompt: 'test',
|
|
278
|
+
command: 'test',
|
|
279
|
+
executed: true,
|
|
280
|
+
exitCode: 0,
|
|
281
|
+
output: '',
|
|
282
|
+
})).toThrow()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('历史文件无写入权限时应该抛出错误', async () => {
|
|
286
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
287
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]))
|
|
288
|
+
mockFs.writeFileSync.mockImplementation(() => {
|
|
289
|
+
const error = new Error('EACCES: permission denied') as NodeJS.ErrnoException
|
|
290
|
+
error.code = 'EACCES'
|
|
291
|
+
throw error
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const { history } = await resetModules()
|
|
295
|
+
|
|
296
|
+
expect(() => history.addHistory({
|
|
297
|
+
userPrompt: 'test',
|
|
298
|
+
command: 'test',
|
|
299
|
+
executed: true,
|
|
300
|
+
exitCode: 0,
|
|
301
|
+
output: '',
|
|
302
|
+
})).toThrow()
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// ============================================================================
|
|
307
|
+
// 文件不存在处理测试
|
|
308
|
+
// ============================================================================
|
|
309
|
+
|
|
310
|
+
describe('文件不存在处理', () => {
|
|
311
|
+
it('历史文件不存在时应该返回空数组', async () => {
|
|
312
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
313
|
+
|
|
314
|
+
const { history } = await resetModules()
|
|
315
|
+
const historyData = history.getHistory()
|
|
316
|
+
|
|
317
|
+
expect(historyData).toEqual([])
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('Shell 历史文件不存在时应该返回空数组', async () => {
|
|
321
|
+
mockConfig.shellHook = true
|
|
322
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
323
|
+
|
|
324
|
+
const { shellHook } = await resetModules()
|
|
325
|
+
const history = shellHook.getShellHistory()
|
|
326
|
+
|
|
327
|
+
expect(history).toEqual([])
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('添加历史时应该自动创建配置目录', async () => {
|
|
331
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
332
|
+
mockFs.readFileSync.mockReturnValue('[]')
|
|
333
|
+
|
|
334
|
+
const { history } = await resetModules()
|
|
335
|
+
history.addHistory({
|
|
336
|
+
userPrompt: 'test',
|
|
337
|
+
command: 'test',
|
|
338
|
+
executed: true,
|
|
339
|
+
exitCode: 0,
|
|
340
|
+
output: '',
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
expect(mockFs.mkdirSync).toHaveBeenCalled()
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('远程历史文件不存在时应该返回空数组', async () => {
|
|
347
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
348
|
+
|
|
349
|
+
const { remoteHistory } = await resetModules()
|
|
350
|
+
const history = remoteHistory.getRemoteHistory('server1')
|
|
351
|
+
|
|
352
|
+
expect(history).toEqual([])
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// SSH 连接错误处理测试
|
|
358
|
+
// ============================================================================
|
|
359
|
+
|
|
360
|
+
describe('SSH 连接错误处理', () => {
|
|
361
|
+
it('SSH 连接超时应该返回错误', async () => {
|
|
362
|
+
mockGetRemote.mockReturnValue({
|
|
363
|
+
host: '192.168.1.100',
|
|
364
|
+
user: 'root',
|
|
365
|
+
port: 22,
|
|
366
|
+
} as any)
|
|
367
|
+
mockSshExec.mockRejectedValue(new Error('ETIMEDOUT'))
|
|
368
|
+
|
|
369
|
+
const { remoteHistory } = await resetModules()
|
|
370
|
+
|
|
371
|
+
// fetchRemoteShellHistory 应该优雅处理超时
|
|
372
|
+
const history = await remoteHistory.fetchRemoteShellHistory('server1')
|
|
373
|
+
|
|
374
|
+
// 应该返回空数组或缓存数据
|
|
375
|
+
expect(Array.isArray(history)).toBe(true)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('SSH 认证失败应该返回错误', async () => {
|
|
379
|
+
mockGetRemote.mockReturnValue({
|
|
380
|
+
host: '192.168.1.100',
|
|
381
|
+
user: 'root',
|
|
382
|
+
port: 22,
|
|
383
|
+
} as any)
|
|
384
|
+
mockSshExec.mockRejectedValue(new Error('Permission denied (publickey)'))
|
|
385
|
+
|
|
386
|
+
const { remoteHistory } = await resetModules()
|
|
387
|
+
const history = await remoteHistory.fetchRemoteShellHistory('server1')
|
|
388
|
+
|
|
389
|
+
expect(Array.isArray(history)).toBe(true)
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('SSH 命令执行失败应该返回本地缓存', async () => {
|
|
393
|
+
mockGetRemote.mockReturnValue({
|
|
394
|
+
host: '192.168.1.100',
|
|
395
|
+
user: 'root',
|
|
396
|
+
port: 22,
|
|
397
|
+
} as any)
|
|
398
|
+
mockSshExec.mockRejectedValue(new Error('Connection refused'))
|
|
399
|
+
|
|
400
|
+
// 设置本地缓存
|
|
401
|
+
const cachedHistory = '{"cmd":"cached","exit":0,"time":"2024-01-01"}'
|
|
402
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
403
|
+
mockFs.readFileSync.mockReturnValue(cachedHistory)
|
|
404
|
+
|
|
405
|
+
const { remoteHistory } = await resetModules()
|
|
406
|
+
const history = await remoteHistory.fetchRemoteShellHistory('server1')
|
|
407
|
+
|
|
408
|
+
expect(history.length).toBe(1)
|
|
409
|
+
expect(history[0].cmd).toBe('cached')
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
// ============================================================================
|
|
414
|
+
// 空数据处理测试
|
|
415
|
+
// ============================================================================
|
|
416
|
+
|
|
417
|
+
describe('空数据处理', () => {
|
|
418
|
+
it('格式化空历史应该返回空字符串', async () => {
|
|
419
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
420
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]))
|
|
421
|
+
|
|
422
|
+
const { history } = await resetModules()
|
|
423
|
+
const formatted = history.formatHistoryForAI()
|
|
424
|
+
|
|
425
|
+
expect(formatted).toBe('')
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('格式化空 Shell 历史应该返回空字符串', async () => {
|
|
429
|
+
mockConfig.shellHook = true
|
|
430
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
431
|
+
mockFs.readFileSync.mockReturnValue('')
|
|
432
|
+
|
|
433
|
+
const { shellHook } = await resetModules()
|
|
434
|
+
const formatted = shellHook.formatShellHistoryForAI()
|
|
435
|
+
|
|
436
|
+
expect(formatted).toBe('')
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('格式化空远程历史应该返回空字符串', async () => {
|
|
440
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
441
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]))
|
|
442
|
+
|
|
443
|
+
const { remoteHistory } = await resetModules()
|
|
444
|
+
const formatted = remoteHistory.formatRemoteHistoryForAI('server1')
|
|
445
|
+
|
|
446
|
+
expect(formatted).toBe('')
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('格式化空远程 Shell 历史应该返回空字符串', async () => {
|
|
450
|
+
const { remoteHistory } = await resetModules()
|
|
451
|
+
const formatted = remoteHistory.formatRemoteShellHistoryForAI([])
|
|
452
|
+
|
|
453
|
+
expect(formatted).toBe('')
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
// ============================================================================
|
|
458
|
+
// 边界情况处理测试
|
|
459
|
+
// ============================================================================
|
|
460
|
+
|
|
461
|
+
describe('边界情况处理', () => {
|
|
462
|
+
it('超长命令应该正常保存', async () => {
|
|
463
|
+
const longCommand = 'echo ' + 'x'.repeat(10000)
|
|
464
|
+
|
|
465
|
+
let savedHistory: any[] = []
|
|
466
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
467
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]))
|
|
468
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
469
|
+
if (path.toString().includes('history.json')) {
|
|
470
|
+
savedHistory = JSON.parse(content)
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
const { history } = await resetModules()
|
|
475
|
+
history.addHistory({
|
|
476
|
+
userPrompt: '测试长命令',
|
|
477
|
+
command: longCommand,
|
|
478
|
+
executed: true,
|
|
479
|
+
exitCode: 0,
|
|
480
|
+
output: '',
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
expect(savedHistory[0].command).toBe(longCommand)
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it('特殊字符命令应该正常保存', async () => {
|
|
487
|
+
const specialCommand = 'echo "hello\\nworld" | grep \'test\' && rm -rf /tmp/*'
|
|
488
|
+
|
|
489
|
+
let savedHistory: any[] = []
|
|
490
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
491
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]))
|
|
492
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
493
|
+
if (path.toString().includes('history.json')) {
|
|
494
|
+
savedHistory = JSON.parse(content)
|
|
495
|
+
}
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
const { history } = await resetModules()
|
|
499
|
+
history.addHistory({
|
|
500
|
+
userPrompt: '测试特殊字符',
|
|
501
|
+
command: specialCommand,
|
|
502
|
+
executed: true,
|
|
503
|
+
exitCode: 0,
|
|
504
|
+
output: '',
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
expect(savedHistory[0].command).toBe(specialCommand)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it('Unicode 字符应该正常处理', async () => {
|
|
511
|
+
const unicodePrompt = '检查中文路径 /home/用户/文档'
|
|
512
|
+
const unicodeCommand = 'ls /home/用户/文档'
|
|
513
|
+
|
|
514
|
+
let savedHistory: any[] = []
|
|
515
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
516
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]))
|
|
517
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
518
|
+
if (path.toString().includes('history.json')) {
|
|
519
|
+
savedHistory = JSON.parse(content)
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
const { history } = await resetModules()
|
|
524
|
+
history.addHistory({
|
|
525
|
+
userPrompt: unicodePrompt,
|
|
526
|
+
command: unicodeCommand,
|
|
527
|
+
executed: true,
|
|
528
|
+
exitCode: 0,
|
|
529
|
+
output: '',
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
expect(savedHistory[0].userPrompt).toBe(unicodePrompt)
|
|
533
|
+
expect(savedHistory[0].command).toBe(unicodeCommand)
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('空 userPrompt 应该正常处理', async () => {
|
|
537
|
+
let savedHistory: any[] = []
|
|
538
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
539
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]))
|
|
540
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
541
|
+
if (path.toString().includes('history.json')) {
|
|
542
|
+
savedHistory = JSON.parse(content)
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
const { history } = await resetModules()
|
|
547
|
+
history.addHistory({
|
|
548
|
+
userPrompt: '',
|
|
549
|
+
command: 'ls',
|
|
550
|
+
executed: true,
|
|
551
|
+
exitCode: 0,
|
|
552
|
+
output: '',
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
expect(savedHistory[0].userPrompt).toBe('')
|
|
556
|
+
expect(savedHistory[0].command).toBe('ls')
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('null exitCode 应该正常处理', async () => {
|
|
560
|
+
let savedHistory: any[] = []
|
|
561
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
562
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify([]))
|
|
563
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
564
|
+
if (path.toString().includes('history.json')) {
|
|
565
|
+
savedHistory = JSON.parse(content)
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
const { history } = await resetModules()
|
|
570
|
+
history.addHistory({
|
|
571
|
+
userPrompt: '测试',
|
|
572
|
+
command: 'test',
|
|
573
|
+
executed: false,
|
|
574
|
+
exitCode: null,
|
|
575
|
+
output: '',
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
expect(savedHistory[0].exitCode).toBeNull()
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
// ============================================================================
|
|
583
|
+
// 清理操作测试
|
|
584
|
+
// ============================================================================
|
|
585
|
+
|
|
586
|
+
describe('清理操作', () => {
|
|
587
|
+
it('clearHistory 应该清空历史文件', async () => {
|
|
588
|
+
let writtenContent = ''
|
|
589
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
590
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
591
|
+
writtenContent = content
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
const { history } = await resetModules()
|
|
595
|
+
history.clearHistory()
|
|
596
|
+
|
|
597
|
+
expect(JSON.parse(writtenContent)).toEqual([])
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
it('clearChatHistory 应该清空聊天历史', async () => {
|
|
601
|
+
let writtenContent = ''
|
|
602
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
603
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
604
|
+
writtenContent = content
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
const { chatHistory } = await resetModules()
|
|
608
|
+
chatHistory.clearChatHistory()
|
|
609
|
+
|
|
610
|
+
expect(JSON.parse(writtenContent)).toEqual([])
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('clearRemoteHistory 文件不存在时不应该报错', async () => {
|
|
614
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
615
|
+
|
|
616
|
+
const { remoteHistory } = await resetModules()
|
|
617
|
+
|
|
618
|
+
expect(() => remoteHistory.clearRemoteHistory('server1')).not.toThrow()
|
|
619
|
+
})
|
|
620
|
+
})
|