@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.
- package/README.md +30 -2
- 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,1043 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置管理模块测试
|
|
3
|
+
* 测试配置读写、API Key 处理、配置验证等功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
7
|
+
import {
|
|
8
|
+
validConfig,
|
|
9
|
+
minimalConfig,
|
|
10
|
+
configWithoutApiKey,
|
|
11
|
+
corruptedConfigJson,
|
|
12
|
+
emptyConfig,
|
|
13
|
+
legacyConfig,
|
|
14
|
+
configWithExtraFields,
|
|
15
|
+
} from '../../tests/fixtures/config'
|
|
16
|
+
|
|
17
|
+
// Mock fs 模块
|
|
18
|
+
vi.mock('fs', () => ({
|
|
19
|
+
default: {
|
|
20
|
+
existsSync: vi.fn(),
|
|
21
|
+
readFileSync: vi.fn(),
|
|
22
|
+
writeFileSync: vi.fn(),
|
|
23
|
+
mkdirSync: vi.fn(),
|
|
24
|
+
},
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
// Mock os 模块
|
|
28
|
+
vi.mock('os', () => ({
|
|
29
|
+
default: {
|
|
30
|
+
homedir: vi.fn(() => '/home/testuser'),
|
|
31
|
+
},
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
// Mock theme 模块
|
|
35
|
+
vi.mock('../ui/theme.js', () => ({
|
|
36
|
+
getCurrentTheme: vi.fn(() => ({
|
|
37
|
+
primary: '#007acc',
|
|
38
|
+
secondary: '#6c757d',
|
|
39
|
+
success: '#4caf50',
|
|
40
|
+
error: '#f44336',
|
|
41
|
+
warning: '#ff9800',
|
|
42
|
+
})),
|
|
43
|
+
isValidTheme: vi.fn((theme: string) => ['dark', 'light', 'dracula'].includes(theme)),
|
|
44
|
+
getAllThemeMetadata: vi.fn(() => [
|
|
45
|
+
{ name: 'dark', displayName: 'Dark' },
|
|
46
|
+
{ name: 'light', displayName: 'Light' },
|
|
47
|
+
{ name: 'dracula', displayName: 'Dracula' },
|
|
48
|
+
]),
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
// Mock chalk
|
|
52
|
+
vi.mock('chalk', () => ({
|
|
53
|
+
default: {
|
|
54
|
+
bold: Object.assign(vi.fn((s: string) => s), {
|
|
55
|
+
hex: vi.fn(() => (s: string) => s),
|
|
56
|
+
}),
|
|
57
|
+
gray: vi.fn((s: string) => s),
|
|
58
|
+
hex: vi.fn(() => (s: string) => s),
|
|
59
|
+
},
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
// Mock readline 模块
|
|
63
|
+
vi.mock('readline', () => ({
|
|
64
|
+
default: {
|
|
65
|
+
createInterface: vi.fn(() => ({
|
|
66
|
+
question: vi.fn((prompt: string, callback: (answer: string) => void) => callback('')),
|
|
67
|
+
close: vi.fn(),
|
|
68
|
+
})),
|
|
69
|
+
},
|
|
70
|
+
createInterface: vi.fn(() => ({
|
|
71
|
+
question: vi.fn((prompt: string, callback: (answer: string) => void) => callback('')),
|
|
72
|
+
close: vi.fn(),
|
|
73
|
+
})),
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
import fs from 'fs'
|
|
77
|
+
import os from 'os'
|
|
78
|
+
import readline from 'readline'
|
|
79
|
+
import { isValidTheme } from '../ui/theme.js'
|
|
80
|
+
|
|
81
|
+
// 获取 mock 函数引用
|
|
82
|
+
const mockFs = vi.mocked(fs)
|
|
83
|
+
const mockOs = vi.mocked(os)
|
|
84
|
+
const mockReadline = vi.mocked(readline)
|
|
85
|
+
const mockIsValidTheme = vi.mocked(isValidTheme)
|
|
86
|
+
|
|
87
|
+
// 模块状态重置辅助
|
|
88
|
+
async function resetConfigModule() {
|
|
89
|
+
// 重新导入模块以重置缓存
|
|
90
|
+
vi.resetModules()
|
|
91
|
+
return await import('../config.js')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
vi.clearAllMocks()
|
|
96
|
+
mockOs.homedir.mockReturnValue('/home/testuser')
|
|
97
|
+
// 默认配置目录存在
|
|
98
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
afterEach(() => {
|
|
102
|
+
vi.restoreAllMocks()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// 配置读取测试
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
describe('getConfig', () => {
|
|
110
|
+
it('首次调用应该返回默认配置(配置文件不存在)', async () => {
|
|
111
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
112
|
+
|
|
113
|
+
const { getConfig } = await resetConfigModule()
|
|
114
|
+
const config = getConfig()
|
|
115
|
+
|
|
116
|
+
expect(config.provider).toBe('openai')
|
|
117
|
+
expect(config.model).toBe('gpt-4-turbo')
|
|
118
|
+
expect(config.shellHook).toBe(false)
|
|
119
|
+
expect(config.shellHistoryLimit).toBe(10)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('应该从配置文件读取', async () => {
|
|
123
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
124
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
125
|
+
|
|
126
|
+
const { getConfig } = await resetConfigModule()
|
|
127
|
+
const config = getConfig()
|
|
128
|
+
|
|
129
|
+
expect(config.apiKey).toBe('sk-1234567890abcdef')
|
|
130
|
+
expect(config.provider).toBe('openai')
|
|
131
|
+
expect(config.model).toBe('gpt-4')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('JSON 损坏时应该返回默认配置', async () => {
|
|
135
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
136
|
+
mockFs.readFileSync.mockReturnValue(corruptedConfigJson)
|
|
137
|
+
|
|
138
|
+
const { getConfig } = await resetConfigModule()
|
|
139
|
+
const config = getConfig()
|
|
140
|
+
|
|
141
|
+
expect(config.provider).toBe('openai')
|
|
142
|
+
expect(config.model).toBe('gpt-4-turbo')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('缺少字段时应该合并默认值', async () => {
|
|
146
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
147
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(minimalConfig))
|
|
148
|
+
|
|
149
|
+
const { getConfig } = await resetConfigModule()
|
|
150
|
+
const config = getConfig()
|
|
151
|
+
|
|
152
|
+
expect(config.apiKey).toBe('sk-test123456')
|
|
153
|
+
expect(config.provider).toBe('openai') // 默认值
|
|
154
|
+
expect(config.shellHistoryLimit).toBe(10) // 默认值
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('多余字段应该保留', async () => {
|
|
158
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
159
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(configWithExtraFields))
|
|
160
|
+
|
|
161
|
+
const { getConfig } = await resetConfigModule()
|
|
162
|
+
const config = getConfig()
|
|
163
|
+
|
|
164
|
+
expect((config as any).unknownField).toBe('should be preserved')
|
|
165
|
+
expect((config as any).futureFeature).toBe(true)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('应该有配置缓存机制(多次调用不重复读取文件)', async () => {
|
|
169
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
170
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
171
|
+
|
|
172
|
+
const { getConfig } = await resetConfigModule()
|
|
173
|
+
|
|
174
|
+
getConfig()
|
|
175
|
+
getConfig()
|
|
176
|
+
getConfig()
|
|
177
|
+
|
|
178
|
+
// 只应该读取一次
|
|
179
|
+
expect(mockFs.readFileSync).toHaveBeenCalledTimes(1)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('空配置文件应该返回默认值', async () => {
|
|
183
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
184
|
+
mockFs.readFileSync.mockReturnValue(emptyConfig)
|
|
185
|
+
|
|
186
|
+
const { getConfig } = await resetConfigModule()
|
|
187
|
+
const config = getConfig()
|
|
188
|
+
|
|
189
|
+
expect(config.provider).toBe('openai')
|
|
190
|
+
expect(config.shellHistoryLimit).toBe(10)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('旧版本配置应该兼容', async () => {
|
|
194
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
195
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(legacyConfig))
|
|
196
|
+
|
|
197
|
+
const { getConfig } = await resetConfigModule()
|
|
198
|
+
const config = getConfig()
|
|
199
|
+
|
|
200
|
+
expect(config.apiKey).toBe('sk-legacy-key')
|
|
201
|
+
expect(config.model).toBe('gpt-3.5-turbo')
|
|
202
|
+
// 新字段使用默认值
|
|
203
|
+
expect(config.userPreferencesTopK).toBe(20)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// 配置保存测试
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
describe('saveConfig', () => {
|
|
212
|
+
it('应该写入配置文件', async () => {
|
|
213
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
214
|
+
|
|
215
|
+
const { saveConfig, getConfig } = await resetConfigModule()
|
|
216
|
+
const config = getConfig()
|
|
217
|
+
config.apiKey = 'new-api-key'
|
|
218
|
+
saveConfig(config)
|
|
219
|
+
|
|
220
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('应该创建配置目录(如果不存在)', async () => {
|
|
224
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
225
|
+
|
|
226
|
+
const { saveConfig, getConfig } = await resetConfigModule()
|
|
227
|
+
const config = getConfig()
|
|
228
|
+
saveConfig(config)
|
|
229
|
+
|
|
230
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(
|
|
231
|
+
expect.stringContaining('.please'),
|
|
232
|
+
{ recursive: true }
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('应该格式化 JSON(2 空格缩进)', async () => {
|
|
237
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
238
|
+
|
|
239
|
+
let writtenContent: string = ''
|
|
240
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
241
|
+
writtenContent = content
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const { saveConfig, getConfig } = await resetConfigModule()
|
|
245
|
+
const config = getConfig()
|
|
246
|
+
saveConfig(config)
|
|
247
|
+
|
|
248
|
+
// 检查是否有缩进
|
|
249
|
+
expect(writtenContent).toContain(' ')
|
|
250
|
+
expect(() => JSON.parse(writtenContent)).not.toThrow()
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// setConfigValue 测试
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
describe('setConfigValue', () => {
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
261
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('应该设置字符串配置项', async () => {
|
|
265
|
+
const { setConfigValue } = await resetConfigModule()
|
|
266
|
+
const config = setConfigValue('apiKey', 'new-key')
|
|
267
|
+
|
|
268
|
+
expect(config.apiKey).toBe('new-key')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('应该设置布尔配置项', async () => {
|
|
272
|
+
const { setConfigValue } = await resetConfigModule()
|
|
273
|
+
const config = setConfigValue('shellHook', true)
|
|
274
|
+
|
|
275
|
+
expect(config.shellHook).toBe(true)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('应该设置数字配置项', async () => {
|
|
279
|
+
const { setConfigValue } = await resetConfigModule()
|
|
280
|
+
const config = setConfigValue('shellHistoryLimit', 20)
|
|
281
|
+
|
|
282
|
+
expect(config.shellHistoryLimit).toBe(20)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('字符串 "true" 应该转换为布尔 true', async () => {
|
|
286
|
+
const { setConfigValue } = await resetConfigModule()
|
|
287
|
+
const config = setConfigValue('shellHook', 'true')
|
|
288
|
+
|
|
289
|
+
expect(config.shellHook).toBe(true)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('无效字段名应该抛出错误', async () => {
|
|
293
|
+
const { setConfigValue } = await resetConfigModule()
|
|
294
|
+
|
|
295
|
+
expect(() => setConfigValue('invalidField', 'value'))
|
|
296
|
+
.toThrow('未知的配置项: invalidField')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('无效的 provider 应该抛出错误', async () => {
|
|
300
|
+
const { setConfigValue } = await resetConfigModule()
|
|
301
|
+
|
|
302
|
+
expect(() => setConfigValue('provider', 'invalid-provider'))
|
|
303
|
+
.toThrow('provider 必须是以下之一')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('无效的 editMode 应该抛出错误', async () => {
|
|
307
|
+
const { setConfigValue } = await resetConfigModule()
|
|
308
|
+
|
|
309
|
+
expect(() => setConfigValue('editMode', 'invalid'))
|
|
310
|
+
.toThrow('editMode 必须是以下之一')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('无效的 theme 应该抛出错误', async () => {
|
|
314
|
+
mockIsValidTheme.mockReturnValue(false)
|
|
315
|
+
|
|
316
|
+
const { setConfigValue } = await resetConfigModule()
|
|
317
|
+
|
|
318
|
+
expect(() => setConfigValue('theme', 'invalid-theme'))
|
|
319
|
+
.toThrow('theme 必须是以下之一')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('数字配置项小于 1 应该抛出错误', async () => {
|
|
323
|
+
const { setConfigValue } = await resetConfigModule()
|
|
324
|
+
|
|
325
|
+
expect(() => setConfigValue('shellHistoryLimit', 0))
|
|
326
|
+
.toThrow('必须是大于 0 的整数')
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('数字配置项为负数应该抛出错误', async () => {
|
|
330
|
+
const { setConfigValue } = await resetConfigModule()
|
|
331
|
+
|
|
332
|
+
expect(() => setConfigValue('chatHistoryLimit', -5))
|
|
333
|
+
.toThrow('必须是大于 0 的整数')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('数字配置项为字符串数字应该正常转换', async () => {
|
|
337
|
+
const { setConfigValue } = await resetConfigModule()
|
|
338
|
+
const config = setConfigValue('commandHistoryLimit', '15' as any)
|
|
339
|
+
|
|
340
|
+
expect(config.commandHistoryLimit).toBe(15)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('有效的 provider 应该设置成功', async () => {
|
|
344
|
+
const { setConfigValue } = await resetConfigModule()
|
|
345
|
+
const config = setConfigValue('provider', 'anthropic')
|
|
346
|
+
|
|
347
|
+
expect(config.provider).toBe('anthropic')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('有效的 editMode 应该设置成功', async () => {
|
|
351
|
+
const { setConfigValue } = await resetConfigModule()
|
|
352
|
+
const config = setConfigValue('editMode', 'auto')
|
|
353
|
+
|
|
354
|
+
expect(config.editMode).toBe('auto')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('设置后应该清除缓存', async () => {
|
|
358
|
+
const { setConfigValue, getConfig } = await resetConfigModule()
|
|
359
|
+
|
|
360
|
+
// 第一次读取
|
|
361
|
+
getConfig()
|
|
362
|
+
expect(mockFs.readFileSync).toHaveBeenCalledTimes(1)
|
|
363
|
+
|
|
364
|
+
// 设置新值(会清除缓存)
|
|
365
|
+
setConfigValue('apiKey', 'new-key')
|
|
366
|
+
|
|
367
|
+
// 再次读取应该重新读取文件(但由于 mock 返回同样的值)
|
|
368
|
+
// 这里主要验证缓存被清除的逻辑
|
|
369
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled()
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// API Key 处理测试
|
|
375
|
+
// ============================================================================
|
|
376
|
+
|
|
377
|
+
describe('maskApiKey', () => {
|
|
378
|
+
it('长度 >= 10 应该显示前 6 后 4', async () => {
|
|
379
|
+
const { maskApiKey } = await resetConfigModule()
|
|
380
|
+
|
|
381
|
+
expect(maskApiKey('sk-1234567890abcdef')).toBe('sk-123****cdef')
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('长度 < 10 应该返回原值', async () => {
|
|
385
|
+
const { maskApiKey } = await resetConfigModule()
|
|
386
|
+
|
|
387
|
+
expect(maskApiKey('sk-short')).toBe('sk-short')
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('空字符串应该返回 (未设置)', async () => {
|
|
391
|
+
const { maskApiKey } = await resetConfigModule()
|
|
392
|
+
|
|
393
|
+
expect(maskApiKey('')).toBe('(未设置)')
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('null/undefined 应该返回 (未设置)', async () => {
|
|
397
|
+
const { maskApiKey } = await resetConfigModule()
|
|
398
|
+
|
|
399
|
+
expect(maskApiKey(null as any)).toBe('(未设置)')
|
|
400
|
+
expect(maskApiKey(undefined as any)).toBe('(未设置)')
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('应该使用 **** 掩码', async () => {
|
|
404
|
+
const { maskApiKey } = await resetConfigModule()
|
|
405
|
+
|
|
406
|
+
const masked = maskApiKey('sk-abcdefghij1234')
|
|
407
|
+
expect(masked).toContain('****')
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
// ============================================================================
|
|
412
|
+
// isConfigValid 测试
|
|
413
|
+
// ============================================================================
|
|
414
|
+
|
|
415
|
+
describe('isConfigValid', () => {
|
|
416
|
+
it('有 apiKey 应该返回 true', async () => {
|
|
417
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
418
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
419
|
+
|
|
420
|
+
const { isConfigValid } = await resetConfigModule()
|
|
421
|
+
|
|
422
|
+
expect(isConfigValid()).toBe(true)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('无 apiKey 应该返回 false', async () => {
|
|
426
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
427
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(configWithoutApiKey))
|
|
428
|
+
|
|
429
|
+
const { isConfigValid } = await resetConfigModule()
|
|
430
|
+
|
|
431
|
+
expect(isConfigValid()).toBe(false)
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('apiKey 为空字符串应该返回 false', async () => {
|
|
435
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
436
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ apiKey: '' }))
|
|
437
|
+
|
|
438
|
+
const { isConfigValid } = await resetConfigModule()
|
|
439
|
+
|
|
440
|
+
expect(isConfigValid()).toBe(false)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('配置文件不存在应该返回 false', async () => {
|
|
444
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
445
|
+
|
|
446
|
+
const { isConfigValid } = await resetConfigModule()
|
|
447
|
+
|
|
448
|
+
expect(isConfigValid()).toBe(false)
|
|
449
|
+
})
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// ============================================================================
|
|
453
|
+
// CONFIG_DIR 测试
|
|
454
|
+
// ============================================================================
|
|
455
|
+
|
|
456
|
+
describe('CONFIG_DIR', () => {
|
|
457
|
+
it('应该指向用户 home 目录下的 .please', async () => {
|
|
458
|
+
mockOs.homedir.mockReturnValue('/home/testuser')
|
|
459
|
+
|
|
460
|
+
const { CONFIG_DIR } = await resetConfigModule()
|
|
461
|
+
|
|
462
|
+
// 使用跨平台兼容的断言
|
|
463
|
+
expect(CONFIG_DIR).toContain('testuser')
|
|
464
|
+
expect(CONFIG_DIR).toContain('.please')
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('Windows 路径应该正确', async () => {
|
|
468
|
+
mockOs.homedir.mockReturnValue('C:\\Users\\TestUser')
|
|
469
|
+
|
|
470
|
+
const { CONFIG_DIR } = await resetConfigModule()
|
|
471
|
+
|
|
472
|
+
expect(CONFIG_DIR).toContain('.please')
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
// ============================================================================
|
|
477
|
+
// 边界情况测试
|
|
478
|
+
// ============================================================================
|
|
479
|
+
|
|
480
|
+
describe('边界情况', () => {
|
|
481
|
+
it('配置文件读取失败应该返回默认配置', async () => {
|
|
482
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
483
|
+
mockFs.readFileSync.mockImplementation(() => {
|
|
484
|
+
throw new Error('EACCES: permission denied')
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const { getConfig } = await resetConfigModule()
|
|
488
|
+
const config = getConfig()
|
|
489
|
+
|
|
490
|
+
expect(config.provider).toBe('openai')
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it('配置目录创建失败应该抛出错误', async () => {
|
|
494
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
495
|
+
mockFs.mkdirSync.mockImplementation(() => {
|
|
496
|
+
throw new Error('EACCES: permission denied')
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
const configModule = await resetConfigModule()
|
|
500
|
+
|
|
501
|
+
expect(() => configModule.getConfig()).toThrow()
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('写入配置失败应该抛出错误', async () => {
|
|
505
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
506
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
507
|
+
mockFs.writeFileSync.mockImplementation(() => {
|
|
508
|
+
throw new Error('ENOSPC: no space left on device')
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
const { saveConfig, getConfig } = await resetConfigModule()
|
|
512
|
+
const config = getConfig()
|
|
513
|
+
|
|
514
|
+
expect(() => saveConfig(config)).toThrow()
|
|
515
|
+
})
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
// ============================================================================
|
|
519
|
+
// Provider 配置测试
|
|
520
|
+
// ============================================================================
|
|
521
|
+
|
|
522
|
+
describe('Provider 配置', () => {
|
|
523
|
+
const providers = ['openai', 'anthropic', 'deepseek', 'google', 'groq', 'mistral', 'cohere', 'fireworks', 'together']
|
|
524
|
+
|
|
525
|
+
beforeEach(() => {
|
|
526
|
+
vi.clearAllMocks()
|
|
527
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
528
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
529
|
+
mockFs.writeFileSync.mockImplementation(() => {})
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
providers.forEach(provider => {
|
|
533
|
+
it(`应该支持 ${provider} provider`, async () => {
|
|
534
|
+
const { setConfigValue } = await resetConfigModule()
|
|
535
|
+
const config = setConfigValue('provider', provider)
|
|
536
|
+
|
|
537
|
+
expect(config.provider).toBe(provider)
|
|
538
|
+
})
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
// ============================================================================
|
|
543
|
+
// 数值配置项测试
|
|
544
|
+
// ============================================================================
|
|
545
|
+
|
|
546
|
+
describe('数值配置项', () => {
|
|
547
|
+
const numericFields = [
|
|
548
|
+
'chatHistoryLimit',
|
|
549
|
+
'commandHistoryLimit',
|
|
550
|
+
'shellHistoryLimit',
|
|
551
|
+
'userPreferencesTopK',
|
|
552
|
+
'systemCacheExpireDays',
|
|
553
|
+
]
|
|
554
|
+
|
|
555
|
+
beforeEach(() => {
|
|
556
|
+
vi.clearAllMocks()
|
|
557
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
558
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
559
|
+
mockFs.writeFileSync.mockImplementation(() => {})
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
numericFields.forEach(field => {
|
|
563
|
+
it(`${field} 应该接受正整数`, async () => {
|
|
564
|
+
const { setConfigValue } = await resetConfigModule()
|
|
565
|
+
const config = setConfigValue(field, 100)
|
|
566
|
+
|
|
567
|
+
expect(config[field as keyof typeof config]).toBe(100)
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
// ============================================================================
|
|
573
|
+
// displayConfig 测试
|
|
574
|
+
// ============================================================================
|
|
575
|
+
|
|
576
|
+
describe('displayConfig', () => {
|
|
577
|
+
let consoleLogSpy: ReturnType<typeof vi.spyOn>
|
|
578
|
+
|
|
579
|
+
beforeEach(() => {
|
|
580
|
+
vi.clearAllMocks()
|
|
581
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
582
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
afterEach(() => {
|
|
586
|
+
consoleLogSpy.mockRestore()
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it('应该显示当前配置', async () => {
|
|
590
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
591
|
+
|
|
592
|
+
const { displayConfig } = await resetConfigModule()
|
|
593
|
+
displayConfig()
|
|
594
|
+
|
|
595
|
+
expect(consoleLogSpy).toHaveBeenCalled()
|
|
596
|
+
// 验证输出包含配置标题
|
|
597
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
598
|
+
expect(allCalls).toContain('当前配置')
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('应该显示 apiKey(掩码形式)', async () => {
|
|
602
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({
|
|
603
|
+
...validConfig,
|
|
604
|
+
apiKey: 'sk-1234567890abcdef',
|
|
605
|
+
}))
|
|
606
|
+
|
|
607
|
+
const { displayConfig } = await resetConfigModule()
|
|
608
|
+
displayConfig()
|
|
609
|
+
|
|
610
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
611
|
+
expect(allCalls).toContain('apiKey')
|
|
612
|
+
// 掩码后不应显示完整 key
|
|
613
|
+
expect(allCalls).not.toContain('sk-1234567890abcdef')
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it('应该显示 provider 和 model', async () => {
|
|
617
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({
|
|
618
|
+
...validConfig,
|
|
619
|
+
provider: 'anthropic',
|
|
620
|
+
model: 'claude-3',
|
|
621
|
+
}))
|
|
622
|
+
|
|
623
|
+
const { displayConfig } = await resetConfigModule()
|
|
624
|
+
displayConfig()
|
|
625
|
+
|
|
626
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
627
|
+
expect(allCalls).toContain('provider')
|
|
628
|
+
expect(allCalls).toContain('anthropic')
|
|
629
|
+
expect(allCalls).toContain('model')
|
|
630
|
+
expect(allCalls).toContain('claude-3')
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it('应该显示 shellHook 状态', async () => {
|
|
634
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({
|
|
635
|
+
...validConfig,
|
|
636
|
+
shellHook: true,
|
|
637
|
+
}))
|
|
638
|
+
|
|
639
|
+
const { displayConfig } = await resetConfigModule()
|
|
640
|
+
displayConfig()
|
|
641
|
+
|
|
642
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
643
|
+
expect(allCalls).toContain('shellHook')
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
it('应该显示 editMode', async () => {
|
|
647
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({
|
|
648
|
+
...validConfig,
|
|
649
|
+
editMode: 'auto',
|
|
650
|
+
}))
|
|
651
|
+
|
|
652
|
+
const { displayConfig } = await resetConfigModule()
|
|
653
|
+
displayConfig()
|
|
654
|
+
|
|
655
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
656
|
+
expect(allCalls).toContain('editMode')
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
it('应该显示各种历史限制配置', async () => {
|
|
660
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({
|
|
661
|
+
...validConfig,
|
|
662
|
+
chatHistoryLimit: 10,
|
|
663
|
+
commandHistoryLimit: 20,
|
|
664
|
+
shellHistoryLimit: 30,
|
|
665
|
+
}))
|
|
666
|
+
|
|
667
|
+
const { displayConfig } = await resetConfigModule()
|
|
668
|
+
displayConfig()
|
|
669
|
+
|
|
670
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
671
|
+
expect(allCalls).toContain('chatHistoryLimit')
|
|
672
|
+
expect(allCalls).toContain('commandHistoryLimit')
|
|
673
|
+
expect(allCalls).toContain('shellHistoryLimit')
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
it('应该显示 userPreferencesTopK', async () => {
|
|
677
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({
|
|
678
|
+
...validConfig,
|
|
679
|
+
userPreferencesTopK: 50,
|
|
680
|
+
}))
|
|
681
|
+
|
|
682
|
+
const { displayConfig } = await resetConfigModule()
|
|
683
|
+
displayConfig()
|
|
684
|
+
|
|
685
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
686
|
+
expect(allCalls).toContain('userPreferencesTopK')
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
it('应该显示 systemCacheExpireDays(如果存在)', async () => {
|
|
690
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({
|
|
691
|
+
...validConfig,
|
|
692
|
+
systemCacheExpireDays: 14,
|
|
693
|
+
}))
|
|
694
|
+
|
|
695
|
+
const { displayConfig } = await resetConfigModule()
|
|
696
|
+
displayConfig()
|
|
697
|
+
|
|
698
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
699
|
+
expect(allCalls).toContain('systemCacheExpireDays')
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
it('应该显示 theme 信息', async () => {
|
|
703
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({
|
|
704
|
+
...validConfig,
|
|
705
|
+
theme: 'dark',
|
|
706
|
+
}))
|
|
707
|
+
|
|
708
|
+
const { displayConfig } = await resetConfigModule()
|
|
709
|
+
displayConfig()
|
|
710
|
+
|
|
711
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
712
|
+
expect(allCalls).toContain('theme')
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
it('应该显示配置文件路径', async () => {
|
|
716
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
717
|
+
|
|
718
|
+
const { displayConfig } = await resetConfigModule()
|
|
719
|
+
displayConfig()
|
|
720
|
+
|
|
721
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
722
|
+
expect(allCalls).toContain('配置文件')
|
|
723
|
+
expect(allCalls).toContain('.please')
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
it('配置文件不存在时应该显示默认配置', async () => {
|
|
727
|
+
mockFs.existsSync.mockReturnValue(false)
|
|
728
|
+
mockFs.mkdirSync.mockImplementation(() => undefined)
|
|
729
|
+
|
|
730
|
+
const { displayConfig } = await resetConfigModule()
|
|
731
|
+
displayConfig()
|
|
732
|
+
|
|
733
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
734
|
+
expect(allCalls).toContain('当前配置')
|
|
735
|
+
expect(allCalls).toContain('openai') // 默认 provider
|
|
736
|
+
})
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
// ============================================================================
|
|
740
|
+
// runConfigWizard 测试
|
|
741
|
+
// ============================================================================
|
|
742
|
+
|
|
743
|
+
describe('runConfigWizard', () => {
|
|
744
|
+
let consoleLogSpy: ReturnType<typeof vi.spyOn>
|
|
745
|
+
|
|
746
|
+
beforeEach(() => {
|
|
747
|
+
vi.clearAllMocks()
|
|
748
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
749
|
+
mockFs.existsSync.mockReturnValue(true)
|
|
750
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(validConfig))
|
|
751
|
+
mockFs.writeFileSync.mockImplementation(() => {})
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
afterEach(() => {
|
|
755
|
+
consoleLogSpy.mockRestore()
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
it('应该显示配置向导标题', async () => {
|
|
759
|
+
// Mock readline 返回空字符串(使用默认值)
|
|
760
|
+
mockReadline.createInterface.mockReturnValue({
|
|
761
|
+
question: vi.fn((prompt, callback) => callback('')),
|
|
762
|
+
close: vi.fn(),
|
|
763
|
+
} as any)
|
|
764
|
+
|
|
765
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
766
|
+
await runConfigWizard()
|
|
767
|
+
|
|
768
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
769
|
+
expect(allCalls).toContain('配置向导')
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it('全部使用默认值时应该保存配置', async () => {
|
|
773
|
+
mockReadline.createInterface.mockReturnValue({
|
|
774
|
+
question: vi.fn((prompt, callback) => callback('')),
|
|
775
|
+
close: vi.fn(),
|
|
776
|
+
} as any)
|
|
777
|
+
|
|
778
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
779
|
+
await runConfigWizard()
|
|
780
|
+
|
|
781
|
+
expect(mockFs.writeFileSync).toHaveBeenCalled()
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it('无效的 provider 应该提前退出', async () => {
|
|
785
|
+
mockReadline.createInterface.mockReturnValue({
|
|
786
|
+
question: vi.fn((prompt, callback) => {
|
|
787
|
+
if (prompt.includes('Provider')) {
|
|
788
|
+
callback('invalid-provider')
|
|
789
|
+
} else {
|
|
790
|
+
callback('')
|
|
791
|
+
}
|
|
792
|
+
}),
|
|
793
|
+
close: vi.fn(),
|
|
794
|
+
} as any)
|
|
795
|
+
|
|
796
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
797
|
+
await runConfigWizard()
|
|
798
|
+
|
|
799
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
800
|
+
expect(allCalls).toContain('无效')
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
it('无效的 editMode 应该提前退出', async () => {
|
|
804
|
+
mockReadline.createInterface.mockReturnValue({
|
|
805
|
+
question: vi.fn((prompt, callback) => {
|
|
806
|
+
if (prompt.includes('编辑模式')) {
|
|
807
|
+
callback('invalid-mode')
|
|
808
|
+
} else {
|
|
809
|
+
callback('')
|
|
810
|
+
}
|
|
811
|
+
}),
|
|
812
|
+
close: vi.fn(),
|
|
813
|
+
} as any)
|
|
814
|
+
|
|
815
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
816
|
+
await runConfigWizard()
|
|
817
|
+
|
|
818
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
819
|
+
expect(allCalls).toContain('无效')
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
it('设置有效的 provider 应该更新配置', async () => {
|
|
823
|
+
let savedConfig: any = null
|
|
824
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
825
|
+
savedConfig = JSON.parse(content)
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
mockReadline.createInterface.mockReturnValue({
|
|
829
|
+
question: vi.fn((prompt, callback) => {
|
|
830
|
+
if (prompt.includes('Provider')) {
|
|
831
|
+
callback('anthropic')
|
|
832
|
+
} else {
|
|
833
|
+
callback('')
|
|
834
|
+
}
|
|
835
|
+
}),
|
|
836
|
+
close: vi.fn(),
|
|
837
|
+
} as any)
|
|
838
|
+
|
|
839
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
840
|
+
await runConfigWizard()
|
|
841
|
+
|
|
842
|
+
expect(savedConfig?.provider).toBe('anthropic')
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
it('设置有效的 API Key 应该更新配置', async () => {
|
|
846
|
+
let savedConfig: any = null
|
|
847
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
848
|
+
savedConfig = JSON.parse(content)
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
mockReadline.createInterface.mockReturnValue({
|
|
852
|
+
question: vi.fn((prompt, callback) => {
|
|
853
|
+
if (prompt.includes('API Key')) {
|
|
854
|
+
callback('new-api-key-12345')
|
|
855
|
+
} else {
|
|
856
|
+
callback('')
|
|
857
|
+
}
|
|
858
|
+
}),
|
|
859
|
+
close: vi.fn(),
|
|
860
|
+
} as any)
|
|
861
|
+
|
|
862
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
863
|
+
await runConfigWizard()
|
|
864
|
+
|
|
865
|
+
expect(savedConfig?.apiKey).toBe('new-api-key-12345')
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
it('设置有效的 baseUrl 应该更新配置', async () => {
|
|
869
|
+
let savedConfig: any = null
|
|
870
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
871
|
+
savedConfig = JSON.parse(content)
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
mockReadline.createInterface.mockReturnValue({
|
|
875
|
+
question: vi.fn((prompt, callback) => {
|
|
876
|
+
if (prompt.includes('Base URL')) {
|
|
877
|
+
callback('https://custom-api.example.com/v1')
|
|
878
|
+
} else {
|
|
879
|
+
callback('')
|
|
880
|
+
}
|
|
881
|
+
}),
|
|
882
|
+
close: vi.fn(),
|
|
883
|
+
} as any)
|
|
884
|
+
|
|
885
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
886
|
+
await runConfigWizard()
|
|
887
|
+
|
|
888
|
+
expect(savedConfig?.baseUrl).toBe('https://custom-api.example.com/v1')
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
it('设置有效的 model 应该更新配置', async () => {
|
|
892
|
+
let savedConfig: any = null
|
|
893
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
894
|
+
savedConfig = JSON.parse(content)
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
mockReadline.createInterface.mockReturnValue({
|
|
898
|
+
question: vi.fn((prompt, callback) => {
|
|
899
|
+
if (prompt.includes('Model')) {
|
|
900
|
+
callback('gpt-4o')
|
|
901
|
+
} else {
|
|
902
|
+
callback('')
|
|
903
|
+
}
|
|
904
|
+
}),
|
|
905
|
+
close: vi.fn(),
|
|
906
|
+
} as any)
|
|
907
|
+
|
|
908
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
909
|
+
await runConfigWizard()
|
|
910
|
+
|
|
911
|
+
expect(savedConfig?.model).toBe('gpt-4o')
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
it('设置 shellHook 为 true 应该更新配置', async () => {
|
|
915
|
+
let savedConfig: any = null
|
|
916
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
917
|
+
savedConfig = JSON.parse(content)
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
mockReadline.createInterface.mockReturnValue({
|
|
921
|
+
question: vi.fn((prompt, callback) => {
|
|
922
|
+
if (prompt.includes('Shell Hook')) {
|
|
923
|
+
callback('true')
|
|
924
|
+
} else {
|
|
925
|
+
callback('')
|
|
926
|
+
}
|
|
927
|
+
}),
|
|
928
|
+
close: vi.fn(),
|
|
929
|
+
} as any)
|
|
930
|
+
|
|
931
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
932
|
+
await runConfigWizard()
|
|
933
|
+
|
|
934
|
+
expect(savedConfig?.shellHook).toBe(true)
|
|
935
|
+
})
|
|
936
|
+
|
|
937
|
+
it('设置有效的 editMode 应该更新配置', async () => {
|
|
938
|
+
let savedConfig: any = null
|
|
939
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
940
|
+
savedConfig = JSON.parse(content)
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
mockReadline.createInterface.mockReturnValue({
|
|
944
|
+
question: vi.fn((prompt, callback) => {
|
|
945
|
+
if (prompt.includes('编辑模式')) {
|
|
946
|
+
callback('auto')
|
|
947
|
+
} else {
|
|
948
|
+
callback('')
|
|
949
|
+
}
|
|
950
|
+
}),
|
|
951
|
+
close: vi.fn(),
|
|
952
|
+
} as any)
|
|
953
|
+
|
|
954
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
955
|
+
await runConfigWizard()
|
|
956
|
+
|
|
957
|
+
expect(savedConfig?.editMode).toBe('auto')
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
it('设置有效的数字配置应该更新', async () => {
|
|
961
|
+
let savedConfig: any = null
|
|
962
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
963
|
+
savedConfig = JSON.parse(content)
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
mockReadline.createInterface.mockReturnValue({
|
|
967
|
+
question: vi.fn((prompt, callback) => {
|
|
968
|
+
if (prompt.includes('Chat 历史保留轮数')) {
|
|
969
|
+
callback('15')
|
|
970
|
+
} else if (prompt.includes('命令历史保留条数')) {
|
|
971
|
+
callback('25')
|
|
972
|
+
} else if (prompt.includes('Shell 历史保留条数')) {
|
|
973
|
+
callback('35')
|
|
974
|
+
} else if (prompt.includes('用户偏好显示命令数')) {
|
|
975
|
+
callback('45')
|
|
976
|
+
} else {
|
|
977
|
+
callback('')
|
|
978
|
+
}
|
|
979
|
+
}),
|
|
980
|
+
close: vi.fn(),
|
|
981
|
+
} as any)
|
|
982
|
+
|
|
983
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
984
|
+
await runConfigWizard()
|
|
985
|
+
|
|
986
|
+
expect(savedConfig?.chatHistoryLimit).toBe(15)
|
|
987
|
+
expect(savedConfig?.commandHistoryLimit).toBe(25)
|
|
988
|
+
expect(savedConfig?.shellHistoryLimit).toBe(35)
|
|
989
|
+
expect(savedConfig?.userPreferencesTopK).toBe(45)
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
it('无效的数字配置应该显示警告并保持原值', async () => {
|
|
993
|
+
let savedConfig: any = null
|
|
994
|
+
mockFs.writeFileSync.mockImplementation((path: any, content: any) => {
|
|
995
|
+
savedConfig = JSON.parse(content)
|
|
996
|
+
})
|
|
997
|
+
|
|
998
|
+
mockReadline.createInterface.mockReturnValue({
|
|
999
|
+
question: vi.fn((prompt, callback) => {
|
|
1000
|
+
if (prompt.includes('Chat 历史保留轮数')) {
|
|
1001
|
+
callback('invalid')
|
|
1002
|
+
} else {
|
|
1003
|
+
callback('')
|
|
1004
|
+
}
|
|
1005
|
+
}),
|
|
1006
|
+
close: vi.fn(),
|
|
1007
|
+
} as any)
|
|
1008
|
+
|
|
1009
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
1010
|
+
await runConfigWizard()
|
|
1011
|
+
|
|
1012
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
1013
|
+
expect(allCalls).toContain('输入无效')
|
|
1014
|
+
// 保持原值(来自 validConfig)
|
|
1015
|
+
expect(savedConfig?.chatHistoryLimit).toBe(validConfig.chatHistoryLimit)
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
it('完成后应该显示成功消息', async () => {
|
|
1019
|
+
mockReadline.createInterface.mockReturnValue({
|
|
1020
|
+
question: vi.fn((prompt, callback) => callback('')),
|
|
1021
|
+
close: vi.fn(),
|
|
1022
|
+
} as any)
|
|
1023
|
+
|
|
1024
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
1025
|
+
await runConfigWizard()
|
|
1026
|
+
|
|
1027
|
+
const allCalls = consoleLogSpy.mock.calls.map(call => call[0]).join('\n')
|
|
1028
|
+
expect(allCalls).toContain('配置已保存')
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
it('完成后应该关闭 readline', async () => {
|
|
1032
|
+
const closeFn = vi.fn()
|
|
1033
|
+
mockReadline.createInterface.mockReturnValue({
|
|
1034
|
+
question: vi.fn((prompt, callback) => callback('')),
|
|
1035
|
+
close: closeFn,
|
|
1036
|
+
} as any)
|
|
1037
|
+
|
|
1038
|
+
const { runConfigWizard } = await resetConfigModule()
|
|
1039
|
+
await runConfigWizard()
|
|
1040
|
+
|
|
1041
|
+
expect(closeFn).toHaveBeenCalled()
|
|
1042
|
+
})
|
|
1043
|
+
})
|