clawt 2.10.0 → 2.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/.claude/agent-memory/docs-sync-updater/MEMORY.md +9 -6
- package/.claude/agents/docs-sync-updater.md +11 -0
- package/README.md +63 -290
- package/dist/index.js +203 -104
- package/dist/postinstall.js +242 -0
- package/docs/spec.md +27 -14
- package/package.json +1 -1
- package/src/commands/remove.ts +21 -28
- package/src/constants/index.ts +1 -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 +16 -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/utils/index.ts +2 -2
- package/src/utils/worktree-matcher.ts +92 -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 +207 -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/messages.test.ts +1 -1
- package/tests/unit/utils/config.test.ts +21 -1
- package/tests/unit/utils/formatter.test.ts +44 -1
- package/tests/unit/utils/git.test.ts +44 -0
- package/tests/unit/utils/validate-snapshot.test.ts +25 -0
- package/tests/unit/utils/worktree-matcher.test.ts +81 -5
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
STATUS_TITLE: (name: string) => `项目 ${name} 状态`,
|
|
11
|
+
STATUS_MAIN_SECTION: '主 Worktree',
|
|
12
|
+
STATUS_WORKTREES_SECTION: 'Worktree 列表',
|
|
13
|
+
STATUS_SNAPSHOTS_SECTION: 'Validate 快照',
|
|
14
|
+
STATUS_NO_WORKTREES: '无 worktree',
|
|
15
|
+
STATUS_NO_SNAPSHOTS: '无快照',
|
|
16
|
+
STATUS_CHANGE_COMMITTED: '已提交',
|
|
17
|
+
STATUS_CHANGE_UNCOMMITTED: '未提交',
|
|
18
|
+
STATUS_CHANGE_CONFLICT: '冲突',
|
|
19
|
+
STATUS_CHANGE_CLEAN: '干净',
|
|
20
|
+
STATUS_SNAPSHOT_ORPHANED: '(孤立)',
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('../../../src/utils/index.js', () => ({
|
|
25
|
+
validateMainWorktree: vi.fn(),
|
|
26
|
+
getProjectName: vi.fn(),
|
|
27
|
+
getCurrentBranch: vi.fn(),
|
|
28
|
+
isWorkingDirClean: vi.fn(),
|
|
29
|
+
getProjectWorktrees: vi.fn(),
|
|
30
|
+
getCommitCountAhead: vi.fn(),
|
|
31
|
+
getCommitCountBehind: vi.fn(),
|
|
32
|
+
getDiffStat: vi.fn(),
|
|
33
|
+
hasMergeConflict: vi.fn(),
|
|
34
|
+
hasLocalCommits: vi.fn(),
|
|
35
|
+
hasSnapshot: vi.fn(),
|
|
36
|
+
getProjectSnapshotBranches: vi.fn(),
|
|
37
|
+
printInfo: vi.fn(),
|
|
38
|
+
printDoubleSeparator: vi.fn(),
|
|
39
|
+
printSeparator: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
import { registerStatusCommand } from '../../../src/commands/status.js';
|
|
43
|
+
import {
|
|
44
|
+
getProjectName,
|
|
45
|
+
getCurrentBranch,
|
|
46
|
+
isWorkingDirClean,
|
|
47
|
+
getProjectWorktrees,
|
|
48
|
+
getCommitCountAhead,
|
|
49
|
+
getCommitCountBehind,
|
|
50
|
+
getDiffStat,
|
|
51
|
+
hasMergeConflict,
|
|
52
|
+
hasLocalCommits,
|
|
53
|
+
hasSnapshot,
|
|
54
|
+
getProjectSnapshotBranches,
|
|
55
|
+
printInfo,
|
|
56
|
+
} from '../../../src/utils/index.js';
|
|
57
|
+
|
|
58
|
+
const mockedGetProjectName = vi.mocked(getProjectName);
|
|
59
|
+
const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
|
|
60
|
+
const mockedIsWorkingDirClean = vi.mocked(isWorkingDirClean);
|
|
61
|
+
const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
|
|
62
|
+
const mockedGetCommitCountAhead = vi.mocked(getCommitCountAhead);
|
|
63
|
+
const mockedGetCommitCountBehind = vi.mocked(getCommitCountBehind);
|
|
64
|
+
const mockedGetDiffStat = vi.mocked(getDiffStat);
|
|
65
|
+
const mockedHasMergeConflict = vi.mocked(hasMergeConflict);
|
|
66
|
+
const mockedHasLocalCommits = vi.mocked(hasLocalCommits);
|
|
67
|
+
const mockedHasSnapshot = vi.mocked(hasSnapshot);
|
|
68
|
+
const mockedGetProjectSnapshotBranches = vi.mocked(getProjectSnapshotBranches);
|
|
69
|
+
const mockedPrintInfo = vi.mocked(printInfo);
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
mockedGetProjectName.mockReturnValue('test-project');
|
|
73
|
+
mockedGetCurrentBranch.mockReturnValue('main');
|
|
74
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
75
|
+
mockedGetProjectWorktrees.mockReturnValue([]);
|
|
76
|
+
mockedGetProjectSnapshotBranches.mockReturnValue([]);
|
|
77
|
+
mockedGetCommitCountAhead.mockReturnValue(0);
|
|
78
|
+
mockedGetCommitCountBehind.mockReturnValue(0);
|
|
79
|
+
mockedGetDiffStat.mockReturnValue({ insertions: 0, deletions: 0 });
|
|
80
|
+
mockedHasMergeConflict.mockReturnValue(false);
|
|
81
|
+
mockedHasLocalCommits.mockReturnValue(false);
|
|
82
|
+
mockedHasSnapshot.mockReturnValue(false);
|
|
83
|
+
mockedPrintInfo.mockReset();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('registerStatusCommand', () => {
|
|
87
|
+
it('注册 status 命令', () => {
|
|
88
|
+
const program = new Command();
|
|
89
|
+
registerStatusCommand(program);
|
|
90
|
+
const cmd = program.commands.find((c) => c.name() === 'status');
|
|
91
|
+
expect(cmd).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('handleStatus', () => {
|
|
96
|
+
it('无 worktree 时文本输出', () => {
|
|
97
|
+
const program = new Command();
|
|
98
|
+
program.exitOverride();
|
|
99
|
+
registerStatusCommand(program);
|
|
100
|
+
program.parse(['status'], { from: 'user' });
|
|
101
|
+
|
|
102
|
+
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('--json 输出完整 JSON 结构', () => {
|
|
106
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
107
|
+
{ path: '/path/feature', branch: 'feature' },
|
|
108
|
+
]);
|
|
109
|
+
mockedGetCommitCountAhead.mockReturnValue(2);
|
|
110
|
+
mockedGetDiffStat.mockReturnValue({ insertions: 10, deletions: 5 });
|
|
111
|
+
mockedHasLocalCommits.mockReturnValue(true);
|
|
112
|
+
|
|
113
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
114
|
+
|
|
115
|
+
const program = new Command();
|
|
116
|
+
program.exitOverride();
|
|
117
|
+
registerStatusCommand(program);
|
|
118
|
+
program.parse(['status', '--json'], { from: 'user' });
|
|
119
|
+
|
|
120
|
+
const jsonCall = consoleSpy.mock.calls.find((call) => {
|
|
121
|
+
try { JSON.parse(call[0]); return true; } catch { return false; }
|
|
122
|
+
});
|
|
123
|
+
expect(jsonCall).toBeDefined();
|
|
124
|
+
const parsed = JSON.parse(jsonCall![0]);
|
|
125
|
+
expect(parsed.main.projectName).toBe('test-project');
|
|
126
|
+
expect(parsed.main.branch).toBe('main');
|
|
127
|
+
expect(parsed.totalWorktrees).toBe(1);
|
|
128
|
+
expect(parsed.worktrees).toHaveLength(1);
|
|
129
|
+
expect(parsed.worktrees[0].branch).toBe('feature');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('有 worktree 时收集正确的变更状态', () => {
|
|
133
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
134
|
+
{ path: '/path/feature', branch: 'feature' },
|
|
135
|
+
]);
|
|
136
|
+
// 模拟冲突状态
|
|
137
|
+
mockedHasMergeConflict.mockReturnValue(true);
|
|
138
|
+
|
|
139
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
140
|
+
|
|
141
|
+
const program = new Command();
|
|
142
|
+
program.exitOverride();
|
|
143
|
+
registerStatusCommand(program);
|
|
144
|
+
program.parse(['status', '--json'], { from: 'user' });
|
|
145
|
+
|
|
146
|
+
const jsonCall = consoleSpy.mock.calls.find((call) => {
|
|
147
|
+
try { JSON.parse(call[0]); return true; } catch { return false; }
|
|
148
|
+
});
|
|
149
|
+
const parsed = JSON.parse(jsonCall![0]);
|
|
150
|
+
expect(parsed.worktrees[0].changeStatus).toBe('conflict');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('主 worktree 不干净时 isClean=false', () => {
|
|
154
|
+
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
155
|
+
|
|
156
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
157
|
+
|
|
158
|
+
const program = new Command();
|
|
159
|
+
program.exitOverride();
|
|
160
|
+
registerStatusCommand(program);
|
|
161
|
+
program.parse(['status', '--json'], { from: 'user' });
|
|
162
|
+
|
|
163
|
+
const jsonCall = consoleSpy.mock.calls.find((call) => {
|
|
164
|
+
try { JSON.parse(call[0]); return true; } catch { return false; }
|
|
165
|
+
});
|
|
166
|
+
const parsed = JSON.parse(jsonCall![0]);
|
|
167
|
+
expect(parsed.main.isClean).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('存在快照时标识孤立快照', () => {
|
|
171
|
+
mockedGetProjectSnapshotBranches.mockReturnValue(['feature', 'deleted-branch']);
|
|
172
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
173
|
+
{ path: '/path/feature', branch: 'feature' },
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
177
|
+
|
|
178
|
+
const program = new Command();
|
|
179
|
+
program.exitOverride();
|
|
180
|
+
registerStatusCommand(program);
|
|
181
|
+
program.parse(['status', '--json'], { from: 'user' });
|
|
182
|
+
|
|
183
|
+
const jsonCall = consoleSpy.mock.calls.find((call) => {
|
|
184
|
+
try { JSON.parse(call[0]); return true; } catch { return false; }
|
|
185
|
+
});
|
|
186
|
+
const parsed = JSON.parse(jsonCall![0]);
|
|
187
|
+
expect(parsed.snapshots).toHaveLength(2);
|
|
188
|
+
expect(parsed.snapshots[0]).toEqual({ branch: 'feature', worktreeExists: true });
|
|
189
|
+
expect(parsed.snapshots[1]).toEqual({ branch: 'deleted-branch', worktreeExists: false });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('uncommitted 变更状态正确检测', () => {
|
|
193
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
194
|
+
{ path: '/path/feature', branch: 'feature' },
|
|
195
|
+
]);
|
|
196
|
+
mockedHasMergeConflict.mockReturnValue(false);
|
|
197
|
+
mockedIsWorkingDirClean
|
|
198
|
+
.mockReturnValueOnce(true) // 主 worktree
|
|
199
|
+
.mockReturnValueOnce(false); // 目标 worktree 不干净
|
|
200
|
+
|
|
201
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
202
|
+
|
|
203
|
+
const program = new Command();
|
|
204
|
+
program.exitOverride();
|
|
205
|
+
registerStatusCommand(program);
|
|
206
|
+
program.parse(['status', '--json'], { from: 'user' });
|
|
207
|
+
|
|
208
|
+
const jsonCall = consoleSpy.mock.calls.find((call) => {
|
|
209
|
+
try { JSON.parse(call[0]); return true; } catch { return false; }
|
|
210
|
+
});
|
|
211
|
+
const parsed = JSON.parse(jsonCall![0]);
|
|
212
|
+
expect(parsed.worktrees[0].changeStatus).toBe('uncommitted');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
SYNC_NO_WORKTREES: '没有可用的 worktree',
|
|
21
|
+
SYNC_SELECT_BRANCH: '选择要同步的分支',
|
|
22
|
+
SYNC_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
|
|
23
|
+
SYNC_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
|
|
24
|
+
SYNC_AUTO_COMMITTED: (branch: string) => `已自动保存 ${branch} 的未提交变更`,
|
|
25
|
+
SYNC_MERGING: (branch: string, mainBranch: string) => `正在将 ${mainBranch} 合并到 ${branch}...`,
|
|
26
|
+
SYNC_CONFLICT: (path: string) => `合并冲突,请手动解决: ${path}`,
|
|
27
|
+
SYNC_SUCCESS: (branch: string, mainBranch: string) => `✓ 已将 ${mainBranch} 同步到 ${branch}`,
|
|
28
|
+
},
|
|
29
|
+
AUTO_SAVE_COMMIT_MESSAGE: 'clawt:auto-save',
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('../../../src/utils/index.js', () => ({
|
|
33
|
+
validateMainWorktree: vi.fn(),
|
|
34
|
+
getGitTopLevel: vi.fn(),
|
|
35
|
+
getProjectName: vi.fn(),
|
|
36
|
+
getProjectWorktrees: vi.fn(),
|
|
37
|
+
isWorkingDirClean: vi.fn(),
|
|
38
|
+
gitAddAll: vi.fn(),
|
|
39
|
+
gitCommit: vi.fn(),
|
|
40
|
+
gitMerge: vi.fn(),
|
|
41
|
+
hasMergeConflict: vi.fn(),
|
|
42
|
+
getCurrentBranch: vi.fn(),
|
|
43
|
+
hasSnapshot: vi.fn(),
|
|
44
|
+
removeSnapshot: vi.fn(),
|
|
45
|
+
printSuccess: vi.fn(),
|
|
46
|
+
printInfo: vi.fn(),
|
|
47
|
+
printWarning: vi.fn(),
|
|
48
|
+
resolveTargetWorktree: vi.fn(),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
import { registerSyncCommand } from '../../../src/commands/sync.js';
|
|
52
|
+
import {
|
|
53
|
+
getGitTopLevel,
|
|
54
|
+
getProjectName,
|
|
55
|
+
getProjectWorktrees,
|
|
56
|
+
isWorkingDirClean,
|
|
57
|
+
gitAddAll,
|
|
58
|
+
gitCommit,
|
|
59
|
+
gitMerge,
|
|
60
|
+
hasMergeConflict,
|
|
61
|
+
getCurrentBranch,
|
|
62
|
+
hasSnapshot,
|
|
63
|
+
removeSnapshot,
|
|
64
|
+
printSuccess,
|
|
65
|
+
printWarning,
|
|
66
|
+
resolveTargetWorktree,
|
|
67
|
+
} from '../../../src/utils/index.js';
|
|
68
|
+
|
|
69
|
+
const mockedGetGitTopLevel = vi.mocked(getGitTopLevel);
|
|
70
|
+
const mockedGetProjectName = vi.mocked(getProjectName);
|
|
71
|
+
const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
|
|
72
|
+
const mockedIsWorkingDirClean = vi.mocked(isWorkingDirClean);
|
|
73
|
+
const mockedGitAddAll = vi.mocked(gitAddAll);
|
|
74
|
+
const mockedGitCommit = vi.mocked(gitCommit);
|
|
75
|
+
const mockedGitMerge = vi.mocked(gitMerge);
|
|
76
|
+
const mockedHasMergeConflict = vi.mocked(hasMergeConflict);
|
|
77
|
+
const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
|
|
78
|
+
const mockedHasSnapshot = vi.mocked(hasSnapshot);
|
|
79
|
+
const mockedRemoveSnapshot = vi.mocked(removeSnapshot);
|
|
80
|
+
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
81
|
+
const mockedPrintWarning = vi.mocked(printWarning);
|
|
82
|
+
const mockedResolveTargetWorktree = vi.mocked(resolveTargetWorktree);
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
mockedGetGitTopLevel.mockReturnValue('/repo');
|
|
86
|
+
mockedGetProjectName.mockReturnValue('test-project');
|
|
87
|
+
mockedGetCurrentBranch.mockReturnValue('main');
|
|
88
|
+
mockedGetProjectWorktrees.mockReset();
|
|
89
|
+
mockedIsWorkingDirClean.mockReset();
|
|
90
|
+
mockedGitAddAll.mockReset();
|
|
91
|
+
mockedGitCommit.mockReset();
|
|
92
|
+
mockedGitMerge.mockReset();
|
|
93
|
+
mockedHasMergeConflict.mockReset();
|
|
94
|
+
mockedHasSnapshot.mockReset();
|
|
95
|
+
mockedRemoveSnapshot.mockReset();
|
|
96
|
+
mockedPrintSuccess.mockReset();
|
|
97
|
+
mockedPrintWarning.mockReset();
|
|
98
|
+
mockedResolveTargetWorktree.mockReset();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('registerSyncCommand', () => {
|
|
102
|
+
it('注册 sync 命令', () => {
|
|
103
|
+
const program = new Command();
|
|
104
|
+
registerSyncCommand(program);
|
|
105
|
+
const cmd = program.commands.find((c) => c.name() === 'sync');
|
|
106
|
+
expect(cmd).toBeDefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('handleSync', () => {
|
|
111
|
+
it('目标 worktree 干净时直接合并', async () => {
|
|
112
|
+
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
113
|
+
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
114
|
+
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
115
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
116
|
+
mockedHasSnapshot.mockReturnValue(false);
|
|
117
|
+
|
|
118
|
+
const program = new Command();
|
|
119
|
+
program.exitOverride();
|
|
120
|
+
registerSyncCommand(program);
|
|
121
|
+
await program.parseAsync(['sync', '-b', 'feature'], { from: 'user' });
|
|
122
|
+
|
|
123
|
+
expect(mockedGitAddAll).not.toHaveBeenCalled();
|
|
124
|
+
expect(mockedGitMerge).toHaveBeenCalledWith('main', '/path/feature');
|
|
125
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('目标 worktree 有未提交变更时自动保存后合并', async () => {
|
|
129
|
+
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
130
|
+
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
131
|
+
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
132
|
+
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
133
|
+
mockedHasSnapshot.mockReturnValue(false);
|
|
134
|
+
|
|
135
|
+
const program = new Command();
|
|
136
|
+
program.exitOverride();
|
|
137
|
+
registerSyncCommand(program);
|
|
138
|
+
await program.parseAsync(['sync', '-b', 'feature'], { from: 'user' });
|
|
139
|
+
|
|
140
|
+
expect(mockedGitAddAll).toHaveBeenCalledWith('/path/feature');
|
|
141
|
+
expect(mockedGitCommit).toHaveBeenCalledWith('clawt:auto-save', '/path/feature');
|
|
142
|
+
expect(mockedGitMerge).toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('合并冲突时输出警告并返回', async () => {
|
|
146
|
+
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
147
|
+
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
148
|
+
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
149
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
150
|
+
mockedGitMerge.mockImplementation(() => { throw new Error('conflict'); });
|
|
151
|
+
mockedHasMergeConflict.mockReturnValue(true);
|
|
152
|
+
|
|
153
|
+
const program = new Command();
|
|
154
|
+
program.exitOverride();
|
|
155
|
+
registerSyncCommand(program);
|
|
156
|
+
await program.parseAsync(['sync', '-b', 'feature'], { from: 'user' });
|
|
157
|
+
|
|
158
|
+
expect(mockedPrintWarning).toHaveBeenCalled();
|
|
159
|
+
expect(mockedPrintSuccess).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('合并成功后清理 validate 快照', async () => {
|
|
163
|
+
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
164
|
+
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
165
|
+
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
166
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
167
|
+
mockedHasSnapshot.mockReturnValue(true);
|
|
168
|
+
|
|
169
|
+
const program = new Command();
|
|
170
|
+
program.exitOverride();
|
|
171
|
+
registerSyncCommand(program);
|
|
172
|
+
await program.parseAsync(['sync', '-b', 'feature'], { from: 'user' });
|
|
173
|
+
|
|
174
|
+
expect(mockedRemoveSnapshot).toHaveBeenCalledWith('test-project', 'feature');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('合并成功且无快照时不调用 removeSnapshot', async () => {
|
|
178
|
+
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
179
|
+
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
180
|
+
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
181
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
182
|
+
mockedHasSnapshot.mockReturnValue(false);
|
|
183
|
+
|
|
184
|
+
const program = new Command();
|
|
185
|
+
program.exitOverride();
|
|
186
|
+
registerSyncCommand(program);
|
|
187
|
+
await program.parseAsync(['sync', '-b', 'feature'], { from: 'user' });
|
|
188
|
+
|
|
189
|
+
expect(mockedRemoveSnapshot).not.toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('合并失败(非冲突错误)时向上抛出', async () => {
|
|
193
|
+
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
194
|
+
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
195
|
+
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
196
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
197
|
+
mockedGitMerge.mockImplementation(() => { throw new Error('other error'); });
|
|
198
|
+
mockedHasMergeConflict.mockReturnValue(false);
|
|
199
|
+
|
|
200
|
+
const program = new Command();
|
|
201
|
+
program.exitOverride();
|
|
202
|
+
registerSyncCommand(program);
|
|
203
|
+
|
|
204
|
+
await expect(
|
|
205
|
+
program.parseAsync(['sync', '-b', 'feature'], { from: 'user' }),
|
|
206
|
+
).rejects.toThrow();
|
|
207
|
+
});
|
|
208
|
+
});
|