clawt 2.12.1 → 2.14.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.
@@ -1,85 +1,156 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { Command } from 'commander';
3
3
 
4
+ // mock enquirer(必须在所有 import 之前)
5
+ const { mockSelectRun, mockInputRun } = vi.hoisted(() => {
6
+ const mockSelectRun = vi.fn();
7
+ const mockInputRun = vi.fn();
8
+ return { mockSelectRun, mockInputRun };
9
+ });
10
+
11
+ vi.mock('enquirer', () => ({
12
+ default: {
13
+ Select: function MockSelect() { return { run: mockSelectRun }; },
14
+ Input: function MockInput() { return { run: mockInputRun }; },
15
+ },
16
+ }));
17
+
4
18
  // mock 依赖模块
5
19
  vi.mock('../../../src/logger/index.js', () => ({
6
20
  logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
7
21
  }));
8
22
 
9
- vi.mock('../../../src/utils/index.js', () => ({
10
- loadConfig: vi.fn(),
11
- writeDefaultConfig: vi.fn(),
12
- printInfo: vi.fn(),
13
- printSuccess: vi.fn(),
14
- printSeparator: vi.fn(),
15
- confirmDestructiveAction: vi.fn(),
16
- }));
23
+ vi.mock('../../../src/utils/index.js', async (importOriginal) => {
24
+ const original = await importOriginal<typeof import('../../../src/utils/index.js')>();
25
+ return {
26
+ loadConfig: vi.fn(),
27
+ writeDefaultConfig: vi.fn(),
28
+ saveConfig: vi.fn(),
29
+ printInfo: vi.fn(),
30
+ printSuccess: vi.fn(),
31
+ printError: vi.fn(),
32
+ confirmDestructiveAction: vi.fn(),
33
+ // 策略工具函数透传真实实现(因为常量已被 mock,工具函数可以正常工作)
34
+ isValidConfigKey: original.isValidConfigKey,
35
+ getValidConfigKeys: original.getValidConfigKeys,
36
+ parseConfigValue: original.parseConfigValue,
37
+ promptConfigValue: original.promptConfigValue,
38
+ formatConfigValue: original.formatConfigValue,
39
+ };
40
+ });
17
41
 
18
42
  vi.mock('../../../src/constants/index.js', () => ({
19
43
  CONFIG_PATH: '/mock/.clawt/config.json',
20
44
  DEFAULT_CONFIG: {
21
- claudeCodeCommand: 'claude',
22
45
  autoDeleteBranch: false,
46
+ claudeCodeCommand: 'claude',
23
47
  autoPullPush: false,
24
48
  confirmDestructiveOps: true,
49
+ maxConcurrency: 0,
50
+ terminalApp: 'auto',
25
51
  },
26
52
  CONFIG_DESCRIPTIONS: {
27
- claudeCodeCommand: 'Claude Code CLI 命令',
28
53
  autoDeleteBranch: '自动删除分支',
54
+ claudeCodeCommand: 'Claude Code CLI 命令',
29
55
  autoPullPush: '自动 pull/push',
30
56
  confirmDestructiveOps: '破坏性操作确认',
57
+ maxConcurrency: '最大并发数',
58
+ terminalApp: '终端应用',
59
+ },
60
+ CONFIG_DEFINITIONS: {
61
+ autoDeleteBranch: { defaultValue: false, description: '自动删除分支' },
62
+ claudeCodeCommand: { defaultValue: 'claude', description: 'Claude Code CLI 命令' },
63
+ autoPullPush: { defaultValue: false, description: '自动 pull/push' },
64
+ confirmDestructiveOps: { defaultValue: true, description: '破坏性操作确认' },
65
+ maxConcurrency: { defaultValue: 0, description: '最大并发数' },
66
+ terminalApp: { defaultValue: 'auto', description: '终端应用', allowedValues: ['auto', 'iterm2', 'terminal'] },
31
67
  },
32
68
  MESSAGES: {
33
69
  CONFIG_RESET_SUCCESS: '配置已恢复为默认值',
34
70
  DESTRUCTIVE_OP_CANCELLED: '已取消操作',
71
+ CONFIG_SET_SUCCESS: (key: string, value: string) => `✓ ${key} 已设置为 ${value}`,
72
+ CONFIG_GET_VALUE: (key: string, value: string) => `${key} = ${value}`,
73
+ CONFIG_INVALID_KEY: (key: string, validKeys: string[]) =>
74
+ `无效的配置项: ${key}\n可用的配置项: ${validKeys.join(', ')}`,
75
+ CONFIG_INVALID_BOOLEAN: (key: string) =>
76
+ `配置项 ${key} 为布尔类型,仅接受 true 或 false`,
77
+ CONFIG_INVALID_NUMBER: (key: string) =>
78
+ `配置项 ${key} 为数字类型,请输入有效的数字`,
79
+ CONFIG_INVALID_ENUM: (key: string, validValues: readonly string[]) =>
80
+ `配置项 ${key} 仅接受以下值: ${validValues.join(', ')}`,
81
+ CONFIG_SELECT_PROMPT: '选择要修改的配置项',
82
+ CONFIG_INPUT_PROMPT: (key: string) => `输入 ${key} 的新值`,
83
+ CONFIG_MISSING_VALUE: (key: string) => `缺少配置值,用法: clawt config set ${key} <value>`,
35
84
  },
36
85
  }));
