clawt 3.9.12 → 3.10.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.
Files changed (111) hide show
  1. package/.clawt/postCreate.sh +0 -0
  2. package/README.md +3 -0
  3. package/README.zh-CN.md +3 -0
  4. package/dist/index.js +1936 -592
  5. package/dist/postinstall.js +1626 -283
  6. package/docs/config-file.md +2 -0
  7. package/docs/config.md +2 -1
  8. package/docs/init.md +3 -2
  9. package/docs/project-config.md +10 -1
  10. package/docs/spec.md +69 -2
  11. package/docs/status.md +2 -1
  12. package/package.json +12 -11
  13. package/scripts/release.sh +2 -2
  14. package/src/commands/alias.ts +5 -4
  15. package/src/commands/completion.ts +2 -1
  16. package/src/commands/config.ts +25 -7
  17. package/src/commands/cover-validate.ts +3 -2
  18. package/src/commands/create.ts +8 -7
  19. package/src/commands/home.ts +2 -1
  20. package/src/commands/init.ts +13 -6
  21. package/src/commands/list.ts +6 -4
  22. package/src/commands/merge.ts +8 -7
  23. package/src/commands/projects.ts +5 -3
  24. package/src/commands/remove.ts +7 -6
  25. package/src/commands/reset.ts +3 -2
  26. package/src/commands/resume.ts +10 -7
  27. package/src/commands/run.ts +8 -7
  28. package/src/commands/status.ts +16 -11
  29. package/src/commands/sync.ts +4 -3
  30. package/src/commands/tasks.ts +8 -6
  31. package/src/commands/validate.ts +7 -6
  32. package/src/constants/ai-prompts.ts +11 -11
  33. package/src/constants/config.ts +30 -0
  34. package/src/constants/index.ts +3 -2
  35. package/src/constants/messages/alias.ts +44 -14
  36. package/src/constants/messages/cli-descriptions.ts +91 -0
  37. package/src/constants/messages/common.ts +221 -36
  38. package/src/constants/messages/completion.ts +43 -14
  39. package/src/constants/messages/config.ts +61 -18
  40. package/src/constants/messages/cover-validate.ts +43 -14
  41. package/src/constants/messages/create.ts +16 -5
  42. package/src/constants/messages/home.ts +19 -6
  43. package/src/constants/messages/index.ts +2 -0
  44. package/src/constants/messages/init.ts +45 -14
  45. package/src/constants/messages/interactive-panel.ts +183 -29
  46. package/src/constants/messages/merge.ts +140 -38
  47. package/src/constants/messages/post-create.ts +59 -19
  48. package/src/constants/messages/projects.ts +51 -14
  49. package/src/constants/messages/remove.ts +50 -15
  50. package/src/constants/messages/reset.ts +14 -4
  51. package/src/constants/messages/resume.ts +116 -19
  52. package/src/constants/messages/run.ts +165 -35
  53. package/src/constants/messages/status.ts +84 -23
  54. package/src/constants/messages/sync.ts +54 -17
  55. package/src/constants/messages/tasks.ts +21 -7
  56. package/src/constants/messages/update.ts +35 -11
  57. package/src/constants/messages/validate.ts +218 -57
  58. package/src/constants/progress.ts +17 -6
  59. package/src/constants/project-config.ts +17 -0
  60. package/src/constants/prompt.ts +18 -2
  61. package/src/constants/tasks-template.ts +56 -2
  62. package/src/hooks/post-create.ts +5 -2
  63. package/src/index.ts +6 -5
  64. package/src/types/config.ts +2 -0
  65. package/src/utils/alias.ts +2 -1
  66. package/src/utils/claude.ts +10 -9
  67. package/src/utils/config-strategy.ts +3 -3
  68. package/src/utils/dry-run.ts +2 -2
  69. package/src/utils/formatter.ts +18 -11
  70. package/src/utils/i18n.ts +63 -0
  71. package/src/utils/index.ts +2 -0
  72. package/src/utils/interactive-panel-render.ts +6 -3
  73. package/src/utils/interactive-panel.ts +3 -1
  74. package/src/utils/progress-render.ts +11 -9
  75. package/src/utils/prompt.ts +2 -1
  76. package/src/utils/task-executor.ts +10 -7
  77. package/src/utils/task-file.ts +2 -1
  78. package/src/utils/terminal.ts +9 -9
  79. package/src/utils/ui-prompts.ts +4 -3
  80. package/src/utils/update-checker.ts +1 -1
  81. package/src/utils/validate-branch.ts +16 -9
  82. package/src/utils/validate-core.ts +2 -1
  83. package/src/utils/validate-runner.ts +2 -2
  84. package/src/utils/worktree-matcher.ts +9 -7
  85. package/tests/unit/commands/alias.test.ts +4 -0
  86. package/tests/unit/commands/completion.test.ts +14 -0
  87. package/tests/unit/commands/config.test.ts +61 -28
  88. package/tests/unit/commands/cover-validate.test.ts +13 -2
  89. package/tests/unit/commands/init.test.ts +6 -2
  90. package/tests/unit/commands/merge.test.ts +14 -0
  91. package/tests/unit/commands/run.test.ts +17 -0
  92. package/tests/unit/commands/tasks.test.ts +39 -9
  93. package/tests/unit/constants/config.test.ts +16 -1
  94. package/tests/unit/constants/messages-post-create.test.ts +93 -1
  95. package/tests/unit/constants/messages.test.ts +85 -1
  96. package/tests/unit/hooks/post-create.test.ts +32 -0
  97. package/tests/unit/utils/alias.test.ts +14 -0
  98. package/tests/unit/utils/claude.test.ts +24 -4
  99. package/tests/unit/utils/config-strategy.test.ts +21 -0
  100. package/tests/unit/utils/conflict-resolver.test.ts +24 -4
  101. package/tests/unit/utils/formatter.test.ts +21 -0
  102. package/tests/unit/utils/i18n.test.ts +91 -0
  103. package/tests/unit/utils/interactive-panel.test.ts +191 -0
  104. package/tests/unit/utils/progress.test.ts +39 -18
  105. package/tests/unit/utils/prompt.test.ts +25 -2
  106. package/tests/unit/utils/task-file.test.ts +73 -10
  107. package/tests/unit/utils/terminal-cmux.test.ts +19 -4
  108. package/tests/unit/utils/update-checker.test.ts +2 -0
  109. package/tests/unit/utils/validate-branch.test.ts +26 -1
  110. package/tests/unit/utils/validation.test.ts +2 -2
  111. 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 { POST_CREATE_MESSAGES } from '../../../src/constants/messages/post-create.js';
