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.
- package/.clawt/postCreate.sh +0 -0
- package/README.md +3 -0
- package/README.zh-CN.md +3 -0
- package/dist/index.js +1936 -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/docs/status.md +2 -1
- package/package.json +12 -11
- package/scripts/release.sh +2 -2
- 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/interactive-panel.ts +3 -1
- 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/interactive-panel.test.ts +191 -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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock 所有依赖,避免真实 git 命令执行
|
|
4
|
+
vi.mock('../../../src/constants/index.js', () => ({
|
|
5
|
+
CURSOR_HIDE: '',
|
|
6
|
+
CURSOR_SHOW: '',
|
|
7
|
+
LINE_WRAP_DISABLE: '',
|
|
8
|
+
LINE_WRAP_ENABLE: '',
|
|
9
|
+
SYNC_OUTPUT_START: '',
|
|
10
|
+
SYNC_OUTPUT_END: '',
|
|
11
|
+
ALT_SCREEN_ENTER: '',
|
|
12
|
+
ALT_SCREEN_LEAVE: '',
|
|
13
|
+
CLEAR_SCREEN: '',
|
|
14
|
+
CURSOR_HOME: '',
|
|
15
|
+
DEFAULT_TERMINAL_COLUMNS: 80,
|
|
16
|
+
PANEL_REFRESH_INTERVAL_MS: 5000,
|
|
17
|
+
PANEL_COUNTDOWN_INTERVAL_MS: 1000,
|
|
18
|
+
KEY_ARROW_UP: '\x1b[A',
|
|
19
|
+
KEY_ARROW_DOWN: '\x1b[B',
|
|
20
|
+
KEY_CTRL_C: 3,
|
|
21
|
+
PANEL_SHORTCUT_KEYS: {
|
|
22
|
+
VALIDATE: 'v',
|
|
23
|
+
MERGE: 'm',
|
|
24
|
+
DELETE: 'd',
|
|
25
|
+
RESUME: 'r',
|
|
26
|
+
SYNC: 's',
|
|
27
|
+
COVER: 'c',
|
|
28
|
+
REFRESH: 'f',
|
|
29
|
+
QUIT: 'q',
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('../../../src/constants/messages/index.js', () => ({
|
|
34
|
+
PANEL_NOT_TTY: 'not tty',
|
|
35
|
+
PANEL_PRESS_ENTER_TO_RETURN: 'press enter',
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock('../../../src/utils/shell.js', () => ({
|
|
39
|
+
runCommandInherited: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock('../../../src/utils/interactive-panel-render.js', () => ({
|
|
43
|
+
buildPanelFrame: vi.fn(() => ['line1', 'line2']),
|
|
44
|
+
renderFooter: vi.fn(() => 'footer'),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock('../../../src/utils/progress-render.js', () => ({
|
|
48
|
+
truncateToTerminalWidth: vi.fn((s: string) => s),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock('../../../src/utils/keyboard-controller.js', () => ({
|
|
52
|
+
KeyboardController: vi.fn(function () {
|
|
53
|
+
return {
|
|
54
|
+
start: vi.fn(),
|
|
55
|
+
stop: vi.fn(),
|
|
56
|
+
};
|
|
57
|
+
}),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
vi.mock('../../../src/utils/interactive-panel-state.js', () => ({
|
|
61
|
+
PanelStateManager: vi.fn(function () {
|
|
62
|
+
return {
|
|
63
|
+
updateData: vi.fn(),
|
|
64
|
+
getStatusResult: vi.fn(() => ({ main: {}, worktrees: [], snapshots: {}, totalWorktrees: 0 })),
|
|
65
|
+
getSelectedOriginalIndex: vi.fn(() => 0),
|
|
66
|
+
getSelectedBranch: vi.fn(() => 'feat-test'),
|
|
67
|
+
getScrollOffset: vi.fn(() => 0),
|
|
68
|
+
getCachedPanelLines: vi.fn(() => []),
|
|
69
|
+
adjustScrollForSelection: vi.fn(),
|
|
70
|
+
navigateUp: vi.fn(() => false),
|
|
71
|
+
navigateDown: vi.fn(() => false),
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
77
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
import { InteractivePanel } from '../../../src/utils/interactive-panel.js';
|
|
81
|
+
import type { StatusResult } from '../../../src/types/index.js';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 构造最小可用的 StatusResult mock
|
|
85
|
+
*/
|
|
86
|
+
function makeStatusResult(): StatusResult {
|
|
87
|
+
return {
|
|
88
|
+
main: {
|
|
89
|
+
branch: 'main',
|
|
90
|
+
isClean: true,
|
|
91
|
+
projectName: 'test-project',
|
|
92
|
+
configuredMainBranch: 'main',
|
|
93
|
+
configuredBranchExists: true,
|
|
94
|
+
insertions: 0,
|
|
95
|
+
deletions: 0,
|
|
96
|
+
},
|
|
97
|
+
worktrees: [],
|
|
98
|
+
snapshots: { total: 0, orphaned: 0 },
|
|
99
|
+
totalWorktrees: 0,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe('InteractivePanel.executeOperation()', () => {
|
|
104
|
+
let panel: InteractivePanel;
|
|
105
|
+
let collectStatusMock: ReturnType<typeof vi.fn>;
|
|
106
|
+
let renderSpy: ReturnType<typeof vi.spyOn>;
|
|
107
|
+
let refreshDataSpy: ReturnType<typeof vi.spyOn>;
|
|
108
|
+
|
|
109
|
+
beforeEach(() => {
|
|
110
|
+
// 模拟 TTY 环境
|
|
111
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
|
|
112
|
+
Object.defineProperty(process.stdout, 'columns', { value: 80, configurable: true });
|
|
113
|
+
Object.defineProperty(process.stdout, 'rows', { value: 24, configurable: true });
|
|
114
|
+
vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
115
|
+
|
|
116
|
+
collectStatusMock = vi.fn().mockResolvedValue(makeStatusResult());
|
|
117
|
+
panel = new InteractivePanel(collectStatusMock);
|
|
118
|
+
|
|
119
|
+
// 监视 render 和 refreshData,记录调用顺序
|
|
120
|
+
renderSpy = vi.spyOn(panel as any, 'render');
|
|
121
|
+
refreshDataSpy = vi.spyOn(panel as any, 'refreshData').mockResolvedValue(undefined);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('应在 refreshData 之前调用一次 render(立即渲染旧数据消除白屏)', async () => {
|
|
125
|
+
const callOrder: string[] = [];
|
|
126
|
+
renderSpy.mockImplementation(() => { callOrder.push('render'); });
|
|
127
|
+
refreshDataSpy.mockImplementation(async () => { callOrder.push('refreshData'); });
|
|
128
|
+
|
|
129
|
+
// 直接调用私有方法 executeOperation
|
|
130
|
+
const action = vi.fn();
|
|
131
|
+
// 绕过 waitForEnter,直接 resolve
|
|
132
|
+
vi.spyOn(panel as any, 'waitForEnter').mockResolvedValue(undefined);
|
|
133
|
+
vi.spyOn(panel as any, 'initTerminal').mockImplementation(() => {});
|
|
134
|
+
vi.spyOn(panel as any, 'restoreTerminal').mockImplementation(() => {});
|
|
135
|
+
vi.spyOn(panel as any, 'removeTerminalListeners').mockImplementation(() => {});
|
|
136
|
+
vi.spyOn(panel as any, 'startAutoRefresh').mockImplementation(() => {});
|
|
137
|
+
vi.spyOn(panel as any, 'clearTimers').mockImplementation(() => {});
|
|
138
|
+
|
|
139
|
+
// stateManager.getStatusResult() 需要返回非 null 才能让 render 不被 guard 拦截
|
|
140
|
+
// executeOperation 顶部也检查 statusResult 非 null
|
|
141
|
+
await (panel as any).executeOperation(action);
|
|
142
|
+
|
|
143
|
+
// render 必须在 refreshData 之前被调用至少一次
|
|
144
|
+
const firstRenderIdx = callOrder.indexOf('render');
|
|
145
|
+
const refreshDataIdx = callOrder.indexOf('refreshData');
|
|
146
|
+
expect(firstRenderIdx).toBeGreaterThanOrEqual(0);
|
|
147
|
+
expect(refreshDataIdx).toBeGreaterThanOrEqual(0);
|
|
148
|
+
expect(firstRenderIdx).toBeLessThan(refreshDataIdx);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('应在 refreshData 之后再调用一次 render(刷新为最新数据)', async () => {
|
|
152
|
+
const callOrder: string[] = [];
|
|
153
|
+
renderSpy.mockImplementation(() => { callOrder.push('render'); });
|
|
154
|
+
refreshDataSpy.mockImplementation(async () => { callOrder.push('refreshData'); });
|
|
155
|
+
|
|
156
|
+
vi.spyOn(panel as any, 'waitForEnter').mockResolvedValue(undefined);
|
|
157
|
+
vi.spyOn(panel as any, 'initTerminal').mockImplementation(() => {});
|
|
158
|
+
vi.spyOn(panel as any, 'restoreTerminal').mockImplementation(() => {});
|
|
159
|
+
vi.spyOn(panel as any, 'removeTerminalListeners').mockImplementation(() => {});
|
|
160
|
+
vi.spyOn(panel as any, 'startAutoRefresh').mockImplementation(() => {});
|
|
161
|
+
vi.spyOn(panel as any, 'clearTimers').mockImplementation(() => {});
|
|
162
|
+
|
|
163
|
+
await (panel as any).executeOperation(vi.fn());
|
|
164
|
+
|
|
165
|
+
// refreshData 之后必须还有一次 render
|
|
166
|
+
const refreshDataIdx = callOrder.lastIndexOf('refreshData');
|
|
167
|
+
const lastRenderIdx = callOrder.lastIndexOf('render');
|
|
168
|
+
expect(lastRenderIdx).toBeGreaterThan(refreshDataIdx);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('isOperating 应在 render 之前被设为 false', async () => {
|
|
172
|
+
const isOperatingAtRender: boolean[] = [];
|
|
173
|
+
|
|
174
|
+
renderSpy.mockImplementation(() => {
|
|
175
|
+
isOperatingAtRender.push((panel as any).isOperating);
|
|
176
|
+
});
|
|
177
|
+
refreshDataSpy.mockResolvedValue(undefined);
|
|
178
|
+
|
|
179
|
+
vi.spyOn(panel as any, 'waitForEnter').mockResolvedValue(undefined);
|
|
180
|
+
vi.spyOn(panel as any, 'initTerminal').mockImplementation(() => {});
|
|
181
|
+
vi.spyOn(panel as any, 'restoreTerminal').mockImplementation(() => {});
|
|
182
|
+
vi.spyOn(panel as any, 'removeTerminalListeners').mockImplementation(() => {});
|
|
183
|
+
vi.spyOn(panel as any, 'startAutoRefresh').mockImplementation(() => {});
|
|
184
|
+
vi.spyOn(panel as any, 'clearTimers').mockImplementation(() => {});
|
|
185
|
+
|
|
186
|
+
await (panel as any).executeOperation(vi.fn());
|
|
187
|
+
|
|
188
|
+
// 第一次 render 调用时 isOperating 必须为 false
|
|
189
|
+
expect(isOperatingAtRender[0]).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -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 () => {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { parseTaskFile, loadTaskFile, parseTasksFromOptions } from '../../../src/utils/task-file.js';
|
|
3
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
3
|
|
|
5
4
|
vi.mock('node:fs', () => ({
|
|
@@ -7,6 +6,25 @@ vi.mock('node:fs', () => ({
|
|
|
7
6
|
readFileSync: vi.fn(),
|
|
8
7
|
}));
|
|
9
8
|
|
|
9
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
10
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// mock i18n 模块,避免循环依赖导致 currentLanguage 未初始化
|
|
14
|
+
// 返回 'zh-CN' 以匹配测试中的中文断言
|
|
15
|
+
vi.mock('../../../src/utils/i18n.js', () => ({
|
|
16
|
+
getCurrentLanguage: vi.fn().mockReturnValue('zh-CN'),
|
|
17
|
+
resetLanguageCache: vi.fn(),
|
|
18
|
+
setCurrentLanguage: vi.fn(),
|
|
19
|
+
createMessages: vi.fn((i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
20
|
+
const result: any = {};
|
|
21
|
+
for (const key of Object.keys(i18nMap)) {
|
|
22
|
+
result[key] = i18nMap[key]['zh-CN'];
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
10
28
|
vi.mock('../../../src/errors/index.js', () => ({
|
|
11
29
|
ClawtError: class ClawtError extends Error {
|
|
12
30
|
exitCode: number;
|
|
@@ -17,15 +35,60 @@ vi.mock('../../../src/errors/index.js', () => ({
|
|
|
17
35
|
},
|
|
18
36
|
}));
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
38
|
+
// mock constants/index.js,使用固定的中文 mock 来匹配测试断言中的中文错误消息
|
|
39
|
+
// 同时保留必要的路径常量导出(避免 logger 等模块加载时报错)
|
|
40
|
+
vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
41
|
+
// 仅从 paths.js 获取路径常量,不触发 messages 模块加载
|
|
42
|
+
const { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR, CLAUDE_PROJECTS_DIR, UPDATE_CHECK_PATH, PROJECTS_CONFIG_DIR } = await import('../../../src/constants/paths.js');
|
|
43
|
+
const { INVALID_BRANCH_CHARS, VALIDATE_BRANCH_PREFIX } = await import('../../../src/constants/branch.js');
|
|
44
|
+
const { EXIT_CODES } = await import('../../../src/constants/exitCodes.js');
|
|
45
|
+
const { VALID_TERMINAL_APPS, ITERM2_APP_PATH, ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS } = await import('../../../src/constants/terminal.js');
|
|
46
|
+
return {
|
|
47
|
+
CLAWT_HOME,
|
|
48
|
+
CONFIG_PATH,
|
|
49
|
+
LOGS_DIR,
|
|
50
|
+
WORKTREES_DIR,
|
|
51
|
+
VALIDATE_SNAPSHOTS_DIR,
|
|
52
|
+
CLAUDE_PROJECTS_DIR,
|
|
53
|
+
UPDATE_CHECK_PATH,
|
|
54
|
+
PROJECTS_CONFIG_DIR,
|
|
55
|
+
INVALID_BRANCH_CHARS,
|
|
56
|
+
VALIDATE_BRANCH_PREFIX,
|
|
57
|
+
EXIT_CODES,
|
|
58
|
+
VALID_TERMINAL_APPS,
|
|
59
|
+
ITERM2_APP_PATH,
|
|
60
|
+
ENABLE_BRACKETED_PASTE,
|
|
61
|
+
DISABLE_BRACKETED_PASTE,
|
|
62
|
+
PASTE_THRESHOLD_MS,
|
|
63
|
+
// 使用固定的中文 mock MESSAGES
|
|
64
|
+
MESSAGES: {
|
|
65
|
+
TASK_FILE_NOT_FOUND: (path: string) => `任务文件不存在: ${path}`,
|
|
66
|
+
TASK_FILE_EMPTY: '任务文件中没有解析到有效任务',
|
|
67
|
+
TASK_FILE_MISSING_BRANCH: (blockIndex: number) => `第 ${blockIndex} 个任务块缺少分支名`,
|
|
68
|
+
TASK_FILE_MISSING_TASK: (branch: string) => `分支 ${branch} 缺少任务描述`,
|
|
69
|
+
TASK_FILE_MISSING_TASK_BY_INDEX: (blockIndex: number) => `第 ${blockIndex} 个任务块缺少任务描述`,
|
|
70
|
+
},
|
|
71
|
+
// 其他测试不需要的导出设为空对象或空字符串
|
|
72
|
+
DEFAULT_CONFIG: {},
|
|
73
|
+
CONFIG_DESCRIPTIONS: {},
|
|
74
|
+
CONFIG_DEFINITIONS: {},
|
|
75
|
+
CLAUDE_CODE_ENTRYPOINT_VALUE: 'cli',
|
|
76
|
+
getI18nConfigDescriptions: vi.fn(),
|
|
77
|
+
AUTO_SAVE_COMMIT_MESSAGE_PREFIX: '[clawt-auto-save]',
|
|
78
|
+
DEBUG_LOG_PREFIX: '[clawt]',
|
|
79
|
+
DEBUG_TIMESTAMP_FORMAT: 'YYYY-MM-DD HH:mm:ss',
|
|
80
|
+
UPDATE_CHECK_INTERVAL_MS: 86400000,
|
|
81
|
+
NPM_REGISTRY_URL: 'https://registry.npmjs.org',
|
|
82
|
+
NPM_REGISTRY_TIMEOUT_MS: 5000,
|
|
83
|
+
PACKAGE_NAME: 'clawt',
|
|
84
|
+
CONFLICT_RESOLVE_PROMPT: '',
|
|
85
|
+
CONFIG_ALIAS_DISABLED_HINT: '',
|
|
86
|
+
UPDATE_MESSAGES: {},
|
|
87
|
+
UPDATE_COMMANDS: {},
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
import { parseTaskFile, loadTaskFile, parseTasksFromOptions } from '../../../src/utils/task-file.js';
|
|
29
92
|
|
|
30
93
|
const mockedExistsSync = vi.mocked(existsSync);
|
|
31
94
|
const mockedReadFileSync = vi.mocked(readFileSync);
|
|
@@ -20,6 +20,21 @@ vi.mock('../../../src/utils/config.js', () => ({
|
|
|
20
20
|
getConfigValue: vi.fn(),
|
|
21
21
|
}));
|
|
22
22
|
|
|
23
|
+
// mock i18n 模块,避免循环依赖导致 currentLanguage 未初始化
|
|
24
|
+
// 返回 'en' 以匹配测试中的英文断言
|
|
25
|
+
vi.mock('../../../src/utils/i18n.js', () => ({
|
|
26
|
+
getCurrentLanguage: vi.fn().mockReturnValue('en'),
|
|
27
|
+
resetLanguageCache: vi.fn(),
|
|
28
|
+
setCurrentLanguage: vi.fn(),
|
|
29
|
+
createMessages: vi.fn((i18nMap: Record<string, { en: any; 'zh-CN': any }>) => {
|
|
30
|
+
const result: any = {};
|
|
31
|
+
for (const key of Object.keys(i18nMap)) {
|
|
32
|
+
result[key] = i18nMap[key]['en'];
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
|
|
23
38
|
import { execFileSync } from 'node:child_process';
|
|
24
39
|
import { existsSync } from 'node:fs';
|
|
25
40
|
import {
|
|
@@ -180,7 +195,7 @@ describe('cmux surface 创建', () => {
|
|
|
180
195
|
mockedGetConfigValue.mockReturnValue('cmux');
|
|
181
196
|
|
|
182
197
|
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).toThrow(
|
|
183
|
-
|
|
198
|
+
/Not currently in a cmux environment/
|
|
184
199
|
);
|
|
185
200
|
});
|
|
186
201
|
|
|
@@ -192,7 +207,7 @@ describe('cmux surface 创建', () => {
|
|
|
192
207
|
mockedExecFileSync.mockReturnValueOnce('invalid output format');
|
|
193
208
|
|
|
194
209
|
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).toThrow(
|
|
195
|
-
|
|
210
|
+
/Failed to parse cmux new-split output/
|
|
196
211
|
);
|
|
197
212
|
});
|
|
198
213
|
|
|
@@ -207,7 +222,7 @@ describe('cmux surface 创建', () => {
|
|
|
207
222
|
});
|
|
208
223
|
|
|
209
224
|
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).toThrow(
|
|
210
|
-
|
|
225
|
+
/Failed to create surface in cmux/
|
|
211
226
|
);
|
|
212
227
|
});
|
|
213
228
|
|
|
@@ -215,7 +230,7 @@ describe('cmux surface 创建', () => {
|
|
|
215
230
|
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
216
231
|
|
|
217
232
|
expect(() => openCommandInNewTerminalTab('claude', 'test-title')).toThrow(
|
|
218
|
-
|
|
233
|
+
/Batch resume is only supported on macOS/
|
|
219
234
|
);
|
|
220
235
|
});
|
|
221
236
|
});
|
|
@@ -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() },
|
|
@@ -16,13 +36,18 @@ vi.mock('../../../src/errors/index.js', () => ({
|
|
|
16
36
|
},
|
|
17
37
|
}));
|
|
18
38
|
|
|
19
|
-
// mock constants
|
|
39
|
+
// mock constants(补充 handleDirtyWorkingDir 和 ensureOnMainWorkBranch 使用的消息键)
|
|
20
40
|
vi.mock('../../../src/constants/index.js', () => ({
|
|
21
41
|
VALIDATE_BRANCH_PREFIX: 'clawt-validate-',
|
|
22
42
|
MESSAGES: {
|
|
23
43
|
GUARD_BRANCH_MISMATCH: (mainBranch: string, currentBranch: string) =>
|
|
24
44
|
`当前分支 ${currentBranch} 与配置的主工作分支 ${mainBranch} 不一致`,
|
|
25
45
|
DESTRUCTIVE_OP_CANCELLED: '已取消操作',
|
|
46
|
+
WORKSPACE_STILL_DIRTY: '工作区仍然不干净,请手动处理',
|
|
47
|
+
USER_CHOSE_EXIT: '用户选择退出,请手动处理工作区更改后重试',
|
|
48
|
+
UNCOMMITTED_CHANGES_ON_BRANCH: '当前分支有未提交的更改,请选择处理方式:\n',
|
|
49
|
+
SELECT_ACTION: '选择处理方式',
|
|
50
|
+
CONFIRM_CONTINUE_VALIDATE: '是否继续执行?',
|
|
26
51
|
},
|
|
27
52
|
}));
|
|
28
53
|
|
|
@@ -45,13 +45,13 @@ describe('validateMainWorktree', () => {
|
|
|
45
45
|
mockedIsInsideGitRepo.mockReturnValue(true);
|
|
46
46
|
mockedGetGitCommonDir.mockReturnValue('/path/to/.git');
|
|
47
47
|
expect(() => validateMainWorktree()).toThrow(ClawtError);
|
|
48
|
-
expect(() => validateMainWorktree()).toThrow('
|
|
48
|
+
expect(() => validateMainWorktree()).toThrow('Please run clawt in the root directory of the main worktree');
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it('不在 git 仓库中时抛出 NOT_GIT_REPO 错误', () => {
|
|
52
52
|
mockedIsInsideGitRepo.mockReturnValue(false);
|
|
53
53
|
expect(() => validateMainWorktree()).toThrow(ClawtError);
|
|
54
|
-
expect(() => validateMainWorktree()).toThrow('
|
|
54
|
+
expect(() => validateMainWorktree()).toThrow('Current directory is not a git repository');
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -25,6 +25,8 @@ vi.mock('../../../src/utils/git.js', () => ({
|
|
|
25
25
|
// mock node:fs 的 statSync,分组测试中按需控制返回值
|
|
26
26
|
vi.mock('node:fs', () => ({
|
|
27
27
|
statSync: vi.fn().mockReturnValue({ birthtime: new Date('2026-02-27T10:00:00') }),
|
|
28
|
+
existsSync: vi.fn().mockReturnValue(false),
|
|
29
|
+
mkdirSync: vi.fn(),
|
|
28
30
|
}));
|
|
29
31
|
|
|
30
32
|
import { statSync } from 'node:fs';
|