37
86
 
38
87
  import { registerConfigCommand } from '../../../src/commands/config.js';
39
- import { loadConfig, writeDefaultConfig, printInfo, printSuccess, confirmDestructiveAction } from '../../../src/utils/index.js';
88
+ import { loadConfig, writeDefaultConfig, saveConfig, printInfo, printSuccess, printError, confirmDestructiveAction } from '../../../src/utils/index.js';
40
89
 
41
90
  const mockedLoadConfig = vi.mocked(loadConfig);
42
91
  const mockedWriteDefaultConfig = vi.mocked(writeDefaultConfig);
92
+ const mockedSaveConfig = vi.mocked(saveConfig);
43
93
  const mockedPrintInfo = vi.mocked(printInfo);
44
94
  const mockedPrintSuccess = vi.mocked(printSuccess);
95
+ const mockedPrintError = vi.mocked(printError);
45
96
  const mockedConfirmDestructiveAction = vi.mocked(confirmDestructiveAction);
46
97
 
98
+ /** 创建默认配置对象用于 mock */
99
+ function createMockConfig() {
100
+ return {
101
+ autoDeleteBranch: false,
102
+ claudeCodeCommand: 'claude',
103
+ autoPullPush: false,
104
+ confirmDestructiveOps: true,
105
+ maxConcurrency: 0,
106
+ terminalApp: 'auto',
107
+ };
108
+ }
109
+
47
110
  beforeEach(() => {
48
111
  mockedLoadConfig.mockReset();
49
112
  mockedWriteDefaultConfig.mockReset();
113
+ mockedSaveConfig.mockReset();
50
114
  mockedPrintInfo.mockReset();
51
115
  mockedPrintSuccess.mockReset();
116
+ mockedPrintError.mockReset();
52
117
  mockedConfirmDestructiveAction.mockReset();
118
+ mockSelectRun.mockReset();
119
+ mockInputRun.mockReset();
53
120
  });
54
121
 
