clawt 2.10.0 → 2.11.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/.claude/agent-memory/docs-sync-updater/MEMORY.md +14 -7
- package/.claude/agents/docs-sync-updater.md +11 -0
- package/README.md +80 -284
- package/dist/index.js +839 -307
- package/dist/postinstall.js +272 -0
- package/docs/spec.md +84 -22
- package/package.json +1 -1
- package/src/commands/remove.ts +21 -28
- package/src/commands/run.ts +68 -206
- package/src/constants/config.ts +4 -0
- package/src/constants/index.ts +11 -1
- package/src/constants/messages/common.ts +41 -0
- package/src/constants/messages/config.ts +5 -0
- package/src/constants/messages/create.ts +5 -0
- package/src/constants/messages/index.ts +29 -0
- package/src/constants/messages/merge.ts +42 -0
- package/src/constants/messages/remove.ts +15 -0
- package/src/constants/messages/reset.ts +7 -0
- package/src/constants/messages/resume.ts +12 -0
- package/src/constants/messages/run.ts +46 -0
- package/src/constants/messages/status.ts +25 -0
- package/src/constants/messages/sync.ts +24 -0
- package/src/constants/messages/validate.ts +25 -0
- package/src/constants/progress.ts +39 -0
- package/src/types/command.ts +4 -0
- package/src/types/config.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/taskFile.ts +13 -0
- package/src/utils/formatter.ts +16 -0
- package/src/utils/index.ts +8 -4
- package/src/utils/progress-render.ts +90 -0
- package/src/utils/progress.ts +213 -0
- package/src/utils/task-executor.ts +365 -0
- package/src/utils/task-file.ts +87 -0
- package/src/utils/worktree-matcher.ts +92 -0
- package/src/utils/worktree.ts +27 -0
- package/tests/unit/commands/config.test.ts +110 -0
- package/tests/unit/commands/create.test.ts +115 -0
- package/tests/unit/commands/list.test.ts +118 -0
- package/tests/unit/commands/merge.test.ts +323 -0
- package/tests/unit/commands/remove.test.ts +240 -0
- package/tests/unit/commands/reset.test.ts +124 -0
- package/tests/unit/commands/resume.test.ts +91 -0
- package/tests/unit/commands/run.test.ts +456 -0
- package/tests/unit/commands/status.test.ts +214 -0
- package/tests/unit/commands/sync.test.ts +208 -0
- package/tests/unit/commands/validate.test.ts +382 -0
- package/tests/unit/constants/config.test.ts +1 -0
- package/tests/unit/constants/messages.test.ts +1 -1
- package/tests/unit/utils/config.test.ts +21 -1
- package/tests/unit/utils/formatter.test.ts +70 -1
- package/tests/unit/utils/git.test.ts +44 -0
- package/tests/unit/utils/progress.test.ts +255 -0
- package/tests/unit/utils/task-file.test.ts +236 -0
- package/tests/unit/utils/validate-snapshot.test.ts +25 -0
- package/tests/unit/utils/worktree-matcher.test.ts +81 -5
- package/tests/unit/utils/worktree.test.ts +26 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
|
|
4
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../../../src/errors/index.js', () => ({
|
|
9
|
+
ClawtError: class ClawtError extends Error {
|
|
10
|
+
exitCode: number;
|
|
11
|
+
constructor(message: string, exitCode = 1) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.exitCode = exitCode;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('../../../src/constants/index.js', () => ({
|
|
19
|
+
MESSAGES: {
|
|
20
|
+
NO_WORKTREES: '(无 worktree)',
|
|
21
|
+
WORKTREE_REMOVED: (path: string) => `✓ 已移除 worktree: ${path}`,
|
|
22
|
+
REMOVE_PARTIAL_FAILURE: (failures: Array<{ path: string; error: string }>) => `${failures.length} 个移除失败`,
|
|
23
|
+
REMOVE_NO_WORKTREES: '当前项目没有可用的 worktree,无需移除',
|
|
24
|
+
REMOVE_SELECT_BRANCH: '请选择要移除的分支(空格选择,回车确认)',
|
|
25
|
+
REMOVE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支`,
|
|
26
|
+
REMOVE_NO_MATCH: (name: string, branches: string[]) => `未找到与 "${name}" 匹配的分支,可用:${branches.join(', ')}`,
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('../../../src/utils/index.js', () => ({
|
|
31
|
+
validateMainWorktree: vi.fn(),
|
|
32
|
+
getProjectName: vi.fn(),
|
|
33
|
+
getProjectWorktreeDir: vi.fn(),
|
|
34
|
+
getProjectWorktrees: vi.fn(),
|
|
35
|
+
removeWorktreeByPath: vi.fn(),
|
|
36
|
+
deleteBranch: vi.fn(),
|
|
37
|
+
getConfigValue: vi.fn(),
|
|
38
|
+
gitWorktreePrune: vi.fn(),
|
|
39
|
+
removeEmptyDir: vi.fn(),
|
|
40
|
+
printInfo: vi.fn(),
|
|
41
|
+
printSuccess: vi.fn(),
|
|
42
|
+
printError: vi.fn(),
|
|
43
|
+
confirmAction: vi.fn(),
|
|
44
|
+
removeSnapshot: vi.fn(),
|
|
45
|
+
removeProjectSnapshots: vi.fn(),
|
|
46
|
+
resolveTargetWorktrees: vi.fn(),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
import { registerRemoveCommand } from '../../../src/commands/remove.js';
|
|
50
|
+
import {
|
|
51
|
+
validateMainWorktree,
|
|
52
|
+
getProjectName,
|
|
53
|
+
getProjectWorktrees,
|
|
54
|
+
removeWorktreeByPath,
|
|
55
|
+
deleteBranch,
|
|
56
|
+
getConfigValue,
|
|
57
|
+
confirmAction,
|
|
58
|
+
removeSnapshot,
|
|
59
|
+
removeProjectSnapshots,
|
|
60
|
+
printSuccess,
|
|
61
|
+
printError,
|
|
62
|
+
resolveTargetWorktrees,
|
|
63
|
+
} from '../../../src/utils/index.js';
|
|
64
|
+
|
|
65
|
+
const mockedGetProjectName = vi.mocked(getProjectName);
|
|
66
|
+
const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
|
|
67
|
+
const mockedRemoveWorktreeByPath = vi.mocked(removeWorktreeByPath);
|
|
68
|
+
const mockedDeleteBranch = vi.mocked(deleteBranch);
|
|
69
|
+
const mockedGetConfigValue = vi.mocked(getConfigValue);
|
|
70
|
+
const mockedConfirmAction = vi.mocked(confirmAction);
|
|
71
|
+
const mockedRemoveSnapshot = vi.mocked(removeSnapshot);
|
|
72
|
+
const mockedRemoveProjectSnapshots = vi.mocked(removeProjectSnapshots);
|
|
73
|
+
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
74
|
+
const mockedPrintError = vi.mocked(printError);
|
|
75
|
+
const mockedResolveTargetWorktrees = vi.mocked(resolveTargetWorktrees);
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.mocked(validateMainWorktree).mockReset();
|
|
79
|
+
mockedGetProjectName.mockReturnValue('test-project');
|
|
80
|
+
mockedGetProjectWorktrees.mockReset();
|
|
81
|
+
mockedRemoveWorktreeByPath.mockReset();
|
|
82
|
+
mockedDeleteBranch.mockReset();
|
|
83
|
+
mockedGetConfigValue.mockReset();
|
|
84
|
+
mockedConfirmAction.mockReset();
|
|
85
|
+
mockedRemoveSnapshot.mockReset();
|
|
86
|
+
mockedRemoveProjectSnapshots.mockReset();
|
|
87
|
+
mockedPrintSuccess.mockReset();
|
|
88
|
+
mockedPrintError.mockReset();
|
|
89
|
+
mockedResolveTargetWorktrees.mockReset();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('registerRemoveCommand', () => {
|
|
93
|
+
it('注册 remove 命令', () => {
|
|
94
|
+
const program = new Command();
|
|
95
|
+
registerRemoveCommand(program);
|
|
96
|
+
const cmd = program.commands.find((c) => c.name() === 'remove');
|
|
97
|
+
expect(cmd).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('handleRemove', () => {
|
|
102
|
+
it('--all 移除所有 worktree(autoDeleteBranch=true)', async () => {
|
|
103
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
104
|
+
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
105
|
+
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
106
|
+
]);
|
|
107
|
+
mockedGetConfigValue.mockReturnValue(true);
|
|
108
|
+
|
|
109
|
+
const program = new Command();
|
|
110
|
+
program.exitOverride();
|
|
111
|
+
registerRemoveCommand(program);
|
|
112
|
+
await program.parseAsync(['remove', '--all'], { from: 'user' });
|
|
113
|
+
|
|
114
|
+
expect(mockedRemoveWorktreeByPath).toHaveBeenCalledTimes(2);
|
|
115
|
+
expect(mockedDeleteBranch).toHaveBeenCalledTimes(2);
|
|
116
|
+
expect(mockedRemoveProjectSnapshots).toHaveBeenCalledWith('test-project');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('-b 精确匹配时通过 resolveTargetWorktrees 解析并移除', async () => {
|
|
120
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
121
|
+
{ path: '/path/feature', branch: 'feature' },
|
|
122
|
+
{ path: '/path/other', branch: 'other' },
|
|
123
|
+
]);
|
|
124
|
+
mockedResolveTargetWorktrees.mockResolvedValue([
|
|
125
|
+
{ path: '/path/feature', branch: 'feature' },
|
|
126
|
+
]);
|
|
127
|
+
mockedGetConfigValue.mockReturnValue(true);
|
|
128
|
+
|
|
129
|
+
const program = new Command();
|
|
130
|
+
program.exitOverride();
|
|
131
|
+
registerRemoveCommand(program);
|
|
132
|
+
await program.parseAsync(['remove', '-b', 'feature'], { from: 'user' });
|
|
133
|
+
|
|
134
|
+
expect(mockedResolveTargetWorktrees).toHaveBeenCalled();
|
|
135
|
+
expect(mockedRemoveWorktreeByPath).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(mockedRemoveWorktreeByPath).toHaveBeenCalledWith('/path/feature');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('-b 模糊匹配多个时通过 resolveTargetWorktrees 解析并批量移除', async () => {
|
|
140
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
141
|
+
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
142
|
+
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
143
|
+
{ path: '/path/other', branch: 'other' },
|
|
144
|
+
]);
|
|
145
|
+
// 模拟用户多选了两个
|
|
146
|
+
mockedResolveTargetWorktrees.mockResolvedValue([
|
|
147
|
+
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
148
|
+
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
149
|
+
]);
|
|
150
|
+
mockedGetConfigValue.mockReturnValue(true);
|
|
151
|
+
|
|
152
|
+
const program = new Command();
|
|
153
|
+
program.exitOverride();
|
|
154
|
+
registerRemoveCommand(program);
|
|
155
|
+
await program.parseAsync(['remove', '-b', 'feature'], { from: 'user' });
|
|
156
|
+
|
|
157
|
+
expect(mockedRemoveWorktreeByPath).toHaveBeenCalledTimes(2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('未指定 --all 或 -b 时通过 resolveTargetWorktrees 展示多选列表', async () => {
|
|
161
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
162
|
+
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
163
|
+
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
164
|
+
]);
|
|
165
|
+
mockedResolveTargetWorktrees.mockResolvedValue([
|
|
166
|
+
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
167
|
+
]);
|
|
168
|
+
mockedGetConfigValue.mockReturnValue(true);
|
|
169
|
+
|
|
170
|
+
const program = new Command();
|
|
171
|
+
program.exitOverride();
|
|
172
|
+
registerRemoveCommand(program);
|
|
173
|
+
await program.parseAsync(['remove'], { from: 'user' });
|
|
174
|
+
|
|
175
|
+
// 未传 branch 参数,resolveTargetWorktrees 的第三个参数应为 undefined
|
|
176
|
+
expect(mockedResolveTargetWorktrees).toHaveBeenCalledWith(
|
|
177
|
+
expect.any(Array),
|
|
178
|
+
expect.any(Object),
|
|
179
|
+
undefined,
|
|
180
|
+
);
|
|
181
|
+
expect(mockedRemoveWorktreeByPath).toHaveBeenCalledTimes(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('autoDeleteBranch=false 时询问用户是否删除分支', async () => {
|
|
185
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
186
|
+
{ path: '/path/feature', branch: 'feature' },
|
|
187
|
+
]);
|
|
188
|
+
mockedResolveTargetWorktrees.mockResolvedValue([
|
|
189
|
+
{ path: '/path/feature', branch: 'feature' },
|
|
190
|
+
]);
|
|
191
|
+
mockedGetConfigValue.mockReturnValue(false);
|
|
192
|
+
mockedConfirmAction.mockResolvedValue(false);
|
|
193
|
+
|
|
194
|
+
const program = new Command();
|
|
195
|
+
program.exitOverride();
|
|
196
|
+
registerRemoveCommand(program);
|
|
197
|
+
await program.parseAsync(['remove', '-b', 'feature'], { from: 'user' });
|
|
198
|
+
|
|
199
|
+
expect(mockedConfirmAction).toHaveBeenCalled();
|
|
200
|
+
// 用户拒绝删除分支
|
|
201
|
+
expect(mockedDeleteBranch).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('-b 指定不存在的分支时 resolveTargetWorktrees 抛出错误', async () => {
|
|
205
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
206
|
+
{ path: '/path/other', branch: 'other' },
|
|
207
|
+
]);
|
|
208
|
+
mockedResolveTargetWorktrees.mockRejectedValue(new Error('未找到与 "nonexistent" 匹配的分支'));
|
|
209
|
+
|
|
210
|
+
const program = new Command();
|
|
211
|
+
program.exitOverride();
|
|
212
|
+
registerRemoveCommand(program);
|
|
213
|
+
|
|
214
|
+
await expect(
|
|
215
|
+
program.parseAsync(['remove', '-b', 'nonexistent'], { from: 'user' }),
|
|
216
|
+
).rejects.toThrow();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('移除过程中部分失败时汇报并抛出错误', async () => {
|
|
220
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
221
|
+
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
222
|
+
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
223
|
+
]);
|
|
224
|
+
mockedGetConfigValue.mockReturnValue(true);
|
|
225
|
+
// 第一个成功,第二个失败
|
|
226
|
+
mockedRemoveWorktreeByPath
|
|
227
|
+
.mockImplementationOnce(() => {})
|
|
228
|
+
.mockImplementationOnce(() => { throw new Error('remove failed'); });
|
|
229
|
+
|
|
230
|
+
const program = new Command();
|
|
231
|
+
program.exitOverride();
|
|
232
|
+
registerRemoveCommand(program);
|
|
233
|
+
|
|
234
|
+
await expect(
|
|
235
|
+
program.parseAsync(['remove', '--all'], { from: 'user' }),
|
|
236
|
+
).rejects.toThrow();
|
|
237
|
+
|
|
238
|
+
expect(mockedPrintError).toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
|
|
4
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../../../src/constants/index.js', () => ({
|
|
9
|
+
MESSAGES: {
|
|
10
|
+
RESET_SUCCESS: '✓ 已重置主 worktree',
|
|
11
|
+
RESET_ALREADY_CLEAN: '主 worktree 已经是干净状态',
|
|
12
|
+
DESTRUCTIVE_OP_CANCELLED: '已取消操作',
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('../../../src/utils/index.js', () => ({
|
|
17
|
+
validateMainWorktree: vi.fn(),
|
|
18
|
+
getGitTopLevel: vi.fn(),
|
|
19
|
+
getConfigValue: vi.fn(),
|
|
20
|
+
isWorkingDirClean: vi.fn(),
|
|
21
|
+
gitResetHard: vi.fn(),
|
|
22
|
+
gitCleanForce: vi.fn(),
|
|
23
|
+
confirmDestructiveAction: vi.fn(),
|
|
24
|
+
printSuccess: vi.fn(),
|
|
25
|
+
printInfo: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { registerResetCommand } from '../../../src/commands/reset.js';
|
|
29
|
+
import {
|
|
30
|
+
getGitTopLevel,
|
|
31
|
+
getConfigValue,
|
|
32
|
+
isWorkingDirClean,
|
|
33
|
+
gitResetHard,
|
|
34
|
+
gitCleanForce,
|
|
35
|
+
confirmDestructiveAction,
|
|
36
|
+
printSuccess,
|
|
37
|
+
printInfo,
|
|
38
|
+
} from '../../../src/utils/index.js';
|
|
39
|
+
|
|
40
|
+
const mockedGetGitTopLevel = vi.mocked(getGitTopLevel);
|
|
41
|
+
const mockedGetConfigValue = vi.mocked(getConfigValue);
|
|
42
|
+
const mockedIsWorkingDirClean = vi.mocked(isWorkingDirClean);
|
|
43
|
+
const mockedGitResetHard = vi.mocked(gitResetHard);
|
|
44
|
+
const mockedGitCleanForce = vi.mocked(gitCleanForce);
|
|
45
|
+
const mockedConfirmDestructiveAction = vi.mocked(confirmDestructiveAction);
|
|
46
|
+
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
47
|
+
const mockedPrintInfo = vi.mocked(printInfo);
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
mockedGetGitTopLevel.mockReturnValue('/repo');
|
|
51
|
+
mockedGetConfigValue.mockReset();
|
|
52
|
+
mockedIsWorkingDirClean.mockReset();
|
|
53
|
+
mockedGitResetHard.mockReset();
|
|
54
|
+
mockedGitCleanForce.mockReset();
|
|
55
|
+
mockedConfirmDestructiveAction.mockReset();
|
|
56
|
+
mockedPrintSuccess.mockReset();
|
|
57
|
+
mockedPrintInfo.mockReset();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('registerResetCommand', () => {
|
|
61
|
+
it('注册 reset 命令', () => {
|
|
62
|
+
const program = new Command();
|
|
63
|
+
registerResetCommand(program);
|
|
64
|
+
const cmd = program.commands.find((c) => c.name() === 'reset');
|
|
65
|
+
expect(cmd).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('handleReset', () => {
|
|
70
|
+
it('工作区已干净时提示', async () => {
|
|
71
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
72
|
+
|
|
73
|
+
const program = new Command();
|
|
74
|
+
program.exitOverride();
|
|
75
|
+
registerResetCommand(program);
|
|
76
|
+
await program.parseAsync(['reset'], { from: 'user' });
|
|
77
|
+
|
|
78
|
+
expect(mockedGitResetHard).not.toHaveBeenCalled();
|
|
79
|
+
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('工作区不干净且确认后执行重置', async () => {
|
|
83
|
+
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
84
|
+
mockedGetConfigValue.mockReturnValue(true); // confirmDestructiveOps = true
|
|
85
|
+
mockedConfirmDestructiveAction.mockResolvedValue(true);
|
|
86
|
+
|
|
87
|
+
const program = new Command();
|
|
88
|
+
program.exitOverride();
|
|
89
|
+
registerResetCommand(program);
|
|
90
|
+
await program.parseAsync(['reset'], { from: 'user' });
|
|
91
|
+
|
|
92
|
+
expect(mockedGitResetHard).toHaveBeenCalledWith('/repo');
|
|
93
|
+
expect(mockedGitCleanForce).toHaveBeenCalledWith('/repo');
|
|
94
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('用户拒绝确认时不执行重置', async () => {
|
|
98
|
+
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
99
|
+
mockedGetConfigValue.mockReturnValue(true);
|
|
100
|
+
mockedConfirmDestructiveAction.mockResolvedValue(false);
|
|
101
|
+
|
|
102
|
+
const program = new Command();
|
|
103
|
+
program.exitOverride();
|
|
104
|
+
registerResetCommand(program);
|
|
105
|
+
await program.parseAsync(['reset'], { from: 'user' });
|
|
106
|
+
|
|
107
|
+
expect(mockedGitResetHard).not.toHaveBeenCalled();
|
|
108
|
+
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('confirmDestructiveOps=false 时跳过确认直接重置', async () => {
|
|
112
|
+
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
113
|
+
mockedGetConfigValue.mockReturnValue(false);
|
|
114
|
+
|
|
115
|
+
const program = new Command();
|
|
116
|
+
program.exitOverride();
|
|
117
|
+
registerResetCommand(program);
|
|
118
|
+
await program.parseAsync(['reset'], { from: 'user' });
|
|
119
|
+
|
|
120
|
+
expect(mockedConfirmDestructiveAction).not.toHaveBeenCalled();
|
|
121
|
+
expect(mockedGitResetHard).toHaveBeenCalledWith('/repo');
|
|
122
|
+
expect(mockedGitCleanForce).toHaveBeenCalledWith('/repo');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
|
|
4
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../../../src/constants/index.js', () => ({
|
|
9
|
+
MESSAGES: {
|
|
10
|
+
RESUME_NO_WORKTREES: '没有可用的 worktree',
|
|
11
|
+
RESUME_SELECT_BRANCH: '选择要恢复的分支',
|
|
12
|
+
RESUME_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
|
|
13
|
+
RESUME_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('../../../src/utils/index.js', () => ({
|
|
18
|
+
validateMainWorktree: vi.fn(),
|
|
19
|
+
validateClaudeCodeInstalled: vi.fn(),
|
|
20
|
+
getProjectWorktrees: vi.fn(),
|
|
21
|
+
launchInteractiveClaude: vi.fn(),
|
|
22
|
+
resolveTargetWorktree: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
import { registerResumeCommand } from '../../../src/commands/resume.js';
|
|
26
|
+
import {
|
|
27
|
+
validateMainWorktree,
|
|
28
|
+
validateClaudeCodeInstalled,
|
|
29
|
+
getProjectWorktrees,
|
|
30
|
+
launchInteractiveClaude,
|
|
31
|
+
resolveTargetWorktree,
|
|
32
|
+
} from '../../../src/utils/index.js';
|
|
33
|
+
|
|
34
|
+
const mockedValidateMainWorktree = vi.mocked(validateMainWorktree);
|
|
35
|
+
const mockedValidateClaudeCodeInstalled = vi.mocked(validateClaudeCodeInstalled);
|
|
36
|
+
const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
|
|
37
|
+
const mockedLaunchInteractiveClaude = vi.mocked(launchInteractiveClaude);
|
|
38
|
+
const mockedResolveTargetWorktree = vi.mocked(resolveTargetWorktree);
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
mockedValidateMainWorktree.mockReset();
|
|
42
|
+
mockedValidateClaudeCodeInstalled.mockReset();
|
|
43
|
+
mockedGetProjectWorktrees.mockReset();
|
|
44
|
+
mockedLaunchInteractiveClaude.mockReset();
|
|
45
|
+
mockedResolveTargetWorktree.mockReset();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('registerResumeCommand', () => {
|
|
49
|
+
it('注册 resume 命令', () => {
|
|
50
|
+
const program = new Command();
|
|
51
|
+
registerResumeCommand(program);
|
|
52
|
+
const cmd = program.commands.find((c) => c.name() === 'resume');
|
|
53
|
+
expect(cmd).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('handleResume', () => {
|
|
58
|
+
it('成功恢复 Claude Code 会话', async () => {
|
|
59
|
+
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
60
|
+
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
61
|
+
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
62
|
+
|
|
63
|
+
const program = new Command();
|
|
64
|
+
program.exitOverride();
|
|
65
|
+
registerResumeCommand(program);
|
|
66
|
+
await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
|
|
67
|
+
|
|
68
|
+
expect(mockedValidateMainWorktree).toHaveBeenCalled();
|
|
69
|
+
expect(mockedValidateClaudeCodeInstalled).toHaveBeenCalled();
|
|
70
|
+
expect(mockedResolveTargetWorktree).toHaveBeenCalled();
|
|
71
|
+
expect(mockedLaunchInteractiveClaude).toHaveBeenCalledWith(worktree);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('不传 -b 时也能调用 resolveTargetWorktree', async () => {
|
|
75
|
+
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
76
|
+
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
77
|
+
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
78
|
+
|
|
79
|
+
const program = new Command();
|
|
80
|
+
program.exitOverride();
|
|
81
|
+
registerResumeCommand(program);
|
|
82
|
+
await program.parseAsync(['resume'], { from: 'user' });
|
|
83
|
+
|
|
84
|
+
// branchName 参数为 undefined
|
|
85
|
+
expect(mockedResolveTargetWorktree).toHaveBeenCalledWith(
|
|
86
|
+
expect.any(Array),
|
|
87
|
+
expect.any(Object),
|
|
88
|
+
undefined,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|