3
18
 
4
19
  describe('POST_CREATE_MESSAGES', () => {
@@ -110,3 +125,80 @@ describe('POST_CREATE_MESSAGES', () => {
110
125
  });
111
126
  });
112
127
  });
128
+
129
+ /**
130
+ * 英文版 post-create 消息测试
131
+ * 使用 vi.resetModules + vi.doMock 动态切换语言为 en,
132
+ * 然后重新加载 POST_CREATE_MESSAGES 模块验证英文版消息内容
133
+ */
134
+ describe('POST_CREATE_MESSAGES — 英文版', () => {
135
+ beforeEach(() => {
136
+ vi.resetModules();
137
+ });
138
+
139
+ it('纯字符串消息在英文版下返回英文文本', async () => {
140
+ vi.doMock('../../../src/utils/i18n.js', () => ({
141
+ getCurrentLanguage: () => 'en',
142
+ resetLanguageCache: vi.fn(),
143
+ setCurrentLanguage: vi.fn(),
144
+ createMessages: (i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
145
+ const result: any = {};
146
+ for (const key of Object.keys(i18nMap)) {
147
+ result[key] = i18nMap[key]['en'];
148
+ }
149
+ return result;
150
+ },
151
+ }));
152
+
153
+ const { POST_CREATE_MESSAGES: EN_MSGS } = await import('../../../src/constants/messages/post-create.js');
154
+
155
+ // HOOK_SKIPPED 英文版包含 Skipped
156
+ expect(EN_MSGS.HOOK_SKIPPED).toContain('Skipped');
157
+
158
+ // HOOK_NOT_CONFIGURED 英文版包含 "not configured" 和 "skipping"
159
+ expect(EN_MSGS.HOOK_NOT_CONFIGURED).toContain('not configured');
160
+ expect(EN_MSGS.HOOK_NOT_CONFIGURED).toContain('skipping');
161
+ });
162
+
163
+ it('模板函数消息在英文版下返回英文文本', async () => {
164
+ vi.doMock('../../../src/utils/i18n.js', () => ({
165
+ getCurrentLanguage: () => 'en',
166
+ resetLanguageCache: vi.fn(),
167
+ setCurrentLanguage: vi.fn(),
168
+ createMessages: (i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
169
+ const result: any = {};
170
+ for (const key of Object.keys(i18nMap)) {
171
+ result[key] = i18nMap[key]['en'];
172
+ }
173
+ return result;
174
+ },
175
+ }));
176
+
177
+ const { POST_CREATE_MESSAGES: EN_MSGS } = await import('../../../src/constants/messages/post-create.js');
178
+
179
+ // HOOK_SOURCE_INFO 英文版包含 "postCreate hook source"
180
+ expect(EN_MSGS.HOOK_SOURCE_INFO('project config')).toContain('postCreate hook source');
181
+
182
+ // HOOK_SUCCESS 英文版包含 "successfully" 而非 "成功"
183
+ expect(EN_MSGS.HOOK_SUCCESS('feat-login')).toContain('successfully');
184
+ expect(EN_MSGS.HOOK_SUCCESS('feat-login')).not.toContain('成功');
185
+
186
+ // HOOK_FAILED 英文版包含 "failed" 而非 "失败"
187
+ expect(EN_MSGS.HOOK_FAILED('feat-login', 'error')).toContain('failed');
188
+ expect(EN_MSGS.HOOK_FAILED('feat-login', 'error')).not.toContain('失败');
189
+
190
+ // HOOK_SUMMARY 英文版包含 "succeeded"/"failed" 而非 "成功"/"失败"
191
+ const summary = EN_MSGS.HOOK_SUMMARY(5, 0);
192
+ expect(summary).toContain('5 succeeded');
193
+ expect(summary).toContain('0 failed');
194
+
195
+ // POST_CREATE_SCRIPT_NOT_EXECUTABLE 英文版包含 "not executable"
196
+ expect(EN_MSGS.POST_CREATE_SCRIPT_NOT_EXECUTABLE('/repo/.clawt/postCreate.sh')).toContain('not executable');
197
+
198
+ // POST_CREATE_SCRIPT_AUTO_CHMOD 英文版包含 "auto-added execute permission"
199
+ expect(EN_MSGS.POST_CREATE_SCRIPT_AUTO_CHMOD('/repo/.clawt/postCreate.sh')).toContain('auto-added execute permission');
200
+
201
+ // HOOK_BACKGROUND_START 英文版包含 "running in background"
202
+ expect(EN_MSGS.HOOK_BACKGROUND_START(3, 'npm install')).toContain('running in background');
203
+ });
204
+ });
@@ -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('启动 Claude Code 失败');
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('退出码: 1'));
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
+ });