55
122
  describe('registerConfigCommand', () => {
56
- it('注册 config 命令和 config reset 子命令', () => {
123
+ it('注册 config 命令及所有子命令', () => {
57
124
  const program = new Command();
58
125
  registerConfigCommand(program);
59
126
  const configCmd = program.commands.find((c) => c.name() === 'config');
60
127
  expect(configCmd).toBeDefined();
61
- const resetCmd = configCmd!.commands.find((c) => c.name() === 'reset');
62
- expect(resetCmd).toBeDefined();
128
+
129
+ const subcommandNames = configCmd!.commands.map((c) => c.name());
130
+ expect(subcommandNames).toContain('reset');
131
+ expect(subcommandNames).toContain('set');
132
+ expect(subcommandNames).toContain('get');
63
133
  });
64
134
  });
65
135
 
66
136
  describe('handleConfig(通过 action 间接测试)', () => {
67
- it('展示配置列表', () => {
68
- mockedLoadConfig.mockReturnValue({
69
- claudeCodeCommand: 'claude',
70
- autoDeleteBranch: false,
71
- autoPullPush: false,
72
- confirmDestructiveOps: true,
73
- });
137
+ it('无子命令时进入交互式配置', async () => {
138
+ mockedLoadConfig.mockReturnValue(createMockConfig());
139
+ // 第一次 Select.run 选择配置项
140
+ mockSelectRun.mockResolvedValueOnce('autoDeleteBranch');
141
+ // 第二次 Select.run 选择布尔值
142
+ mockSelectRun.mockResolvedValueOnce('true');
74
143
 
75
144
  const program = new Command();
76
145
  program.exitOverride();
77
146
  registerConfigCommand(program);
78
- program.parse(['config'], { from: 'user' });
147
+ await program.parseAsync(['config'], { from: 'user' });
79
148
 
80
149
  expect(mockedLoadConfig).toHaveBeenCalled();
81
- // 应输出配置信息
82
- expect(mockedPrintInfo).toHaveBeenCalled();
150
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
151
+ expect.objectContaining({ autoDeleteBranch: true }),
152
+ );
153
+ expect(mockedPrintSuccess).toHaveBeenCalled();
83
154
  });
84
155
  });
85
156
 
@@ -108,3 +179,232 @@ describe('handleConfigReset(通过 action 间接测试)', () => {
108
179
  expect(mockedPrintInfo).toHaveBeenCalled();
109
180
  });
110
181
  });
182
+
183
+ describe('handleConfigSet — 直接模式', () => {
184
+ it('设置布尔值 true', async () => {
185
+ mockedLoadConfig.mockReturnValue(createMockConfig());
186
+
187
+ const program = new Command();
188
+ program.exitOverride();
189
+ registerConfigCommand(program);
190
+ await program.parseAsync(['config', 'set', 'autoDeleteBranch', 'true'], { from: 'user' });
191
+
192
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
193
+ expect.objectContaining({ autoDeleteBranch: true }),
194
+ );
195
+ expect(mockedPrintSuccess).toHaveBeenCalled();
196
+ });
197
+
198
+ it('设置布尔值 false', async () => {
199
+ mockedLoadConfig.mockReturnValue({ ...createMockConfig(), confirmDestructiveOps: true });
200
+
201
+ const program = new Command();
202
+ program.exitOverride();
203
+ registerConfigCommand(program);
204
+ await program.parseAsync(['config', 'set', 'confirmDestructiveOps', 'false'], { from: 'user' });
205
+
206
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
207
+ expect.objectContaining({ confirmDestructiveOps: false }),
208
+ );
209
+ expect(mockedPrintSuccess).toHaveBeenCalled();
210
+ });
211
+
212
+ it('布尔值无效时报错', async () => {
213
+ const program = new Command();
214
+ program.exitOverride();
215
+ registerConfigCommand(program);
216
+ await program.parseAsync(['config', 'set', 'autoDeleteBranch', 'abc'], { from: 'user' });
217
+
218
+ expect(mockedSaveConfig).not.toHaveBeenCalled();
219
+ expect(mockedPrintError).toHaveBeenCalled();
220
+ });
221
+
222
+ it('设置数字值', async () => {
223
+ mockedLoadConfig.mockReturnValue(createMockConfig());
224
+
225
+ const program = new Command();
226
+ program.exitOverride();
227
+ registerConfigCommand(program);
228
+ await program.parseAsync(['config', 'set', 'maxConcurrency', '4'], { from: 'user' });
229
+
230
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
231
+ expect.objectContaining({ maxConcurrency: 4 }),
232
+ );
233
+ expect(mockedPrintSuccess).toHaveBeenCalled();
234
+ });
235
+
236
+ it('数字值无效时报错', async () => {
237
+ const program = new Command();
238
+ program.exitOverride();
239
+ registerConfigCommand(program);
240
+ await program.parseAsync(['config', 'set', 'maxConcurrency', 'abc'], { from: 'user' });
241
+
242
+ expect(mockedSaveConfig).not.toHaveBeenCalled();
243
+ expect(mockedPrintError).toHaveBeenCalled();
244
+ });
245
+
246
+ it('设置字符串值', async () => {
247
+ mockedLoadConfig.mockReturnValue(createMockConfig());
248
+
249
+ const program = new Command();
250
+ program.exitOverride();
251
+ registerConfigCommand(program);
252
+ await program.parseAsync(['config', 'set', 'claudeCodeCommand', 'cc'], { from: 'user' });
253
+
254
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
255
+ expect.objectContaining({ claudeCodeCommand: 'cc' }),
256
+ );
257
+ expect(mockedPrintSuccess).toHaveBeenCalled();
258
+ });
259
+
260
+ it('设置 terminalApp 有效值', async () => {
261
+ mockedLoadConfig.mockReturnValue(createMockConfig());
262
+
263
+ const program = new Command();
264
+ program.exitOverride();
265
+ registerConfigCommand(program);
266
+ await program.parseAsync(['config', 'set', 'terminalApp', 'iterm2'], { from: 'user' });
267
+
268
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
269
+ expect.objectContaining({ terminalApp: 'iterm2' }),
270
+ );
271
+ expect(mockedPrintSuccess).toHaveBeenCalled();
272
+ });
273
+
274
+ it('设置 terminalApp 无效值时报错', async () => {
275
+ const program = new Command();
276
+ program.exitOverride();
277
+ registerConfigCommand(program);
278
+ await program.parseAsync(['config', 'set', 'terminalApp', 'invalid'], { from: 'user' });
279
+
280
+ expect(mockedSaveConfig).not.toHaveBeenCalled();
281
+ expect(mockedPrintError).toHaveBeenCalled();
282
+ });
283
+
284
+ it('无效 key 时报错', async () => {
285
+ const program = new Command();
286
+ program.exitOverride();
287
+ registerConfigCommand(program);
288
+ await program.parseAsync(['config', 'set', 'foobar', 'true'], { from: 'user' });
289
+
290
+ expect(mockedSaveConfig).not.toHaveBeenCalled();
291
+ expect(mockedPrintError).toHaveBeenCalled();
292
+ });
293
+
294
+ it('缺少 value 参数时报错', async () => {
295
+ const program = new Command();
296
+ program.exitOverride();
297
+ registerConfigCommand(program);
298
+ await program.parseAsync(['config', 'set', 'autoDeleteBranch'], { from: 'user' });
299
+
300
+ expect(mockedSaveConfig).not.toHaveBeenCalled();
301
+ expect(mockedPrintError).toHaveBeenCalled();
302
+ });
303
+ });
304
+
305
+ describe('handleConfigSet — 交互模式', () => {
306
+ it('交互选择布尔配置项并修改', async () => {
307
+ mockedLoadConfig.mockReturnValue(createMockConfig());
308
+ // 第一次 Select.run 选择配置项
309
+ mockSelectRun.mockResolvedValueOnce('autoDeleteBranch');
310
+ // 第二次 Select.run 选择布尔值
311
+ mockSelectRun.mockResolvedValueOnce('true');
312
+
313
+ const program = new Command();
314
+ program.exitOverride();
315
+ registerConfigCommand(program);
316
+ await program.parseAsync(['config', 'set'], { from: 'user' });
317
+
318
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
319
+ expect.objectContaining({ autoDeleteBranch: true }),
320
+ );
321
+ expect(mockedPrintSuccess).toHaveBeenCalled();
322
+ });
323
+
324
+ it('交互选择数字配置项并修改', async () => {
325
+ mockedLoadConfig.mockReturnValue(createMockConfig());
326
+ // 选择配置项
327
+ mockSelectRun.mockResolvedValueOnce('maxConcurrency');
328
+ // 输入数字
329
+ mockInputRun.mockResolvedValueOnce('8');
330
+
331
+ const program = new Command();
332
+ program.exitOverride();
333
+ registerConfigCommand(program);
334
+ await program.parseAsync(['config', 'set'], { from: 'user' });
335
+
336
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
337
+ expect.objectContaining({ maxConcurrency: 8 }),
338
+ );
339
+ expect(mockedPrintSuccess).toHaveBeenCalled();
340
+ });
341
+
342
+ it('交互选择字符串配置项并修改', async () => {
343
+ mockedLoadConfig.mockReturnValue(createMockConfig());
344
+ // 选择配置项
345
+ mockSelectRun.mockResolvedValueOnce('claudeCodeCommand');
346
+ // 输入字符串
347
+ mockInputRun.mockResolvedValueOnce('cc');
348
+
349
+ const program = new Command();
350
+ program.exitOverride();
351
+ registerConfigCommand(program);
352
+ await program.parseAsync(['config', 'set'], { from: 'user' });
353
+
354
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
355
+ expect.objectContaining({ claudeCodeCommand: 'cc' }),
356
+ );
357
+ expect(mockedPrintSuccess).toHaveBeenCalled();
358
+ });
359
+
360
+ it('交互选择 terminalApp 配置项时使用 Select', async () => {
361
+ mockedLoadConfig.mockReturnValue(createMockConfig());
362
+ // 选择配置项
363
+ mockSelectRun.mockResolvedValueOnce('terminalApp');
364
+ // 选择 terminalApp 值
365
+ mockSelectRun.mockResolvedValueOnce('iterm2');
366
+
367
+ const program = new Command();
368
+ program.exitOverride();
369
+ registerConfigCommand(program);
370
+ await program.parseAsync(['config', 'set'], { from: 'user' });
371
+
372
+ expect(mockedSaveConfig).toHaveBeenCalledWith(
373
+ expect.objectContaining({ terminalApp: 'iterm2' }),
374
+ );
375
+ expect(mockedPrintSuccess).toHaveBeenCalled();
376
+ });
377
+ });
378
+
379
+ describe('handleConfigGet', () => {
380
+ it('获取有效配置项的值', () => {
381
+ mockedLoadConfig.mockReturnValue(createMockConfig());
382
+
383
+ const program = new Command();
384
+ program.exitOverride();
385
+ registerConfigCommand(program);
386
+ program.parse(['config', 'get', 'maxConcurrency'], { from: 'user' });
387
+
388
+ expect(mockedPrintInfo).toHaveBeenCalled();
389
+ });
390
+
391
+ it('获取布尔配置项的值', () => {
392
+ mockedLoadConfig.mockReturnValue({ ...createMockConfig(), autoDeleteBranch: true });
393
+
394
+ const program = new Command();
395
+ program.exitOverride();
396
+ registerConfigCommand(program);
397
+ program.parse(['config', 'get', 'autoDeleteBranch'], { from: 'user' });
398
+
399
+ expect(mockedPrintInfo).toHaveBeenCalled();
400
+ });
401
+
402
+ it('无效 key 时报错', () => {
403
+ const program = new Command();
404
+ program.exitOverride();
405
+ registerConfigCommand(program);
406
+ program.parse(['config', 'get', 'invalidKey'], { from: 'user' });
407
+
408
+ expect(mockedPrintError).toHaveBeenCalled();
409
+ });
410
+ });
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { CONFIG_DEFINITIONS, DEFAULT_CONFIG, CONFIG_DESCRIPTIONS } from '../../../src/constants/config.js';
3
+ import { VALID_TERMINAL_APPS } from '../../../src/constants/terminal.js';
3
4
 
