clawt 3.10.4 → 3.10.6
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/AGENTS.md +16 -0
- package/dist/index.js +228 -86
- package/dist/postinstall.js +27 -0
- package/docs/create.md +1 -0
- package/docs/list.md +21 -10
- package/docs/merge.md +1 -0
- package/docs/remove.md +2 -0
- package/docs/spec.md +4 -1
- package/docs/status.md +9 -1
- package/docs/superpowers/findings/2026-06-01-sync-validate-diverged-findings.md +203 -0
- package/docs/superpowers/findings/2026-06-09-worktree-base-branch-findings.md +58 -0
- package/docs/superpowers/plans/2026-06-01-validate-ignored-files-conflict.md +412 -0
- package/docs/superpowers/plans/2026-06-09-worktree-base-branch.md +386 -0
- package/docs/superpowers/specs/2026-06-01-validate-ignored-files-conflict-design.md +76 -0
- package/docs/superpowers/specs/2026-06-09-worktree-base-branch-design.md +169 -0
- package/docs/validate.md +42 -5
- package/package.json +1 -1
- package/src/commands/list.ts +5 -3
- package/src/commands/merge.ts +1 -1
- package/src/commands/remove.ts +3 -0
- package/src/commands/status.ts +5 -0
- package/src/constants/messages/validate.ts +17 -0
- package/src/types/status.ts +2 -0
- package/src/types/worktree.ts +12 -0
- package/src/utils/formatter.ts +22 -0
- package/src/utils/git-core.ts +23 -0
- package/src/utils/index.ts +4 -2
- package/src/utils/interactive-panel-render.ts +6 -3
- package/src/utils/validate-core.ts +52 -0
- package/src/utils/worktree-metadata.ts +82 -0
- package/src/utils/worktree.ts +29 -10
- package/tests/helpers/fixtures.ts +1 -0
- package/tests/unit/commands/cover-validate.test.ts +4 -4
- package/tests/unit/commands/create.test.ts +3 -3
- package/tests/unit/commands/list.test.ts +66 -3
- package/tests/unit/commands/merge.test.ts +1 -1
- package/tests/unit/commands/remove.test.ts +24 -18
- package/tests/unit/commands/resume.test.ts +21 -21
- package/tests/unit/commands/run.test.ts +17 -17
- package/tests/unit/commands/status.test.ts +85 -10
- package/tests/unit/commands/sync.test.ts +4 -4
- package/tests/unit/commands/validate.test.ts +1 -1
- package/tests/unit/utils/git-core.test.ts +43 -0
- package/tests/unit/utils/interactive-panel-render.test.ts +124 -0
- package/tests/unit/utils/validate-core.test.ts +60 -0
- package/tests/unit/utils/worktree-matcher.test.ts +2 -2
- package/tests/unit/utils/worktree-metadata.test.ts +91 -0
- package/tests/unit/utils/worktree.test.ts +65 -0
|
@@ -45,8 +45,8 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
45
45
|
getProjectName: vi.fn().mockReturnValue('test-project'),
|
|
46
46
|
getGitTopLevel: vi.fn().mockReturnValue('/repo'),
|
|
47
47
|
getCurrentBranch: vi.fn().mockReturnValue('clawt-validate-feature'),
|
|
48
|
-
getProjectWorktrees: vi.fn().mockReturnValue([{ path: '/path/feature', branch: 'feature' }]),
|
|
49
|
-
findExactMatch: vi.fn().mockReturnValue({ path: '/path/feature', branch: 'feature' }),
|
|
48
|
+
getProjectWorktrees: vi.fn().mockReturnValue([{ path: '/path/feature', branch: 'feature', baseBranch: null }]),
|
|
49
|
+
findExactMatch: vi.fn().mockReturnValue({ path: '/path/feature', branch: 'feature', baseBranch: null }),
|
|
50
50
|
hasSnapshot: vi.fn().mockReturnValue(true),
|
|
51
51
|
readSnapshot: vi.fn().mockReturnValue({ treeHash: 'snapshot-tree-hash', headCommitHash: '', stagedTreeHash: '' }),
|
|
52
52
|
writeSnapshot: vi.fn(),
|
|
@@ -103,8 +103,8 @@ beforeEach(() => {
|
|
|
103
103
|
vi.clearAllMocks();
|
|
104
104
|
// 恢复默认 mock 值
|
|
105
105
|
mockedGetCurrentBranch.mockReturnValue('clawt-validate-feature');
|
|
106
|
-
mockedGetProjectWorktrees.mockReturnValue([{ path: '/path/feature', branch: 'feature' }]);
|
|
107
|
-
mockedFindExactMatch.mockReturnValue({ path: '/path/feature', branch: 'feature' });
|
|
106
|
+
mockedGetProjectWorktrees.mockReturnValue([{ path: '/path/feature', branch: 'feature', baseBranch: null }]);
|
|
107
|
+
mockedFindExactMatch.mockReturnValue({ path: '/path/feature', branch: 'feature', baseBranch: null });
|
|
108
108
|
mockedHasSnapshot.mockReturnValue(true);
|
|
109
109
|
mockedReadSnapshot.mockReturnValue({ treeHash: 'snapshot-tree-hash', headCommitHash: '', stagedTreeHash: '' });
|
|
110
110
|
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
@@ -67,7 +67,7 @@ describe('registerCreateCommand', () => {
|
|
|
67
67
|
describe('handleCreate', () => {
|
|
68
68
|
it('成功创建 worktree', async () => {
|
|
69
69
|
mockedCreateWorktrees.mockReturnValue([
|
|
70
|
-
{ path: '/path/feature', branch: 'feature' },
|
|
70
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: null },
|
|
71
71
|
]);
|
|
72
72
|
|
|
73
73
|
const program = new Command();
|
|
@@ -82,8 +82,8 @@ describe('handleCreate', () => {
|
|
|
82
82
|
|
|
83
83
|
it('支持 -n 指定创建数量', async () => {
|
|
84
84
|
mockedCreateWorktrees.mockReturnValue([
|
|
85
|
-
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
86
|
-
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
85
|
+
{ path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
|
|
86
|
+
{ path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
|
|
87
87
|
]);
|
|
88
88
|
|
|
89
89
|
const program = new Command();
|
|
@@ -24,8 +24,17 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
24
24
|
formatWorktreeStatus: vi.fn(),
|
|
25
25
|
isWorktreeIdle: vi.fn(),
|
|
26
26
|
printInfo: vi.fn(),
|
|
27
|
+
formatBaseBranchInline: vi.fn((baseBranch: string | null | undefined) => `<- ${baseBranch ?? '未记录'}`),
|
|
27
28
|
}));
|
|
28
29
|
|
|
30
|
+
vi.mock('../../../src/utils/i18n.js', async (importOriginal) => {
|
|
31
|
+
const actual = await importOriginal<typeof import('../../../src/utils/i18n.js')>();
|
|
32
|
+
return {
|
|
33
|
+
...actual,
|
|
34
|
+
getCurrentLanguage: vi.fn(() => 'zh'),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
29
38
|
import { registerListCommand } from '../../../src/commands/list.js';
|
|
30
39
|
import { runPreChecks, getProjectName, getProjectWorktrees, getWorktreeStatus, printInfo } from '../../../src/utils/index.js';
|
|
31
40
|
|
|
@@ -69,7 +78,7 @@ describe('handleList', () => {
|
|
|
69
78
|
it('有 worktree 时文本输出', async () => {
|
|
70
79
|
mockedGetProjectName.mockReturnValue('test-project');
|
|
71
80
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
72
|
-
{ path: '/path/feature', branch: 'feature' },
|
|
81
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: null },
|
|
73
82
|
]);
|
|
74
83
|
mockedGetWorktreeStatus.mockReturnValue({
|
|
75
84
|
commitCount: 3, insertions: 10, deletions: 5, hasDirtyFiles: false,
|
|
@@ -86,7 +95,7 @@ describe('handleList', () => {
|
|
|
86
95
|
it('--json 输出 JSON 格式', async () => {
|
|
87
96
|
mockedGetProjectName.mockReturnValue('test-project');
|
|
88
97
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
89
|
-
{ path: '/path/feature', branch: 'feature' },
|
|
98
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: null },
|
|
90
99
|
]);
|
|
91
100
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
92
101
|
|
|
@@ -108,7 +117,7 @@ describe('handleList', () => {
|
|
|
108
117
|
it('worktree 状态不可用时显示提示', async () => {
|
|
109
118
|
mockedGetProjectName.mockReturnValue('test-project');
|
|
110
119
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
111
|
-
{ path: '/path/feature', branch: 'feature' },
|
|
120
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: null },
|
|
112
121
|
]);
|
|
113
122
|
mockedGetWorktreeStatus.mockReturnValue(null);
|
|
114
123
|
|
|
@@ -119,4 +128,58 @@ describe('handleList', () => {
|
|
|
119
128
|
|
|
120
129
|
expect(mockedGetWorktreeStatus).toHaveBeenCalled();
|
|
121
130
|
});
|
|
131
|
+
|
|
132
|
+
it('--json 输出包含 baseBranch 字段', async () => {
|
|
133
|
+
mockedGetProjectName.mockReturnValue('test-project');
|
|
134
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
135
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: 'test' },
|
|
136
|
+
]);
|
|
137
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
138
|
+
|
|
139
|
+
const program = new Command();
|
|
140
|
+
program.exitOverride();
|
|
141
|
+
registerListCommand(program);
|
|
142
|
+
await program.parseAsync(['list', '--json'], { from: 'user' });
|
|
143
|
+
|
|
144
|
+
const jsonCall = consoleSpy.mock.calls.find((call) => {
|
|
145
|
+
try { JSON.parse(call[0]); return true; } catch { return false; }
|
|
146
|
+
});
|
|
147
|
+
expect(jsonCall).toBeDefined();
|
|
148
|
+
const parsed = JSON.parse(jsonCall![0]);
|
|
149
|
+
expect(parsed.worktrees[0].baseBranch).toBe('test');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('文本输出包含来源分支(有元数据时)', async () => {
|
|
153
|
+
mockedGetProjectName.mockReturnValue('test-project');
|
|
154
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
155
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: 'test' },
|
|
156
|
+
]);
|
|
157
|
+
mockedGetWorktreeStatus.mockReturnValue({
|
|
158
|
+
commitCount: 3, insertions: 10, deletions: 5, hasDirtyFiles: false,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const program = new Command();
|
|
162
|
+
program.exitOverride();
|
|
163
|
+
registerListCommand(program);
|
|
164
|
+
await program.parseAsync(['list'], { from: 'user' });
|
|
165
|
+
|
|
166
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('<- test'));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('文本输出包含"未记录"(无元数据时)', async () => {
|
|
170
|
+
mockedGetProjectName.mockReturnValue('test-project');
|
|
171
|
+
mockedGetProjectWorktrees.mockReturnValue([
|
|
172
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: null },
|
|
173
|
+
]);
|
|
174
|
+
mockedGetWorktreeStatus.mockReturnValue({
|
|
175
|
+
commitCount: 3, insertions: 10, deletions: 5, hasDirtyFiles: false,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const program = new Command();
|
|
179
|
+
program.exitOverride();
|
|
180
|
+
registerListCommand(program);
|
|
181
|
+
await program.parseAsync(['list'], { from: 'user' });
|
|
182
|
+
|
|
183
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('未记录'));
|
|
184
|
+
});
|
|
122
185
|
});
|
|
@@ -149,7 +149,7 @@ const mockedHandleMergeConflict = vi.mocked(handleMergeConflict);
|
|
|
149
149
|
const mockedPromptCommitMessage = vi.mocked(promptCommitMessage);
|
|
150
150
|
const mockedIsNonInteractive = vi.mocked(isNonInteractive);
|
|
151
151
|
|
|
152
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
152
|
+
const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
|
|
153
153
|
|
|
154
154
|
beforeEach(() => {
|
|
155
155
|
mockedGetGitTopLevel.mockReturnValue('/repo');
|
|
@@ -57,6 +57,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
57
57
|
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
58
58
|
guardMainWorkBranchExists: vi.fn(),
|
|
59
59
|
isNonInteractive: vi.fn().mockReturnValue(false),
|
|
60
|
+
removeWorktreeMetadata: vi.fn(),
|
|
60
61
|
}));
|
|
61
62
|
|
|
62
63
|
import { registerRemoveCommand } from '../../../src/commands/remove.js';
|
|
@@ -75,6 +76,7 @@ import {
|
|
|
75
76
|
printHint,
|
|
76
77
|
resolveTargetWorktrees,
|
|
77
78
|
getCurrentBranch,
|
|
79
|
+
removeWorktreeMetadata,
|
|
78
80
|
} from '../../../src/utils/index.js';
|
|
79
81
|
|
|
80
82
|
const mockedGetProjectName = vi.mocked(getProjectName);
|
|
@@ -90,6 +92,7 @@ const mockedPrintError = vi.mocked(printError);
|
|
|
90
92
|
const mockedPrintHint = vi.mocked(printHint);
|
|
91
93
|
const mockedResolveTargetWorktrees = vi.mocked(resolveTargetWorktrees);
|
|
92
94
|
const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
|
|
95
|
+
const mockedRemoveWorktreeMetadata = vi.mocked(removeWorktreeMetadata);
|
|
93
96
|
|
|
94
97
|
beforeEach(() => {
|
|
95
98
|
vi.mocked(runPreChecks).mockReset();
|
|
@@ -107,6 +110,7 @@ beforeEach(() => {
|
|
|
107
110
|
mockedResolveTargetWorktrees.mockReset();
|
|
108
111
|
mockedGetCurrentBranch.mockReset();
|
|
109
112
|
mockedGetCurrentBranch.mockReturnValue('main');
|
|
113
|
+
mockedRemoveWorktreeMetadata.mockReset();
|
|
110
114
|
});
|
|
111
115
|
|
|
112
116
|
describe('registerRemoveCommand', () => {
|
|
@@ -121,8 +125,8 @@ describe('registerRemoveCommand', () => {
|
|
|
121
125
|
describe('handleRemove', () => {
|
|
122
126
|
it('--all 移除所有 worktree(autoDeleteBranch=true)', async () => {
|
|
123
127
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
124
|
-
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
125
|
-
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
128
|
+
{ path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
|
|
129
|
+
{ path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
|
|
126
130
|
]);
|
|
127
131
|
mockedGetConfigValue.mockReturnValue(true);
|
|
128
132
|
|
|
@@ -133,16 +137,18 @@ describe('handleRemove', () => {
|
|
|
133
137
|
|
|
134
138
|
expect(mockedRemoveWorktreeByPath).toHaveBeenCalledTimes(2);
|
|
135
139
|
expect(mockedDeleteBranch).toHaveBeenCalledTimes(2);
|
|
140
|
+
expect(mockedRemoveWorktreeMetadata).toHaveBeenCalledWith('test-project', 'feature-1');
|
|
141
|
+
expect(mockedRemoveWorktreeMetadata).toHaveBeenCalledWith('test-project', 'feature-2');
|
|
136
142
|
expect(mockedRemoveProjectSnapshots).toHaveBeenCalledWith('test-project');
|
|
137
143
|
});
|
|
138
144
|
|
|
139
145
|
it('-b 精确匹配时通过 resolveTargetWorktrees 解析并移除', async () => {
|
|
140
146
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
141
|
-
{ path: '/path/feature', branch: 'feature' },
|
|
142
|
-
{ path: '/path/other', branch: 'other' },
|
|
147
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: null },
|
|
148
|
+
{ path: '/path/other', branch: 'other', baseBranch: null },
|
|
143
149
|
]);
|
|
144
150
|
mockedResolveTargetWorktrees.mockResolvedValue([
|
|
145
|
-
{ path: '/path/feature', branch: 'feature' },
|
|
151
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: null },
|
|
146
152
|
]);
|
|
147
153
|
mockedGetConfigValue.mockReturnValue(true);
|
|
148
154
|
|
|
@@ -158,14 +164,14 @@ describe('handleRemove', () => {
|
|
|
158
164
|
|
|
159
165
|
it('-b 模糊匹配多个时通过 resolveTargetWorktrees 解析并批量移除', async () => {
|
|
160
166
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
161
|
-
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
162
|
-
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
163
|
-
{ path: '/path/other', branch: 'other' },
|
|
167
|
+
{ path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
|
|
168
|
+
{ path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
|
|
169
|
+
{ path: '/path/other', branch: 'other', baseBranch: null },
|
|
164
170
|
]);
|
|
165
171
|
// 模拟用户多选了两个
|
|
166
172
|
mockedResolveTargetWorktrees.mockResolvedValue([
|
|
167
|
-
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
168
|
-
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
173
|
+
{ path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
|
|
174
|
+
{ path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
|
|
169
175
|
]);
|
|
170
176
|
mockedGetConfigValue.mockReturnValue(true);
|
|
171
177
|
|
|
@@ -179,11 +185,11 @@ describe('handleRemove', () => {
|
|
|
179
185
|
|
|
180
186
|
it('未指定 --all 或 -b 时通过 resolveTargetWorktrees 展示多选列表', async () => {
|
|
181
187
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
182
|
-
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
183
|
-
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
188
|
+
{ path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
|
|
189
|
+
{ path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
|
|
184
190
|
]);
|
|
185
191
|
mockedResolveTargetWorktrees.mockResolvedValue([
|
|
186
|
-
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
192
|
+
{ path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
|
|
187
193
|
]);
|
|
188
194
|
mockedGetConfigValue.mockReturnValue(true);
|
|
189
195
|
|
|
@@ -203,10 +209,10 @@ describe('handleRemove', () => {
|
|
|
203
209
|
|
|
204
210
|
it('autoDeleteBranch=false 时询问用户是否删除分支', async () => {
|
|
205
211
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
206
|
-
{ path: '/path/feature', branch: 'feature' },
|
|
212
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: null },
|
|
207
213
|
]);
|
|
208
214
|
mockedResolveTargetWorktrees.mockResolvedValue([
|
|
209
|
-
{ path: '/path/feature', branch: 'feature' },
|
|
215
|
+
{ path: '/path/feature', branch: 'feature', baseBranch: null },
|
|
210
216
|
]);
|
|
211
217
|
mockedGetConfigValue.mockReturnValue(false);
|
|
212
218
|
mockedConfirmAction.mockResolvedValue(false);
|
|
@@ -225,7 +231,7 @@ describe('handleRemove', () => {
|
|
|
225
231
|
|
|
226
232
|
it('-b 指定不存在的分支时 resolveTargetWorktrees 抛出错误', async () => {
|
|
227
233
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
228
|
-
{ path: '/path/other', branch: 'other' },
|
|
234
|
+
{ path: '/path/other', branch: 'other', baseBranch: null },
|
|
229
235
|
]);
|
|
230
236
|
mockedResolveTargetWorktrees.mockRejectedValue(new Error('未找到与 "nonexistent" 匹配的分支'));
|
|
231
237
|
|
|
@@ -240,8 +246,8 @@ describe('handleRemove', () => {
|
|
|
240
246
|
|
|
241
247
|
it('移除过程中部分失败时汇报并抛出错误', async () => {
|
|
242
248
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
243
|
-
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
244
|
-
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
249
|
+
{ path: '/path/feature-1', branch: 'feature-1', baseBranch: null },
|
|
250
|
+
{ path: '/path/feature-2', branch: 'feature-2', baseBranch: null },
|
|
245
251
|
]);
|
|
246
252
|
mockedGetConfigValue.mockReturnValue(true);
|
|
247
253
|
// 第一个成功,第二个失败
|
|
@@ -125,7 +125,7 @@ describe('registerResumeCommand', () => {
|
|
|
125
125
|
|
|
126
126
|
describe('handleResume', () => {
|
|
127
127
|
it('传 -b 时走标准解析流程', async () => {
|
|
128
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
128
|
+
const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
|
|
129
129
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
130
130
|
mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
|
|
131
131
|
mockedGetConfigValue.mockReturnValue(true);
|
|
@@ -143,8 +143,8 @@ describe('handleResume', () => {
|
|
|
143
143
|
|
|
144
144
|
it('不传 -b 且多个 worktree 时默认使用分组多选', async () => {
|
|
145
145
|
const worktrees = [
|
|
146
|
-
{ path: '/path/feature-a', branch: 'feature-a' },
|
|
147
|
-
{ path: '/path/feature-b', branch: 'feature-b' },
|
|
146
|
+
{ path: '/path/feature-a', branch: 'feature-a', baseBranch: null },
|
|
147
|
+
{ path: '/path/feature-b', branch: 'feature-b', baseBranch: null },
|
|
148
148
|
];
|
|
149
149
|
mockedGetProjectWorktrees.mockReturnValue(worktrees);
|
|
150
150
|
mockedPromptGroupedMultiSelectBranches.mockResolvedValue([worktrees[0]]);
|
|
@@ -163,7 +163,7 @@ describe('handleResume', () => {
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
it('仅 1 个 worktree 时走标准流程', async () => {
|
|
166
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
166
|
+
const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
|
|
167
167
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
168
168
|
mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
|
|
169
169
|
mockedGetConfigValue.mockReturnValue(true);
|
|
@@ -180,7 +180,7 @@ describe('handleResume', () => {
|
|
|
180
180
|
|
|
181
181
|
describe('handleResume — resumeInPlace 配置', () => {
|
|
182
182
|
it('resumeInPlace 为 true 时,单选在当前终端就地恢复', async () => {
|
|
183
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
183
|
+
const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
|
|
184
184
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
185
185
|
mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
|
|
186
186
|
mockedGetConfigValue.mockReturnValue(true);
|
|
@@ -196,7 +196,7 @@ describe('handleResume — resumeInPlace 配置', () => {
|
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
it('resumeInPlace 为 false 时,单选在新终端 Tab 中恢复', async () => {
|
|
199
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
199
|
+
const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
|
|
200
200
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
201
201
|
mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
|
|
202
202
|
mockedGetConfigValue.mockReturnValue(false);
|
|
@@ -214,7 +214,7 @@ describe('handleResume — resumeInPlace 配置', () => {
|
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
it('resumeInPlace 为 false 且无历史会话时,传 false 给新终端启动', async () => {
|
|
217
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
217
|
+
const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
|
|
218
218
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
219
219
|
mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
|
|
220
220
|
mockedGetConfigValue.mockReturnValue(false);
|
|
@@ -230,8 +230,8 @@ describe('handleResume — resumeInPlace 配置', () => {
|
|
|
230
230
|
|
|
231
231
|
it('多选时不受 resumeInPlace 影响,始终在新 Tab 中打开', async () => {
|
|
232
232
|
const worktrees = [
|
|
233
|
-
{ path: '/path/feature-a', branch: 'feature-a' },
|
|
234
|
-
{ path: '/path/feature-b', branch: 'feature-b' },
|
|
233
|
+
{ path: '/path/feature-a', branch: 'feature-a', baseBranch: null },
|
|
234
|
+
{ path: '/path/feature-b', branch: 'feature-b', baseBranch: null },
|
|
235
235
|
];
|
|
236
236
|
mockedGetProjectWorktrees.mockReturnValue(worktrees);
|
|
237
237
|
mockedPromptGroupedMultiSelectBranches.mockResolvedValue(worktrees);
|
|
@@ -251,8 +251,8 @@ describe('handleResume — resumeInPlace 配置', () => {
|
|
|
251
251
|
|
|
252
252
|
it('用户未选择任何分支时直接退出', async () => {
|
|
253
253
|
const worktrees = [
|
|
254
|
-
{ path: '/path/feature-a', branch: 'feature-a' },
|
|
255
|
-
{ path: '/path/feature-b', branch: 'feature-b' },
|
|
254
|
+
{ path: '/path/feature-a', branch: 'feature-a', baseBranch: null },
|
|
255
|
+
{ path: '/path/feature-b', branch: 'feature-b', baseBranch: null },
|
|
256
256
|
];
|
|
257
257
|
mockedGetProjectWorktrees.mockReturnValue(worktrees);
|
|
258
258
|
mockedPromptGroupedMultiSelectBranches.mockResolvedValue([]);
|
|
@@ -270,7 +270,7 @@ describe('handleResume — resumeInPlace 配置', () => {
|
|
|
270
270
|
|
|
271
271
|
describe('handleResume — 非交互式追问', () => {
|
|
272
272
|
it('--prompt + -b 有历史会话时传 [true]', async () => {
|
|
273
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
273
|
+
const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
|
|
274
274
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
275
275
|
mockedFindExactMatch.mockReturnValue(worktree);
|
|
276
276
|
mockedHasClaudeSessionHistory.mockReturnValue(true);
|
|
@@ -296,7 +296,7 @@ describe('handleResume — 非交互式追问', () => {
|
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
it('--prompt + -b 无历史会话时传 [false]', async () => {
|
|
299
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
299
|
+
const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
|
|
300
300
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
301
301
|
mockedFindExactMatch.mockReturnValue(worktree);
|
|
302
302
|
mockedHasClaudeSessionHistory.mockReturnValue(false);
|
|
@@ -339,7 +339,7 @@ describe('handleResume — 非交互式追问', () => {
|
|
|
339
339
|
|
|
340
340
|
it('--prompt 指定的分支不存在时报错', async () => {
|
|
341
341
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
342
|
-
{ path: '/path/other', branch: 'other' },
|
|
342
|
+
{ path: '/path/other', branch: 'other', baseBranch: null },
|
|
343
343
|
]);
|
|
344
344
|
mockedFindExactMatch.mockReturnValue(undefined);
|
|
345
345
|
|
|
@@ -354,8 +354,8 @@ describe('handleResume — 非交互式追问', () => {
|
|
|
354
354
|
|
|
355
355
|
it('-f 批量追问模式', async () => {
|
|
356
356
|
const worktrees = [
|
|
357
|
-
{ path: '/path/feat-a', branch: 'feat-a' },
|
|
358
|
-
{ path: '/path/feat-b', branch: 'feat-b' },
|
|
357
|
+
{ path: '/path/feat-a', branch: 'feat-a', baseBranch: null },
|
|
358
|
+
{ path: '/path/feat-b', branch: 'feat-b', baseBranch: null },
|
|
359
359
|
];
|
|
360
360
|
mockedLoadTaskFile.mockReturnValue([
|
|
361
361
|
{ branch: 'feat-a', task: '追问任务A' },
|
|
@@ -390,7 +390,7 @@ describe('handleResume — 非交互式追问', () => {
|
|
|
390
390
|
{ branch: 'nonexistent', task: '追问任务' },
|
|
391
391
|
]);
|
|
392
392
|
mockedGetProjectWorktrees.mockReturnValue([
|
|
393
|
-
{ path: '/path/feat-a', branch: 'feat-a' },
|
|
393
|
+
{ path: '/path/feat-a', branch: 'feat-a', baseBranch: null },
|
|
394
394
|
]);
|
|
395
395
|
mockedFindExactMatch.mockReturnValue(undefined);
|
|
396
396
|
|
|
@@ -404,7 +404,7 @@ describe('handleResume — 非交互式追问', () => {
|
|
|
404
404
|
});
|
|
405
405
|
|
|
406
406
|
it('-f + -c 传递并发数', async () => {
|
|
407
|
-
const worktree = { path: '/path/feat-a', branch: 'feat-a' };
|
|
407
|
+
const worktree = { path: '/path/feat-a', branch: 'feat-a', baseBranch: null };
|
|
408
408
|
mockedLoadTaskFile.mockReturnValue([
|
|
409
409
|
{ branch: 'feat-a', task: '追问' },
|
|
410
410
|
]);
|
|
@@ -429,9 +429,9 @@ describe('handleResume — 非交互式追问', () => {
|
|
|
429
429
|
|
|
430
430
|
it('-f 批量追问按 worktree 独立检查会话历史', async () => {
|
|
431
431
|
const worktrees = [
|
|
432
|
-
{ path: '/path/feat-a', branch: 'feat-a' },
|
|
433
|
-
{ path: '/path/feat-b', branch: 'feat-b' },
|
|
434
|
-
{ path: '/path/feat-c', branch: 'feat-c' },
|
|
432
|
+
{ path: '/path/feat-a', branch: 'feat-a', baseBranch: null },
|
|
433
|
+
{ path: '/path/feat-b', branch: 'feat-b', baseBranch: null },
|
|
434
|
+
{ path: '/path/feat-c', branch: 'feat-c', baseBranch: null },
|
|
435
435
|
];
|
|
436
436
|
mockedLoadTaskFile.mockReturnValue([
|
|
437
437
|
{ branch: 'feat-a', task: '任务A' },
|
|
@@ -263,7 +263,7 @@ describe('handleRun', () => {
|
|
|
263
263
|
it('未传 --tasks 时创建单个 worktree 并打开交互式界面', async () => {
|
|
264
264
|
mockedSanitizeBranchName.mockReturnValue('feature');
|
|
265
265
|
mockedCheckBranchExists.mockReturnValue(false);
|
|
266
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
266
|
+
const worktree = { path: '/path/feature', branch: 'feature', baseBranch: null };
|
|
267
267
|
mockedCreateWorktrees.mockReturnValue([worktree]);
|
|
268
268
|
|
|
269
269
|
const program = new Command();
|
|
@@ -290,8 +290,8 @@ describe('handleRun', () => {
|
|
|
290
290
|
|
|
291
291
|
it('传 --tasks 时创建对应数量 worktree 并并行执行', async () => {
|
|
292
292
|
const worktrees = [
|
|
293
|
-
{ path: '/path/feat-1', branch: 'feat-1' },
|
|
294
|
-
{ path: '/path/feat-2', branch: 'feat-2' },
|
|
293
|
+
{ path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
|
|
294
|
+
{ path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
|
|
295
295
|
];
|
|
296
296
|
mockedCreateWorktrees.mockReturnValue(worktrees);
|
|
297
297
|
|
|
@@ -314,7 +314,7 @@ describe('handleRun', () => {
|
|
|
314
314
|
});
|
|
315
315
|
|
|
316
316
|
it('任务执行失败时在通知中报告', async () => {
|
|
317
|
-
const worktrees = [{ path: '/path/feat-1', branch: 'feat-1' }];
|
|
317
|
+
const worktrees = [{ path: '/path/feat-1', branch: 'feat-1', baseBranch: null }];
|
|
318
318
|
mockedCreateWorktrees.mockReturnValue(worktrees);
|
|
319
319
|
|
|
320
320
|
const jsonOutput = JSON.stringify({
|
|
@@ -334,7 +334,7 @@ describe('handleRun', () => {
|
|
|
334
334
|
});
|
|
335
335
|
|
|
336
336
|
it('子进程发生错误时返回失败结果', async () => {
|
|
337
|
-
const worktrees = [{ path: '/path/feat-1', branch: 'feat-1' }];
|
|
337
|
+
const worktrees = [{ path: '/path/feat-1', branch: 'feat-1', baseBranch: null }];
|
|
338
338
|
mockedCreateWorktrees.mockReturnValue(worktrees);
|
|
339
339
|
|
|
340
340
|
// 创建会触发 error 事件的子进程
|
|
@@ -359,9 +359,9 @@ describe('handleRun', () => {
|
|
|
359
359
|
it('传 --concurrency 限制并发数', async () => {
|
|
360
360
|
mockedParseConcurrency.mockReturnValue(1);
|
|
361
361
|
const worktrees = [
|
|
362
|
-
{ path: '/path/feat-1', branch: 'feat-1' },
|
|
363
|
-
{ path: '/path/feat-2', branch: 'feat-2' },
|
|
364
|
-
{ path: '/path/feat-3', branch: 'feat-3' },
|
|
362
|
+
{ path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
|
|
363
|
+
{ path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
|
|
364
|
+
{ path: '/path/feat-3', branch: 'feat-3', baseBranch: null },
|
|
365
365
|
];
|
|
366
366
|
mockedCreateWorktrees.mockReturnValue(worktrees);
|
|
367
367
|
|
|
@@ -389,8 +389,8 @@ describe('handleRun', () => {
|
|
|
389
389
|
|
|
390
390
|
it('--concurrency 为 0 时不限制并发', async () => {
|
|
391
391
|
const worktrees = [
|
|
392
|
-
{ path: '/path/feat-1', branch: 'feat-1' },
|
|
393
|
-
{ path: '/path/feat-2', branch: 'feat-2' },
|
|
392
|
+
{ path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
|
|
393
|
+
{ path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
|
|
394
394
|
];
|
|
395
395
|
mockedCreateWorktrees.mockReturnValue(worktrees);
|
|
396
396
|
|
|
@@ -418,9 +418,9 @@ describe('handleRun', () => {
|
|
|
418
418
|
mockedParseConcurrency.mockReturnValue(2);
|
|
419
419
|
|
|
420
420
|
const worktrees = [
|
|
421
|
-
{ path: '/path/feat-1', branch: 'feat-1' },
|
|
422
|
-
{ path: '/path/feat-2', branch: 'feat-2' },
|
|
423
|
-
{ path: '/path/feat-3', branch: 'feat-3' },
|
|
421
|
+
{ path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
|
|
422
|
+
{ path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
|
|
423
|
+
{ path: '/path/feat-3', branch: 'feat-3', baseBranch: null },
|
|
424
424
|
];
|
|
425
425
|
mockedCreateWorktrees.mockReturnValue(worktrees);
|
|
426
426
|
|
|
@@ -450,8 +450,8 @@ describe('handleRun', () => {
|
|
|
450
450
|
{ branch: 'fix-bug', task: '修复问题' },
|
|
451
451
|
]);
|
|
452
452
|
const worktrees = [
|
|
453
|
-
{ path: '/path/feat-login', branch: 'feat-login' },
|
|
454
|
-
{ path: '/path/fix-bug', branch: 'fix-bug' },
|
|
453
|
+
{ path: '/path/feat-login', branch: 'feat-login', baseBranch: null },
|
|
454
|
+
{ path: '/path/fix-bug', branch: 'fix-bug', baseBranch: null },
|
|
455
455
|
];
|
|
456
456
|
mockedCreateWorktreesByBranches.mockReturnValue(worktrees);
|
|
457
457
|
|
|
@@ -480,8 +480,8 @@ describe('handleRun', () => {
|
|
|
480
480
|
{ task: '任务2' },
|
|
481
481
|
]);
|
|
482
482
|
const worktrees = [
|
|
483
|
-
{ path: '/path/feat-1', branch: 'feat-1' },
|
|
484
|
-
{ path: '/path/feat-2', branch: 'feat-2' },
|
|
483
|
+
{ path: '/path/feat-1', branch: 'feat-1', baseBranch: null },
|
|
484
|
+
{ path: '/path/feat-2', branch: 'feat-2', baseBranch: null },
|
|
485
485
|
];
|
|
486
486
|
mockedCreateWorktrees.mockReturnValue(worktrees);
|
|
487
487
|
|