clawt 3.9.13 → 3.10.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 +80 -1
- package/README.zh-CN.md +81 -0
- package/dist/index.js +1935 -592
- package/dist/postinstall.js +1626 -283
- package/docs/config-file.md +2 -0
- package/docs/config.md +2 -1
- package/docs/init.md +3 -2
- package/docs/project-config.md +10 -1
- package/docs/spec.md +69 -2
- package/package.json +1 -1
- package/src/commands/alias.ts +5 -4
- package/src/commands/completion.ts +2 -1
- package/src/commands/config.ts +25 -7
- package/src/commands/cover-validate.ts +3 -2
- package/src/commands/create.ts +8 -7
- package/src/commands/home.ts +2 -1
- package/src/commands/init.ts +13 -6
- package/src/commands/list.ts +6 -4
- package/src/commands/merge.ts +8 -7
- package/src/commands/projects.ts +5 -3
- package/src/commands/remove.ts +7 -6
- package/src/commands/reset.ts +3 -2
- package/src/commands/resume.ts +10 -7
- package/src/commands/run.ts +8 -7
- package/src/commands/status.ts +16 -11
- package/src/commands/sync.ts +4 -3
- package/src/commands/tasks.ts +8 -6
- package/src/commands/validate.ts +7 -6
- package/src/constants/ai-prompts.ts +11 -11
- package/src/constants/config.ts +30 -0
- package/src/constants/index.ts +3 -2
- package/src/constants/messages/alias.ts +44 -14
- package/src/constants/messages/cli-descriptions.ts +91 -0
- package/src/constants/messages/common.ts +221 -36
- package/src/constants/messages/completion.ts +43 -14
- package/src/constants/messages/config.ts +61 -18
- package/src/constants/messages/cover-validate.ts +43 -14
- package/src/constants/messages/create.ts +16 -5
- package/src/constants/messages/home.ts +19 -6
- package/src/constants/messages/index.ts +2 -0
- package/src/constants/messages/init.ts +45 -14
- package/src/constants/messages/interactive-panel.ts +183 -29
- package/src/constants/messages/merge.ts +140 -38
- package/src/constants/messages/post-create.ts +59 -19
- package/src/constants/messages/projects.ts +51 -14
- package/src/constants/messages/remove.ts +50 -15
- package/src/constants/messages/reset.ts +14 -4
- package/src/constants/messages/resume.ts +116 -19
- package/src/constants/messages/run.ts +165 -35
- package/src/constants/messages/status.ts +84 -23
- package/src/constants/messages/sync.ts +54 -17
- package/src/constants/messages/tasks.ts +21 -7
- package/src/constants/messages/update.ts +35 -11
- package/src/constants/messages/validate.ts +218 -57
- package/src/constants/progress.ts +17 -6
- package/src/constants/project-config.ts +17 -0
- package/src/constants/prompt.ts +18 -2
- package/src/constants/tasks-template.ts +56 -2
- package/src/hooks/post-create.ts +5 -2
- package/src/index.ts +6 -5
- package/src/types/config.ts +2 -0
- package/src/utils/alias.ts +2 -1
- package/src/utils/claude.ts +10 -9
- package/src/utils/config-strategy.ts +3 -3
- package/src/utils/dry-run.ts +2 -2
- package/src/utils/formatter.ts +18 -11
- package/src/utils/i18n.ts +63 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/interactive-panel-render.ts +6 -3
- package/src/utils/progress-render.ts +11 -9
- package/src/utils/prompt.ts +2 -1
- package/src/utils/task-executor.ts +10 -7
- package/src/utils/task-file.ts +2 -1
- package/src/utils/terminal.ts +9 -9
- package/src/utils/ui-prompts.ts +4 -3
- package/src/utils/update-checker.ts +1 -1
- package/src/utils/validate-branch.ts +16 -9
- package/src/utils/validate-core.ts +2 -1
- package/src/utils/validate-runner.ts +2 -2
- package/src/utils/worktree-matcher.ts +9 -7
- package/tests/unit/commands/alias.test.ts +4 -0
- package/tests/unit/commands/completion.test.ts +14 -0
- package/tests/unit/commands/config.test.ts +61 -28
- package/tests/unit/commands/cover-validate.test.ts +13 -2
- package/tests/unit/commands/init.test.ts +6 -2
- package/tests/unit/commands/merge.test.ts +14 -0
- package/tests/unit/commands/run.test.ts +17 -0
- package/tests/unit/commands/tasks.test.ts +39 -9
- package/tests/unit/constants/config.test.ts +16 -1
- package/tests/unit/constants/messages-post-create.test.ts +93 -1
- package/tests/unit/constants/messages.test.ts +85 -1
- package/tests/unit/hooks/post-create.test.ts +32 -0
- package/tests/unit/utils/alias.test.ts +14 -0
- package/tests/unit/utils/claude.test.ts +24 -4
- package/tests/unit/utils/config-strategy.test.ts +21 -0
- package/tests/unit/utils/conflict-resolver.test.ts +24 -4
- package/tests/unit/utils/formatter.test.ts +21 -0
- package/tests/unit/utils/i18n.test.ts +91 -0
- package/tests/unit/utils/progress.test.ts +39 -18
- package/tests/unit/utils/prompt.test.ts +25 -2
- package/tests/unit/utils/task-file.test.ts +73 -10
- package/tests/unit/utils/terminal-cmux.test.ts +19 -4
- package/tests/unit/utils/update-checker.test.ts +2 -0
- package/tests/unit/utils/validate-branch.test.ts +26 -1
- package/tests/unit/utils/validation.test.ts +2 -2
- package/tests/unit/utils/worktree-matcher.test.ts +2 -0
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'zh-CN' 以匹配中文断言
|
|
4
|
+
vi.mock('../../../src/utils/i18n.js', () => ({
|
|
5
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
6
|
+
resetLanguageCache: vi.fn(),
|
|
7
|
+
setCurrentLanguage: vi.fn(),
|
|
8
|
+
createMessages: vi.fn((i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
9
|
+
const result: any = {};
|
|
10
|
+
for (const key of Object.keys(i18nMap)) {
|
|
11
|
+
result[key] = i18nMap[key]['zh-CN'];
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
2
17
|
import { MESSAGES } from '../../../src/constants/messages/index.js';
|
|
3
18
|
|
|
4
19
|
describe('MESSAGES', () => {
|
|
@@ -271,3 +286,72 @@ describe('MESSAGES', () => {
|
|
|
271
286
|
});
|
|
272
287
|
});
|
|
273
288
|
});
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 英文版消息测试
|
|
292
|
+
* 使用 vi.resetModules + vi.doMock 动态切换语言为 en,
|
|
293
|
+
* 然后重新加载 MESSAGES 模块验证英文版消息内容
|
|
294
|
+
*/
|
|
295
|
+
describe('MESSAGES — 英文版', () => {
|
|
296
|
+
beforeEach(() => {
|
|
297
|
+
vi.resetModules();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('纯字符串消息在英文版下返回英文文本', async () => {
|
|
301
|
+
vi.doMock('../../../src/utils/i18n.js', () => ({
|
|
302
|
+
getCurrentLanguage: () => 'en',
|
|
303
|
+
resetLanguageCache: vi.fn(),
|
|
304
|
+
setCurrentLanguage: vi.fn(),
|
|
305
|
+
createMessages: (i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
306
|
+
const result: any = {};
|
|
307
|
+
for (const key of Object.keys(i18nMap)) {
|
|
308
|
+
result[key] = i18nMap[key]['en'];
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
},
|
|
312
|
+
}));
|
|
313
|
+
|
|
314
|
+
const { MESSAGES: EN_MESSAGES } = await import('../../../src/constants/messages/index.js');
|
|
315
|
+
|
|
316
|
+
// 验证部分关键纯字符串消息是英文而非中文
|
|
317
|
+
expect(EN_MESSAGES.NO_WORKTREES).toBe('(No worktrees)');
|
|
318
|
+
expect(EN_MESSAGES.MAIN_WORKTREE_DIRTY).toBe('Main worktree has uncommitted changes. Please resolve first');
|
|
319
|
+
expect(EN_MESSAGES.MERGE_CONFLICT).toContain('Merge has conflicts');
|
|
320
|
+
expect(EN_MESSAGES.DESTRUCTIVE_OP_CANCELLED).toBe('Operation cancelled');
|
|
321
|
+
expect(EN_MESSAGES.CONFIG_CORRUPTED).toContain('Config file corrupted');
|
|
322
|
+
expect(EN_MESSAGES.PULL_CONFLICT).toContain('Conflict during auto-pull');
|
|
323
|
+
expect(EN_MESSAGES.PUSH_FAILED).toContain('Auto-push failed');
|
|
324
|
+
expect(EN_MESSAGES.ALIAS_LIST_EMPTY).toBe('(No aliases)');
|
|
325
|
+
expect(EN_MESSAGES.ALIAS_LIST_TITLE).toBe('Current aliases:');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('模板函数消息在英文版下返回英文文本', async () => {
|
|
329
|
+
vi.doMock('../../../src/utils/i18n.js', () => ({
|
|
330
|
+
getCurrentLanguage: () => 'en',
|
|
331
|
+
resetLanguageCache: vi.fn(),
|
|
332
|
+
setCurrentLanguage: vi.fn(),
|
|
333
|
+
createMessages: (i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
334
|
+
const result: any = {};
|
|
335
|
+
for (const key of Object.keys(i18nMap)) {
|
|
336
|
+
result[key] = i18nMap[key]['en'];
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
},
|
|
340
|
+
}));
|
|
341
|
+
|
|
342
|
+
const { MESSAGES: EN_MESSAGES } = await import('../../../src/constants/messages/index.js');
|
|
343
|
+
|
|
344
|
+
// MERGE_SUCCESS 英文版用 "Pushed to remote" 而非 "已推送"
|
|
345
|
+
expect(EN_MESSAGES.MERGE_SUCCESS('feat-a', 'fix bug', true)).toContain('Pushed to remote');
|
|
346
|
+
expect(EN_MESSAGES.MERGE_SUCCESS('feat-a', 'fix bug', false)).not.toContain('Pushed to remote');
|
|
347
|
+
|
|
348
|
+
// BRANCH_EXISTS 英文版包含 "already exists"
|
|
349
|
+
expect(EN_MESSAGES.BRANCH_EXISTS('feature-a')).toContain('already exists');
|
|
350
|
+
|
|
351
|
+
// WORKTREE_CREATED 英文版用 "Created" 而非 "已创建"
|
|
352
|
+
expect(EN_MESSAGES.WORKTREE_CREATED(3)).toContain('Created');
|
|
353
|
+
|
|
354
|
+
// ALIAS_SET_SUCCESS 英文版用 "Alias set" 而非 "已设置别名"
|
|
355
|
+
expect(EN_MESSAGES.ALIAS_SET_SUCCESS('ls', 'list')).toContain('Alias set');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -5,6 +5,38 @@ vi.mock('../../../src/logger/index.js', () => ({
|
|
|
5
5
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'zh-CN' 以匹配中文断言
|
|
9
|
+
// 同时导出 createMessages 供 constants 模块使用
|
|
10
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
15
|
+
resetLanguageCache: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// mock constants 消息(post-create.ts 通过 MESSAGES 使用了 i18n 消息)
|
|
20
|
+
vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
MESSAGES: {
|
|
25
|
+
...actual.MESSAGES,
|
|
26
|
+
// 覆盖 post-create 相关的 MESSAGES,确保中文输出
|
|
27
|
+
POST_CREATE_SCRIPT_AUTO_CHMOD: (path: string) =>
|
|
28
|
+
`${path} 不可执行,已自动添加执行权限`,
|
|
29
|
+
POST_CREATE_SCRIPT_NOT_EXECUTABLE: (path: string) =>
|
|
30
|
+
`检测到 ${path} 但不可执行,自动添加权限失败,请手动执行 chmod +x ${path}`,
|
|
31
|
+
HOOK_SKIPPED: '已跳过 postCreate hook(--no-post-create)',
|
|
32
|
+
HOOK_NOT_CONFIGURED: '未配置 postCreate hook,跳过',
|
|
33
|
+
HOOK_SOURCE_INFO: (source: string) => `postCreate hook 来源: ${source}`,
|
|
34
|
+
HOOK_BACKGROUND_START: (count: number, command: string) =>
|
|
35
|
+
`postCreate hook 正在后台执行 (${count} 个 worktree): ${command}`,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
8
40
|
// mock node:fs
|
|
9
41
|
vi.mock('node:fs', () => ({
|
|
10
42
|
existsSync: vi.fn(),
|
|
@@ -5,6 +5,20 @@ vi.mock('../../../src/logger/index.js', () => ({
|
|
|
5
5
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
|
+
// mock i18n 模块,避免循环依赖导致 currentLanguage 未初始化
|
|
9
|
+
vi.mock('../../../src/utils/i18n.js', () => ({
|
|
10
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
11
|
+
resetLanguageCache: vi.fn(),
|
|
12
|
+
setCurrentLanguage: vi.fn(),
|
|
13
|
+
createMessages: vi.fn((i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
14
|
+
const result: any = {};
|
|
15
|
+
for (const key of Object.keys(i18nMap)) {
|
|
16
|
+
result[key] = i18nMap[key]['zh-CN'];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
8
22
|
import { applyAliases } from '../../../src/utils/alias.js';
|
|
9
23
|
|
|
10
24
|
describe('applyAliases', () => {
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'en' 以匹配英文断言
|
|
4
|
+
// 同时重写 createMessages 使其直接选择 'en' 分支,确保 constants 模块加载时生成英文消息
|
|
5
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
getCurrentLanguage: vi.fn().mockReturnValue('en'),
|
|
10
|
+
resetLanguageCache: vi.fn(),
|
|
11
|
+
createMessages: <T extends Record<string, { en: any; 'zh-CN': any }>>(
|
|
12
|
+
i18nMap: T,
|
|
13
|
+
) => {
|
|
14
|
+
const result: any = {};
|
|
15
|
+
for (const key of Object.keys(i18nMap)) {
|
|
16
|
+
result[key] = i18nMap[key]['en'];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
3
23
|
// mock logger(避免测试时写日志文件)
|
|
4
24
|
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
25
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
@@ -177,7 +197,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
177
197
|
});
|
|
178
198
|
|
|
179
199
|
expect(() => launchInteractiveClaude(worktree)).toThrow(ClawtError);
|
|
180
|
-
expect(() => launchInteractiveClaude(worktree)).toThrow('
|
|
200
|
+
expect(() => launchInteractiveClaude(worktree)).toThrow('Failed to start Claude Code');
|
|
181
201
|
});
|
|
182
202
|
|
|
183
203
|
it('非零退出码时调用 printWarning', () => {
|
|
@@ -195,7 +215,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
195
215
|
|
|
196
216
|
launchInteractiveClaude(worktree);
|
|
197
217
|
|
|
198
|
-
expect(mockedPrintWarning).toHaveBeenCalledWith(expect.stringContaining('
|
|
218
|
+
expect(mockedPrintWarning).toHaveBeenCalledWith(expect.stringContaining('exit code: 1'));
|
|
199
219
|
});
|
|
200
220
|
|
|
201
221
|
it('退出码为 null 时不调用 printWarning', () => {
|
|
@@ -252,7 +272,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
252
272
|
|
|
253
273
|
const callArgs = mockedSpawnSync.mock.calls[0][1] as string[];
|
|
254
274
|
expect(callArgs).toContain('--continue');
|
|
255
|
-
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('
|
|
275
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('Continue previous session'));
|
|
256
276
|
});
|
|
257
277
|
|
|
258
278
|
it('autoContinue 启用但无会话历史时不追加 --continue 参数', () => {
|
|
@@ -272,7 +292,7 @@ describe('launchInteractiveClaude', () => {
|
|
|
272
292
|
|
|
273
293
|
const callArgs = mockedSpawnSync.mock.calls[0][1] as string[];
|
|
274
294
|
expect(callArgs).not.toContain('--continue');
|
|
275
|
-
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('
|
|
295
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('New session'));
|
|
276
296
|
});
|
|
277
297
|
|
|
278
298
|
it('不传 autoContinue 时即使有会话历史也不追加 --continue', () => {
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
|
|
3
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'zh-CN' 以匹配中文断言
|
|
4
|
+
// 同时重写 createMessages 使其直接选择 'zh-CN' 分支,确保 constants 模块加载时生成中文消息
|
|
5
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
10
|
+
resetLanguageCache: vi.fn(),
|
|
11
|
+
createMessages: <T extends Record<string, { en: any; 'zh-CN': any }>>(
|
|
12
|
+
i18nMap: T,
|
|
13
|
+
) => {
|
|
14
|
+
const result: any = {};
|
|
15
|
+
for (const key of Object.keys(i18nMap)) {
|
|
16
|
+
result[key] = i18nMap[key]['zh-CN'];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
3
23
|
// mock enquirer(必须在所有 import 之前)
|
|
4
24
|
const { mockSelectRun, mockInputRun } = vi.hoisted(() => {
|
|
5
25
|
const mockSelectRun = vi.fn();
|
|
@@ -49,6 +69,7 @@ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
|
49
69
|
`配置项 ${key} 仅接受以下值: ${validValues.join(', ')}`,
|
|
50
70
|
CONFIG_INPUT_PROMPT: (key: string) => `输入 ${key} 的新值`,
|
|
51
71
|
CONFIG_SELECT_PROMPT: '选择要修改的配置项',
|
|
72
|
+
NOT_SET: '(未设置)',
|
|
52
73
|
},
|
|
53
74
|
};
|
|
54
75
|
});
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
|
|
3
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'zh-CN' 以匹配中文断言
|
|
4
|
+
// 同时重写 createMessages 使其直接选择 'zh-CN' 分支,确保 constants 模块加载时生成中文消息
|
|
5
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
10
|
+
resetLanguageCache: vi.fn(),
|
|
11
|
+
createMessages: <T extends Record<string, { en: any; 'zh-CN': any }>>(
|
|
12
|
+
i18nMap: T,
|
|
13
|
+
) => {
|
|
14
|
+
const result: any = {};
|
|
15
|
+
for (const key of Object.keys(i18nMap)) {
|
|
16
|
+
result[key] = i18nMap[key]['zh-CN'];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
3
23
|
// mock logger
|
|
4
24
|
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
25
|
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
@@ -97,12 +117,12 @@ beforeEach(() => {
|
|
|
97
117
|
describe('buildConflictResolvePrompt', () => {
|
|
98
118
|
it('生成纯指令性 prompt(无参数)', () => {
|
|
99
119
|
const prompt = buildConflictResolvePrompt();
|
|
100
|
-
|
|
101
|
-
expect(prompt).toContain('Git
|
|
120
|
+
// CONFLICT_RESOLVE_PROMPT 始终为英文,不跟随语言切换
|
|
121
|
+
expect(prompt).toContain('Git merge conflict resolution expert');
|
|
102
122
|
expect(prompt).toContain('git status');
|
|
103
123
|
expect(prompt).toContain('git log');
|
|
104
|
-
expect(prompt).toContain('
|
|
105
|
-
expect(prompt).toContain('
|
|
124
|
+
expect(prompt).toContain('conflict markers');
|
|
125
|
+
expect(prompt).toContain('Please begin.');
|
|
106
126
|
});
|
|
107
127
|
});
|
|
108
128
|
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'zh-CN' 以匹配中文断言
|
|
4
|
+
// 同时重写 createMessages 使其直接选择 'zh-CN' 分支,确保 constants 模块加载时生成中文消息
|
|
5
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
10
|
+
resetLanguageCache: vi.fn(),
|
|
11
|
+
createMessages: <T extends Record<string, { en: any; 'zh-CN': any }>>(
|
|
12
|
+
i18nMap: T,
|
|
13
|
+
) => {
|
|
14
|
+
const result: any = {};
|
|
15
|
+
for (const key of Object.keys(i18nMap)) {
|
|
16
|
+
result[key] = i18nMap[key]['zh-CN'];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
2
23
|
import { formatWorktreeStatus, printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, isWorktreeIdle, formatDuration, formatRelativeTime, formatDiskSize, formatLocalISOString, generateTaskFilename } from '../../../src/utils/formatter.js';
|
|
3
24
|
import { createWorktreeStatus } from '../../helpers/fixtures.js';
|
|
4
25
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('i18n', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.resetModules();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
describe('getCurrentLanguage', () => {
|
|
9
|
+
it('should return en when config has language: en', async () => {
|
|
10
|
+
vi.doMock('../../../src/utils/config.js', () => ({
|
|
11
|
+
loadConfig: () => ({ language: 'en' }),
|
|
12
|
+
}));
|
|
13
|
+
const { getCurrentLanguage } = await import('../../../src/utils/i18n.js');
|
|
14
|
+
expect(getCurrentLanguage()).toBe('en');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return zh-CN when config has language: zh-CN', async () => {
|
|
18
|
+
vi.doMock('../../../src/utils/config.js', () => ({
|
|
19
|
+
loadConfig: () => ({ language: 'zh-CN' }),
|
|
20
|
+
}));
|
|
21
|
+
const { getCurrentLanguage } = await import('../../../src/utils/i18n.js');
|
|
22
|
+
expect(getCurrentLanguage()).toBe('zh-CN');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should default to en when language is not set', async () => {
|
|
26
|
+
vi.doMock('../../../src/utils/config.js', () => ({
|
|
27
|
+
loadConfig: () => ({}),
|
|
28
|
+
}));
|
|
29
|
+
const { getCurrentLanguage } = await import('../../../src/utils/i18n.js');
|
|
30
|
+
expect(getCurrentLanguage()).toBe('en');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should default to en when loadConfig throws', async () => {
|
|
34
|
+
vi.doMock('../../../src/utils/config.js', () => ({
|
|
35
|
+
loadConfig: () => { throw new Error('file not found'); },
|
|
36
|
+
}));
|
|
37
|
+
const { getCurrentLanguage } = await import('../../../src/utils/i18n.js');
|
|
38
|
+
expect(getCurrentLanguage()).toBe('en');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('setCurrentLanguage / resetLanguageCache', () => {
|
|
43
|
+
it('should use cached language after setCurrentLanguage', async () => {
|
|
44
|
+
vi.doMock('../../../src/utils/config.js', () => ({
|
|
45
|
+
loadConfig: () => ({ language: 'zh-CN' }),
|
|
46
|
+
}));
|
|
47
|
+
const { getCurrentLanguage, setCurrentLanguage } = await import('../../../src/utils/i18n.js');
|
|
48
|
+
setCurrentLanguage('en');
|
|
49
|
+
expect(getCurrentLanguage()).toBe('en');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should reload from config after resetLanguageCache', async () => {
|
|
53
|
+
vi.doMock('../../../src/utils/config.js', () => ({
|
|
54
|
+
loadConfig: () => ({ language: 'zh-CN' }),
|
|
55
|
+
}));
|
|
56
|
+
const { getCurrentLanguage, setCurrentLanguage, resetLanguageCache } = await import('../../../src/utils/i18n.js');
|
|
57
|
+
setCurrentLanguage('en');
|
|
58
|
+
expect(getCurrentLanguage()).toBe('en');
|
|
59
|
+
resetLanguageCache();
|
|
60
|
+
expect(getCurrentLanguage()).toBe('zh-CN');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('createMessages', () => {
|
|
65
|
+
it('should return en messages when language is en', async () => {
|
|
66
|
+
vi.doMock('../../../src/utils/config.js', () => ({
|
|
67
|
+
loadConfig: () => ({ language: 'en' }),
|
|
68
|
+
}));
|
|
69
|
+
const { createMessages } = await import('../../../src/utils/i18n.js');
|
|
70
|
+
const messages = createMessages({
|
|
71
|
+
FOO: { en: 'Hello', 'zh-CN': '你好' },
|
|
72
|
+
BAR: { en: (n: number) => `${n} items`, 'zh-CN': (n: number) => `${n} 个` },
|
|
73
|
+
});
|
|
74
|
+
expect(messages.FOO).toBe('Hello');
|
|
75
|
+
expect(messages.BAR(3)).toBe('3 items');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return zh-CN messages when language is zh-CN', async () => {
|
|
79
|
+
vi.doMock('../../../src/utils/config.js', () => ({
|
|
80
|
+
loadConfig: () => ({ language: 'zh-CN' }),
|
|
81
|
+
}));
|
|
82
|
+
const { createMessages } = await import('../../../src/utils/i18n.js');
|
|
83
|
+
const messages = createMessages({
|
|
84
|
+
FOO: { en: 'Hello', 'zh-CN': '你好' },
|
|
85
|
+
BAR: { en: (n: number) => `${n} items`, 'zh-CN': (n: number) => `${n} 个` },
|
|
86
|
+
});
|
|
87
|
+
expect(messages.FOO).toBe('你好');
|
|
88
|
+
expect(messages.BAR(3)).toBe('3 个');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'en' 以匹配英文断言
|
|
4
|
+
// 同时重写 createMessages 使其直接选择 'en' 分支,确保 constants 模块加载时生成英文消息
|
|
5
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
getCurrentLanguage: vi.fn().mockReturnValue('en'),
|
|
10
|
+
resetLanguageCache: vi.fn(),
|
|
11
|
+
createMessages: <T extends Record<string, { en: any; 'zh-CN': any }>>(
|
|
12
|
+
i18nMap: T,
|
|
13
|
+
) => {
|
|
14
|
+
const result: any = {};
|
|
15
|
+
for (const key of Object.keys(i18nMap)) {
|
|
16
|
+
result[key] = i18nMap[key]['en'];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
2
23
|
import { ProgressRenderer } from '../../../src/utils/progress.js';
|
|
3
24
|
|
|
4
25
|
describe('ProgressRenderer', () => {
|
|
@@ -66,7 +87,7 @@ describe('ProgressRenderer', () => {
|
|
|
66
87
|
// 第二列应显示路径
|
|
67
88
|
expect(allOutput).toContain('/path/feat-1');
|
|
68
89
|
expect(allOutput).toContain('/path/feat-2');
|
|
69
|
-
expect(allOutput).toContain('
|
|
90
|
+
expect(allOutput).toContain('Running');
|
|
70
91
|
// running 状态下不显示额外路径信息
|
|
71
92
|
|
|
72
93
|
renderer.stop();
|
|
@@ -82,7 +103,7 @@ describe('ProgressRenderer', () => {
|
|
|
82
103
|
|
|
83
104
|
const allOutput = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
84
105
|
expect(allOutput).toContain('✓');
|
|
85
|
-
expect(allOutput).toContain('
|
|
106
|
+
expect(allOutput).toContain('Done');
|
|
86
107
|
expect(allOutput).toContain('5.0s');
|
|
87
108
|
expect(allOutput).toContain('$0.05');
|
|
88
109
|
// 第二列显示路径
|
|
@@ -101,7 +122,7 @@ describe('ProgressRenderer', () => {
|
|
|
101
122
|
|
|
102
123
|
const allOutput = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
103
124
|
expect(allOutput).toContain('✓');
|
|
104
|
-
expect(allOutput).toContain('
|
|
125
|
+
expect(allOutput).toContain('Done');
|
|
105
126
|
expect(allOutput).toContain('5.0s');
|
|
106
127
|
expect(allOutput).toContain('$0.05');
|
|
107
128
|
// 第二列仍显示路径
|
|
@@ -118,7 +139,7 @@ describe('ProgressRenderer', () => {
|
|
|
118
139
|
|
|
119
140
|
const allOutput = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
120
141
|
expect(allOutput).toContain('✗');
|
|
121
|
-
expect(allOutput).toContain('
|
|
142
|
+
expect(allOutput).toContain('Failed');
|
|
122
143
|
expect(allOutput).toContain('3.0s');
|
|
123
144
|
// 第二列显示路径
|
|
124
145
|
expect(allOutput).toContain('/path/feat-1');
|
|
@@ -136,7 +157,7 @@ describe('ProgressRenderer', () => {
|
|
|
136
157
|
|
|
137
158
|
const allOutput = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
138
159
|
expect(allOutput).toContain('✗');
|
|
139
|
-
expect(allOutput).toContain('
|
|
160
|
+
expect(allOutput).toContain('Failed');
|
|
140
161
|
expect(allOutput).toContain('3.0s');
|
|
141
162
|
// 第二列仍显示路径
|
|
142
163
|
expect(allOutput).toContain('/path/feat-1');
|
|
@@ -161,7 +182,7 @@ describe('ProgressRenderer', () => {
|
|
|
161
182
|
|
|
162
183
|
const allOutput = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
163
184
|
expect(allOutput).toContain('◦');
|
|
164
|
-
expect(allOutput).toContain('
|
|
185
|
+
expect(allOutput).toContain('Pending');
|
|
165
186
|
|
|
166
187
|
renderer.stop();
|
|
167
188
|
});
|
|
@@ -175,9 +196,9 @@ describe('ProgressRenderer', () => {
|
|
|
175
196
|
renderer.stop();
|
|
176
197
|
|
|
177
198
|
const allOutput = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
178
|
-
expect(allOutput).toContain('
|
|
199
|
+
expect(allOutput).toContain('Running');
|
|
179
200
|
// 第二个任务仍为排队中
|
|
180
|
-
expect(allOutput).toContain('
|
|
201
|
+
expect(allOutput).toContain('Pending');
|
|
181
202
|
});
|
|
182
203
|
|
|
183
204
|
it('allRunning=false 时面板包含汇总行', () => {
|
|
@@ -187,7 +208,7 @@ describe('ProgressRenderer', () => {
|
|
|
187
208
|
const allOutput = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
188
209
|
// 汇总行应包含排队中的计数
|
|
189
210
|
expect(allOutput).toContain('3/3');
|
|
190
|
-
expect(allOutput).toContain('
|
|
211
|
+
expect(allOutput).toContain('Pending');
|
|
191
212
|
|
|
192
213
|
renderer.stop();
|
|
193
214
|
});
|
|
@@ -230,7 +251,7 @@ describe('ProgressRenderer', () => {
|
|
|
230
251
|
const allOutput = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
231
252
|
// 第二列显示路径
|
|
232
253
|
expect(allOutput).toContain('/path/feat-1');
|
|
233
|
-
expect(allOutput).toContain('
|
|
254
|
+
expect(allOutput).toContain('Running');
|
|
234
255
|
});
|
|
235
256
|
|
|
236
257
|
it('任务完成后活动文本不再显示', () => {
|
|
@@ -269,7 +290,7 @@ describe('ProgressRenderer', () => {
|
|
|
269
290
|
const allOutput = writeSpy.mock.calls.map((c) => c[0]).join('');
|
|
270
291
|
// pending 状态第二列应显示路径,末尾无额外路径
|
|
271
292
|
expect(allOutput).toContain('/path/feat-1');
|
|
272
|
-
expect(allOutput).toContain('
|
|
293
|
+
expect(allOutput).toContain('Pending');
|
|
273
294
|
|
|
274
295
|
renderer.stop();
|
|
275
296
|
});
|
|
@@ -336,7 +357,7 @@ describe('ProgressRenderer', () => {
|
|
|
336
357
|
|
|
337
358
|
expect(logSpy).toHaveBeenCalledTimes(2);
|
|
338
359
|
expect(logSpy.mock.calls[0][0]).toContain('feat-1');
|
|
339
|
-
expect(logSpy.mock.calls[0][0]).toContain('
|
|
360
|
+
expect(logSpy.mock.calls[0][0]).toContain('started');
|
|
340
361
|
expect(logSpy.mock.calls[1][0]).toContain('feat-2');
|
|
341
362
|
|
|
342
363
|
renderer.stop();
|
|
@@ -351,7 +372,7 @@ describe('ProgressRenderer', () => {
|
|
|
351
372
|
|
|
352
373
|
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
353
374
|
expect(logSpy.mock.calls[0][0]).toContain('✓');
|
|
354
|
-
expect(logSpy.mock.calls[0][0]).toContain('
|
|
375
|
+
expect(logSpy.mock.calls[0][0]).toContain('done');
|
|
355
376
|
expect(logSpy.mock.calls[0][0]).toContain('5.0s');
|
|
356
377
|
expect(logSpy.mock.calls[0][0]).toContain('$0.05');
|
|
357
378
|
// 末尾显示结果预览
|
|
@@ -369,7 +390,7 @@ describe('ProgressRenderer', () => {
|
|
|
369
390
|
|
|
370
391
|
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
371
392
|
expect(logSpy.mock.calls[0][0]).toContain('✓');
|
|
372
|
-
expect(logSpy.mock.calls[0][0]).toContain('
|
|
393
|
+
expect(logSpy.mock.calls[0][0]).toContain('done');
|
|
373
394
|
// 无 resultPreview 时回退到 path
|
|
374
395
|
expect(logSpy.mock.calls[0][0]).toContain('/path/feat-1');
|
|
375
396
|
|
|
@@ -385,7 +406,7 @@ describe('ProgressRenderer', () => {
|
|
|
385
406
|
|
|
386
407
|
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
387
408
|
expect(logSpy.mock.calls[0][0]).toContain('✗');
|
|
388
|
-
expect(logSpy.mock.calls[0][0]).toContain('
|
|
409
|
+
expect(logSpy.mock.calls[0][0]).toContain('failed');
|
|
389
410
|
expect(logSpy.mock.calls[0][0]).toContain('3.0s');
|
|
390
411
|
// 末尾显示结果预览
|
|
391
412
|
expect(logSpy.mock.calls[0][0]).toContain('执行过程中发生错误');
|
|
@@ -402,7 +423,7 @@ describe('ProgressRenderer', () => {
|
|
|
402
423
|
|
|
403
424
|
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
404
425
|
expect(logSpy.mock.calls[0][0]).toContain('✗');
|
|
405
|
-
expect(logSpy.mock.calls[0][0]).toContain('
|
|
426
|
+
expect(logSpy.mock.calls[0][0]).toContain('failed');
|
|
406
427
|
expect(logSpy.mock.calls[0][0]).toContain('3.0s');
|
|
407
428
|
// 无 resultPreview 时回退到 path
|
|
408
429
|
expect(logSpy.mock.calls[0][0]).toContain('/path/feat-1');
|
|
@@ -438,7 +459,7 @@ describe('ProgressRenderer', () => {
|
|
|
438
459
|
|
|
439
460
|
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
440
461
|
expect(logSpy.mock.calls[0][0]).toContain('feat-1');
|
|
441
|
-
expect(logSpy.mock.calls[0][0]).toContain('
|
|
462
|
+
expect(logSpy.mock.calls[0][0]).toContain('started');
|
|
442
463
|
|
|
443
464
|
renderer.stop();
|
|
444
465
|
});
|
|
@@ -528,7 +549,7 @@ describe('ProgressRenderer', () => {
|
|
|
528
549
|
// ALT_SCREEN_LEAVE 之后应有面板最终状态输出
|
|
529
550
|
const afterLeave = allCalls.slice(leaveIndex + 1).join('');
|
|
530
551
|
expect(afterLeave).toContain('✓');
|
|
531
|
-
expect(afterLeave).toContain('
|
|
552
|
+
expect(afterLeave).toContain('Done');
|
|
532
553
|
expect(afterLeave).toContain('/path/feat-1');
|
|
533
554
|
expect(afterLeave).toContain('代码审查完成');
|
|
534
555
|
});
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
|
|
3
|
+
// mock i18n 模块,使 getCurrentLanguage 返回 'en' 以匹配英文断言
|
|
4
|
+
// 需要同时提供 createMessages 的真实实现,以便 constants 模块加载时能正确生成英文消息
|
|
5
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
6
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
7
|
+
// 在 createMessages 调用前就 mock getCurrentLanguage
|
|
8
|
+
const mockGetCurrentLanguage = vi.fn().mockReturnValue('en');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
getCurrentLanguage: mockGetCurrentLanguage,
|
|
12
|
+
resetLanguageCache: vi.fn(),
|
|
13
|
+
// 重写 createMessages 使其使用 mock 的 getCurrentLanguage
|
|
14
|
+
createMessages: <T extends Record<string, { en: any; 'zh-CN': any }>>(
|
|
15
|
+
i18nMap: T,
|
|
16
|
+
) => {
|
|
17
|
+
const result: any = {};
|
|
18
|
+
for (const key of Object.keys(i18nMap)) {
|
|
19
|
+
result[key] = i18nMap[key]['en'];
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
|
|
3
26
|
// mock 非交互模式判断函数
|
|
4
27
|
const { mockIsNonInteractive } = vi.hoisted(() => {
|
|
5
28
|
return { mockIsNonInteractive: vi.fn().mockReturnValue(false) };
|
|
@@ -95,7 +118,7 @@ describe('promptCommitMessage', () => {
|
|
|
95
118
|
|
|
96
119
|
await expect(
|
|
97
120
|
promptCommitMessage('请输入提交信息', '非交互模式错误'),
|
|
98
|
-
).rejects.toThrow('
|
|
121
|
+
).rejects.toThrow('Commit message cannot be empty');
|
|
99
122
|
});
|
|
100
123
|
|
|
101
124
|
it('用户输入仅空白字符时抛出 ClawtError', async () => {
|
|
@@ -104,7 +127,7 @@ describe('promptCommitMessage', () => {
|
|
|
104
127
|
|
|
105
128
|
await expect(
|
|
106
129
|
promptCommitMessage('请输入提交信息', '非交互模式错误'),
|
|
107
|
-
).rejects.toThrow('
|
|
130
|
+
).rejects.toThrow('Commit message cannot be empty');
|
|
108
131
|
});
|
|
109
132
|
|
|
110
133
|
it('返回 trim 后的用户输入', async () => {
|