4
5
  describe('CONFIG_DEFINITIONS', () => {
5
6
  it('所有配置项都有 defaultValue 和 description', () => {
@@ -21,7 +22,7 @@ describe('DEFAULT_CONFIG', () => {
21
22
 
22
23
  it('每个 key 的值等于对应 CONFIG_DEFINITIONS 的 defaultValue', () => {
23
24
  for (const [key, def] of Object.entries(CONFIG_DEFINITIONS)) {
24
- expect((DEFAULT_CONFIG as Record<string, unknown>)[key]).toBe(def.defaultValue);
25
+ expect((DEFAULT_CONFIG as unknown as Record<string, unknown>)[key]).toBe(def.defaultValue);
25
26
  }
26
27
  });
27
28
 
@@ -31,6 +32,7 @@ describe('DEFAULT_CONFIG', () => {
31
32
  expect(DEFAULT_CONFIG.autoPullPush).toBe(false);
32
33
  expect(DEFAULT_CONFIG.confirmDestructiveOps).toBe(true);
33
34
  expect(DEFAULT_CONFIG.maxConcurrency).toBe(0);
35
+ expect(DEFAULT_CONFIG.aliases).toEqual({});
34
36
  });
35
37
  });
36
38
 
@@ -54,3 +56,25 @@ describe('CONFIG_DESCRIPTIONS', () => {
54
56
  }
55
57
  });
56
58
  });
59
+
60
+ describe('CONFIG_DEFINITIONS — allowedValues', () => {
61
+ it('terminalApp 的 allowedValues 与 VALID_TERMINAL_APPS 一致', () => {
62
+ expect(CONFIG_DEFINITIONS.terminalApp.allowedValues).toEqual(VALID_TERMINAL_APPS);
63
+ });
64
+
65
+ it('有 allowedValues 的配置项,defaultValue 必须在其中', () => {
66
+ for (const [, def] of Object.entries(CONFIG_DEFINITIONS)) {
67
+ if (def.allowedValues) {
68
+ expect(def.allowedValues).toContain(def.defaultValue);
69
+ }
70
+ }
71
+ });
72
+
73
+ it('非 string 类型的配置项不应有 allowedValues', () => {
74
+ for (const [, def] of Object.entries(CONFIG_DEFINITIONS)) {
75
+ if (typeof def.defaultValue !== 'string') {
76
+ expect(def.allowedValues).toBeUndefined();
77
+ }
78
+ }
79
+ });
80
+ });
@@ -37,6 +37,8 @@ describe('MESSAGES', () => {
37
37
  'SYNC_SELECT_BRANCH',
38
38
  'PULL_CONFLICT',
39
39
  'PUSH_FAILED',
40
+ 'ALIAS_LIST_EMPTY',
41
+ 'ALIAS_LIST_TITLE',
40
42
  ] as const;
41
43
 
42
44
  it.each(stringKeys)('%s 是非空字符串', (key) => {
@@ -236,5 +238,31 @@ describe('MESSAGES', () => {
236
238
  const result = MESSAGES.SYNC_MULTIPLE_MATCHES('feat');
237
239
  expect(result).toContain('feat');
238
240
  });
241
+
242
+ it('ALIAS_SET_SUCCESS 包含别名和命令', () => {
243
+ const result = MESSAGES.ALIAS_SET_SUCCESS('ls', 'list');
244
+ expect(result).toContain('ls');
245
+ expect(result).toContain('list');
246
+ });
247
+
248
+ it('ALIAS_REMOVE_SUCCESS 包含别名', () => {
249
+ const result = MESSAGES.ALIAS_REMOVE_SUCCESS('ls');
250
+ expect(result).toContain('ls');
251
+ });
252
+
253
+ it('ALIAS_NOT_FOUND 包含别名', () => {
254
+ const result = MESSAGES.ALIAS_NOT_FOUND('xxx');
255
+ expect(result).toContain('xxx');
256
+ });
257
+
258
+ it('ALIAS_CONFLICTS_BUILTIN 包含别名', () => {
259
+ const result = MESSAGES.ALIAS_CONFLICTS_BUILTIN('list');
260
+ expect(result).toContain('list');
261
+ });
262
+
263
+ it('ALIAS_TARGET_NOT_FOUND 包含命令名', () => {
264
+ const result = MESSAGES.ALIAS_TARGET_NOT_FOUND('xxx');
265
+ expect(result).toContain('xxx');
266
+ });
239
267
  });
240
268
  });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Command } from 'commander';
3
+
4
+ vi.mock('../../../src/logger/index.js', () => ({
5
+ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
6
+ }));
7
+
8
+ import { applyAliases } from '../../../src/utils/alias.js';
9
+
10
+ describe('applyAliases', () => {
11
+ it('为已注册命令添加别名', () => {
12
+ const program = new Command();
13
+ const listCmd = program.command('list').action(() => {});
14
+
15
+ applyAliases(program, { ls: 'list' });
16
+
17
+ expect(listCmd.aliases()).toContain('ls');
18
+ });
19
+
20
+ it('目标命令不存在时静默跳过', () => {
21
+ const program = new Command();
22
+ program.command('list').action(() => {});
23
+
24
+ // 不应抛出异常
25
+ applyAliases(program, { xx: 'nonexistent' });
26
+
27
+ const listCmd = program.commands.find((c) => c.name() === 'list');
28
+ expect(listCmd!.aliases()).not.toContain('xx');
29
+ });
30
+
31
+ it('空别名映射时不做任何操作', () => {
32
+ const program = new Command();
33
+ program.command('list').action(() => {});
34
+
35
+ applyAliases(program, {});
36
+
37
+ const listCmd = program.commands.find((c) => c.name() === 'list');
38
+ expect(listCmd!.aliases()).toEqual([]);
39
+ });
40
+
41
+ it('支持多个别名映射到不同命令', () => {
42
+ const program = new Command();
43
+ const listCmd = program.command('list').action(() => {});
44
+ const removeCmd = program.command('remove').action(() => {});
45
+
46
+ applyAliases(program, { ls: 'list', rm: 'remove' });
47
+
48
+ expect(listCmd.aliases()).toContain('ls');
49
+ expect(removeCmd.aliases()).toContain('rm');
50
+ });
51
+ });