clawt 2.19.0 → 3.0.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/README.md +12 -2
- package/dist/index.js +626 -219
- package/dist/postinstall.js +39 -6
- package/docs/alias.md +108 -0
- package/docs/completion.md +55 -0
- package/docs/config-file.md +43 -0
- package/docs/config.md +91 -0
- package/docs/create.md +85 -0
- package/docs/init.md +65 -0
- package/docs/list.md +67 -0
- package/docs/log.md +67 -0
- package/docs/merge.md +137 -0
- package/docs/notification.md +94 -0
- package/docs/projects.md +135 -0
- package/docs/remove.md +79 -0
- package/docs/reset.md +35 -0
- package/docs/resume.md +99 -0
- package/docs/run.md +146 -0
- package/docs/spec.md +156 -1879
- package/docs/status.md +155 -0
- package/docs/sync.md +114 -0
- package/docs/update-check.md +95 -0
- package/docs/validate.md +368 -0
- package/package.json +1 -1
- package/src/commands/alias.ts +1 -1
- package/src/commands/create.ts +10 -5
- package/src/commands/init.ts +75 -0
- package/src/commands/list.ts +1 -1
- package/src/commands/merge.ts +11 -4
- package/src/commands/remove.ts +10 -3
- package/src/commands/reset.ts +3 -0
- package/src/commands/resume.ts +9 -3
- package/src/commands/run.ts +9 -3
- package/src/commands/status.ts +1 -1
- package/src/commands/sync.ts +18 -6
- package/src/commands/validate.ts +46 -52
- package/src/constants/branch.ts +3 -0
- package/src/constants/config.ts +1 -1
- package/src/constants/index.ts +3 -3
- package/src/constants/messages/completion.ts +1 -1
- package/src/constants/messages/create.ts +3 -0
- package/src/constants/messages/index.ts +2 -0
- package/src/constants/messages/init.ts +18 -0
- package/src/constants/messages/remove.ts +2 -0
- package/src/constants/messages/sync.ts +3 -0
- package/src/constants/messages/validate.ts +6 -0
- package/src/constants/paths.ts +3 -0
- package/src/constants/prompt.ts +28 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +7 -1
- package/src/types/index.ts +2 -1
- package/src/types/projectConfig.ts +5 -0
- package/src/utils/config.ts +2 -1
- package/src/utils/git.ts +18 -0
- package/src/utils/index.ts +6 -1
- package/src/utils/json.ts +67 -0
- package/src/utils/project-config.ts +77 -0
- package/src/utils/validate-branch.ts +166 -0
- package/src/utils/worktree-matcher.ts +268 -1
- package/src/utils/worktree.ts +6 -2
- package/tests/unit/commands/create.test.ts +20 -16
- package/tests/unit/commands/init.test.ts +146 -0
- package/tests/unit/commands/merge.test.ts +7 -1
- package/tests/unit/commands/remove.test.ts +4 -0
- package/tests/unit/commands/reset.test.ts +2 -0
- package/tests/unit/commands/resume.test.ts +29 -8
- package/tests/unit/commands/run.test.ts +2 -0
- package/tests/unit/commands/sync.test.ts +6 -0
- package/tests/unit/commands/validate.test.ts +13 -0
- package/tests/unit/utils/config.test.ts +2 -2
- package/tests/unit/utils/project-config.test.ts +136 -0
- package/tests/unit/utils/update-checker.test.ts +28 -7
- package/tests/unit/utils/validate-branch.test.ts +272 -0
- package/tests/unit/utils/worktree-matcher.test.ts +142 -1
- package/tests/unit/utils/worktree.test.ts +6 -0
|
@@ -26,6 +26,10 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
26
26
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
27
27
|
validateMainWorktree: vi.fn(),
|
|
28
28
|
createWorktrees: vi.fn(),
|
|
29
|
+
getConfigValue: vi.fn().mockReturnValue(true),
|
|
30
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
31
|
+
ensureOnMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
29
33
|
printSuccess: vi.fn(),
|
|
30
34
|
printInfo: vi.fn(),
|
|
31
35
|
printSeparator: vi.fn(),
|
|
@@ -54,7 +58,7 @@ describe('registerCreateCommand', () => {
|
|
|
54
58
|
});
|
|
55
59
|
|
|
56
60
|
describe('handleCreate', () => {
|
|
57
|
-
it('成功创建 worktree', () => {
|
|
61
|
+
it('成功创建 worktree', async () => {
|
|
58
62
|
mockedCreateWorktrees.mockReturnValue([
|
|
59
63
|
{ path: '/path/feature', branch: 'feature' },
|
|
60
64
|
]);
|
|
@@ -62,14 +66,14 @@ describe('handleCreate', () => {
|
|
|
62
66
|
const program = new Command();
|
|
63
67
|
program.exitOverride();
|
|
64
68
|
registerCreateCommand(program);
|
|
65
|
-
program.
|
|
69
|
+
await program.parseAsync(['create', '-b', 'feature'], { from: 'user' });
|
|
66
70
|
|
|
67
71
|
expect(mockedValidateMainWorktree).toHaveBeenCalled();
|
|
68
72
|
expect(mockedCreateWorktrees).toHaveBeenCalledWith('feature', 1);
|
|
69
73
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
70
74
|
});
|
|
71
75
|
|
|
72
|
-
it('支持 -n 指定创建数量', () => {
|
|
76
|
+
it('支持 -n 指定创建数量', async () => {
|
|
73
77
|
mockedCreateWorktrees.mockReturnValue([
|
|
74
78
|
{ path: '/path/feature-1', branch: 'feature-1' },
|
|
75
79
|
{ path: '/path/feature-2', branch: 'feature-2' },
|
|
@@ -78,38 +82,38 @@ describe('handleCreate', () => {
|
|
|
78
82
|
const program = new Command();
|
|
79
83
|
program.exitOverride();
|
|
80
84
|
registerCreateCommand(program);
|
|
81
|
-
program.
|
|
85
|
+
await program.parseAsync(['create', '-b', 'feature', '-n', '2'], { from: 'user' });
|
|
82
86
|
|
|
83
87
|
expect(mockedCreateWorktrees).toHaveBeenCalledWith('feature', 2);
|
|
84
88
|
});
|
|
85
89
|
|
|
86
|
-
it('无效数量抛出 ClawtError', () => {
|
|
90
|
+
it('无效数量抛出 ClawtError', async () => {
|
|
87
91
|
const program = new Command();
|
|
88
92
|
program.exitOverride();
|
|
89
93
|
registerCreateCommand(program);
|
|
90
94
|
|
|
91
|
-
expect(
|
|
92
|
-
program.
|
|
93
|
-
|
|
95
|
+
await expect(
|
|
96
|
+
program.parseAsync(['create', '-b', 'feature', '-n', 'abc'], { from: 'user' }),
|
|
97
|
+
).rejects.toThrow();
|
|
94
98
|
});
|
|
95
99
|
|
|
96
|
-
it('数量为 0 时抛出 ClawtError', () => {
|
|
100
|
+
it('数量为 0 时抛出 ClawtError', async () => {
|
|
97
101
|
const program = new Command();
|
|
98
102
|
program.exitOverride();
|
|
99
103
|
registerCreateCommand(program);
|
|
100
104
|
|
|
101
|
-
expect(
|
|
102
|
-
program.
|
|
103
|
-
|
|
105
|
+
await expect(
|
|
106
|
+
program.parseAsync(['create', '-b', 'feature', '-n', '0'], { from: 'user' }),
|
|
107
|
+
).rejects.toThrow();
|
|
104
108
|
});
|
|
105
109
|
|
|
106
|
-
it('负数数量抛出 ClawtError', () => {
|
|
110
|
+
it('负数数量抛出 ClawtError', async () => {
|
|
107
111
|
const program = new Command();
|
|
108
112
|
program.exitOverride();
|
|
109
113
|
registerCreateCommand(program);
|
|
110
114
|
|
|
111
|
-
expect(
|
|
112
|
-
program.
|
|
113
|
-
|
|
115
|
+
await expect(
|
|
116
|
+
program.parseAsync(['create', '-b', 'feature', '-n', '-1'], { from: 'user' }),
|
|
117
|
+
).rejects.toThrow();
|
|
114
118
|
});
|
|
115
119
|
});
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
INIT_SUCCESS: (branch: string) => `✓ 项目初始化成功,主工作分支设置为: ${branch}`,
|
|
11
|
+
INIT_UPDATED: (oldBranch: string, newBranch: string) => `✓ 已将主工作分支从 ${oldBranch} 更新为 ${newBranch}`,
|
|
12
|
+
INIT_SHOW: (configJson: string) => `当前项目配置:\n${configJson}`,
|
|
13
|
+
PROJECT_NOT_INITIALIZED: '项目尚未初始化,请先执行 clawt init 设置主工作分支',
|
|
14
|
+
PROJECT_CONFIG_MISSING_BRANCH: '项目配置缺少主工作分支信息,请重新执行 clawt init 设置主工作分支',
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('../../../src/utils/index.js', () => ({
|
|
19
|
+
validateMainWorktree: vi.fn(),
|
|
20
|
+
getCurrentBranch: vi.fn().mockReturnValue('main'),
|
|
21
|
+
loadProjectConfig: vi.fn(),
|
|
22
|
+
saveProjectConfig: vi.fn(),
|
|
23
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
24
|
+
printSuccess: vi.fn(),
|
|
25
|
+
printInfo: vi.fn(),
|
|
26
|
+
safeStringify: vi.fn((value: unknown, indent: number = 2) => JSON.stringify(value, null, indent)),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
import { registerInitCommand } from '../../../src/commands/init.js';
|
|
30
|
+
import {
|
|
31
|
+
loadProjectConfig,
|
|
32
|
+
saveProjectConfig,
|
|
33
|
+
requireProjectConfig,
|
|
34
|
+
printSuccess,
|
|
35
|
+
printInfo,
|
|
36
|
+
getCurrentBranch,
|
|
37
|
+
} from '../../../src/utils/index.js';
|
|
38
|
+
|
|
39
|
+
const mockedLoadProjectConfig = vi.mocked(loadProjectConfig);
|
|
40
|
+
const mockedSaveProjectConfig = vi.mocked(saveProjectConfig);
|
|
41
|
+
const mockedRequireProjectConfig = vi.mocked(requireProjectConfig);
|
|
42
|
+
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
43
|
+
const mockedPrintInfo = vi.mocked(printInfo);
|
|
44
|
+
const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
mockedLoadProjectConfig.mockReset();
|
|
48
|
+
mockedSaveProjectConfig.mockReset();
|
|
49
|
+
mockedRequireProjectConfig.mockReset();
|
|
50
|
+
mockedRequireProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'main' });
|
|
51
|
+
mockedPrintSuccess.mockReset();
|
|
52
|
+
mockedPrintInfo.mockReset();
|
|
53
|
+
mockedGetCurrentBranch.mockReturnValue('main');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('registerInitCommand', () => {
|
|
57
|
+
it('注册 init 命令', () => {
|
|
58
|
+
const program = new Command();
|
|
59
|
+
registerInitCommand(program);
|
|
60
|
+
const cmd = program.commands.find((c) => c.name() === 'init');
|
|
61
|
+
expect(cmd).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('注册 init show 子命令', () => {
|
|
65
|
+
const program = new Command();
|
|
66
|
+
registerInitCommand(program);
|
|
67
|
+
const initCmd = program.commands.find((c) => c.name() === 'init');
|
|
68
|
+
const showCmd = initCmd?.commands.find((c) => c.name() === 'show');
|
|
69
|
+
expect(showCmd).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('handleInit', () => {
|
|
74
|
+
it('无参数且已初始化时使用当前分支切换主工作分支', async () => {
|
|
75
|
+
mockedLoadProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'develop' });
|
|
76
|
+
|
|
77
|
+
const program = new Command();
|
|
78
|
+
program.exitOverride();
|
|
79
|
+
registerInitCommand(program);
|
|
80
|
+
await program.parseAsync(['init'], { from: 'user' });
|
|
81
|
+
|
|
82
|
+
expect(mockedSaveProjectConfig).toHaveBeenCalledWith({ clawtMainWorkBranch: 'main' });
|
|
83
|
+
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
84
|
+
expect.stringContaining('develop'),
|
|
85
|
+
);
|
|
86
|
+
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
87
|
+
expect.stringContaining('main'),
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('无参数且未初始化时使用当前分支初始化', async () => {
|
|
92
|
+
mockedLoadProjectConfig.mockReturnValue(null);
|
|
93
|
+
|
|
94
|
+
const program = new Command();
|
|
95
|
+
program.exitOverride();
|
|
96
|
+
registerInitCommand(program);
|
|
97
|
+
await program.parseAsync(['init'], { from: 'user' });
|
|
98
|
+
|
|
99
|
+
expect(mockedSaveProjectConfig).toHaveBeenCalledWith({ clawtMainWorkBranch: 'main' });
|
|
100
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('有 -b 参数时设置指定分支', async () => {
|
|
104
|
+
mockedLoadProjectConfig.mockReturnValue(null);
|
|
105
|
+
|
|
106
|
+
const program = new Command();
|
|
107
|
+
program.exitOverride();
|
|
108
|
+
registerInitCommand(program);
|
|
109
|
+
await program.parseAsync(['init', '-b', 'develop'], { from: 'user' });
|
|
110
|
+
|
|
111
|
+
expect(mockedSaveProjectConfig).toHaveBeenCalledWith({ clawtMainWorkBranch: 'develop' });
|
|
112
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('有 -b 参数且已初始化时更新配置', async () => {
|
|
116
|
+
mockedLoadProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'main' });
|
|
117
|
+
|
|
118
|
+
const program = new Command();
|
|
119
|
+
program.exitOverride();
|
|
120
|
+
registerInitCommand(program);
|
|
121
|
+
await program.parseAsync(['init', '-b', 'develop'], { from: 'user' });
|
|
122
|
+
|
|
123
|
+
expect(mockedSaveProjectConfig).toHaveBeenCalledWith({ clawtMainWorkBranch: 'develop' });
|
|
124
|
+
// 验证 INIT_UPDATED 传入了旧分支名和新分支名
|
|
125
|
+
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
126
|
+
expect.stringContaining('main'),
|
|
127
|
+
);
|
|
128
|
+
expect(mockedPrintSuccess).toHaveBeenCalledWith(
|
|
129
|
+
expect.stringContaining('develop'),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('handleInitShow (show 子命令)', () => {
|
|
135
|
+
it('clawt init show 展示当前配置', async () => {
|
|
136
|
+
mockedRequireProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'develop' });
|
|
137
|
+
|
|
138
|
+
const program = new Command();
|
|
139
|
+
program.exitOverride();
|
|
140
|
+
registerInitCommand(program);
|
|
141
|
+
await program.parseAsync(['init', 'show'], { from: 'user' });
|
|
142
|
+
|
|
143
|
+
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
144
|
+
expect(mockedSaveProjectConfig).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -64,7 +64,13 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
64
64
|
gitMergeBase: vi.fn(),
|
|
65
65
|
gitResetSoftTo: vi.fn(),
|
|
66
66
|
getCurrentBranch: vi.fn(),
|
|
67
|
+
gitResetHard: vi.fn(),
|
|
68
|
+
gitCleanForce: vi.fn(),
|
|
69
|
+
gitCheckout: vi.fn(),
|
|
67
70
|
resolveTargetWorktree: vi.fn(),
|
|
71
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
72
|
+
getMainWorkBranch: vi.fn().mockReturnValue('main'),
|
|
73
|
+
ensureOnMainWorkBranch: vi.fn(),
|
|
68
74
|
}));
|
|
69
75
|
|
|
70
76
|
import { registerMergeCommand } from '../../../src/commands/merge.js';
|
|
@@ -175,7 +181,7 @@ describe('handleMerge', () => {
|
|
|
175
181
|
|
|
176
182
|
it('目标 worktree 有未提交修改且提供 -m 时先提交再合并', async () => {
|
|
177
183
|
mockedIsWorkingDirClean
|
|
178
|
-
.mockReturnValueOnce(true) // 主 worktree
|
|
184
|
+
.mockReturnValueOnce(true) // 主 worktree 状态检测:干净
|
|
179
185
|
.mockReturnValueOnce(false); // 目标 worktree 不干净
|
|
180
186
|
mockedConfirmAction.mockResolvedValue(false); // 不清理
|
|
181
187
|
|
|
@@ -46,6 +46,10 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
46
46
|
removeSnapshot: vi.fn(),
|
|
47
47
|
removeProjectSnapshots: vi.fn(),
|
|
48
48
|
resolveTargetWorktrees: vi.fn(),
|
|
49
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
50
|
+
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
51
|
+
deleteValidateBranch: vi.fn(),
|
|
52
|
+
ensureOnMainWorkBranch: vi.fn(),
|
|
49
53
|
}));
|
|
50
54
|
|
|
51
55
|
import { registerRemoveCommand } from '../../../src/commands/remove.js';
|
|
@@ -23,6 +23,8 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
23
23
|
confirmDestructiveAction: vi.fn(),
|
|
24
24
|
printSuccess: vi.fn(),
|
|
25
25
|
printInfo: vi.fn(),
|
|
26
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
27
|
+
switchBackIfOnValidateBranch: vi.fn(),
|
|
26
28
|
}));
|
|
27
29
|
|
|
28
30
|
import { registerResetCommand } from '../../../src/commands/reset.js';
|
|
@@ -20,6 +20,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
20
20
|
getProjectWorktrees: vi.fn(),
|
|
21
21
|
launchInteractiveClaude: vi.fn(),
|
|
22
22
|
resolveTargetWorktrees: vi.fn(),
|
|
23
|
+
promptGroupedMultiSelectBranches: vi.fn(),
|
|
23
24
|
}));
|
|
24
25
|
|
|
25
26
|
import { registerResumeCommand } from '../../../src/commands/resume.js';
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
getProjectWorktrees,
|
|
30
31
|
launchInteractiveClaude,
|
|
31
32
|
resolveTargetWorktrees,
|
|
33
|
+
promptGroupedMultiSelectBranches,
|
|
32
34
|
} from '../../../src/utils/index.js';
|
|
33
35
|
|
|
34
36
|
const mockedValidateMainWorktree = vi.mocked(validateMainWorktree);
|
|
@@ -36,6 +38,7 @@ const mockedValidateClaudeCodeInstalled = vi.mocked(validateClaudeCodeInstalled)
|
|
|
36
38
|
const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
|
|
37
39
|
const mockedLaunchInteractiveClaude = vi.mocked(launchInteractiveClaude);
|
|
38
40
|
const mockedResolveTargetWorktrees = vi.mocked(resolveTargetWorktrees);
|
|
41
|
+
const mockedPromptGroupedMultiSelectBranches = vi.mocked(promptGroupedMultiSelectBranches);
|
|
39
42
|
|
|
40
43
|
beforeEach(() => {
|
|
41
44
|
mockedValidateMainWorktree.mockReset();
|
|
@@ -43,6 +46,7 @@ beforeEach(() => {
|
|
|
43
46
|
mockedGetProjectWorktrees.mockReset();
|
|
44
47
|
mockedLaunchInteractiveClaude.mockReset();
|
|
45
48
|
mockedResolveTargetWorktrees.mockReset();
|
|
49
|
+
mockedPromptGroupedMultiSelectBranches.mockReset();
|
|
46
50
|
});
|
|
47
51
|
|
|
48
52
|
describe('registerResumeCommand', () => {
|
|
@@ -55,7 +59,7 @@ describe('registerResumeCommand', () => {
|
|
|
55
59
|
});
|
|
56
60
|
|
|
57
61
|
describe('handleResume', () => {
|
|
58
|
-
it('
|
|
62
|
+
it('传 -b 时走标准解析流程', async () => {
|
|
59
63
|
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
60
64
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
61
65
|
mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
|
|
@@ -68,10 +72,31 @@ describe('handleResume', () => {
|
|
|
68
72
|
expect(mockedValidateMainWorktree).toHaveBeenCalled();
|
|
69
73
|
expect(mockedValidateClaudeCodeInstalled).toHaveBeenCalled();
|
|
70
74
|
expect(mockedResolveTargetWorktrees).toHaveBeenCalled();
|
|
75
|
+
expect(mockedPromptGroupedMultiSelectBranches).not.toHaveBeenCalled();
|
|
71
76
|
expect(mockedLaunchInteractiveClaude).toHaveBeenCalledWith(worktree, { autoContinue: true });
|
|
72
77
|
});
|
|
73
78
|
|
|
74
|
-
it('不传 -b
|
|
79
|
+
it('不传 -b 且多个 worktree 时默认使用分组多选', async () => {
|
|
80
|
+
const worktrees = [
|
|
81
|
+
{ path: '/path/feature-a', branch: 'feature-a' },
|
|
82
|
+
{ path: '/path/feature-b', branch: 'feature-b' },
|
|
83
|
+
];
|
|
84
|
+
mockedGetProjectWorktrees.mockReturnValue(worktrees);
|
|
85
|
+
mockedPromptGroupedMultiSelectBranches.mockResolvedValue([worktrees[0]]);
|
|
86
|
+
|
|
87
|
+
const program = new Command();
|
|
88
|
+
program.exitOverride();
|
|
89
|
+
registerResumeCommand(program);
|
|
90
|
+
await program.parseAsync(['resume'], { from: 'user' });
|
|
91
|
+
|
|
92
|
+
expect(mockedPromptGroupedMultiSelectBranches).toHaveBeenCalledWith(
|
|
93
|
+
worktrees,
|
|
94
|
+
expect.any(String),
|
|
95
|
+
);
|
|
96
|
+
expect(mockedResolveTargetWorktrees).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('仅 1 个 worktree 时走标准流程', async () => {
|
|
75
100
|
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
76
101
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
77
102
|
mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
|
|
@@ -81,11 +106,7 @@ describe('handleResume', () => {
|
|
|
81
106
|
registerResumeCommand(program);
|
|
82
107
|
await program.parseAsync(['resume'], { from: 'user' });
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
expect(
|
|
86
|
-
expect.any(Array),
|
|
87
|
-
expect.any(Object),
|
|
88
|
-
undefined,
|
|
89
|
-
);
|
|
109
|
+
expect(mockedResolveTargetWorktrees).toHaveBeenCalled();
|
|
110
|
+
expect(mockedPromptGroupedMultiSelectBranches).not.toHaveBeenCalled();
|
|
90
111
|
});
|
|
91
112
|
});
|
|
@@ -71,6 +71,8 @@ vi.mock('../../../src/utils/index.js', async (importOriginal) => {
|
|
|
71
71
|
parseTasksFromOptions: vi.fn(),
|
|
72
72
|
createWorktreesByBranches: vi.fn(),
|
|
73
73
|
printDryRunPreview: vi.fn(),
|
|
74
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
75
|
+
ensureOnMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
74
76
|
};
|
|
75
77
|
});
|
|
76
78
|
|
|
@@ -25,6 +25,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
25
25
|
SYNC_MERGING: (branch: string, mainBranch: string) => `正在将 ${mainBranch} 合并到 ${branch}...`,
|
|
26
26
|
SYNC_CONFLICT: (path: string) => `合并冲突,请手动解决: ${path}`,
|
|
27
27
|
SYNC_SUCCESS: (branch: string, mainBranch: string) => `✓ 已将 ${mainBranch} 同步到 ${branch}`,
|
|
28
|
+
SYNC_VALIDATE_BRANCH_REBUILT: (validateBranch: string) => `验证分支 ${validateBranch} 已重建`,
|
|
28
29
|
},
|
|
29
30
|
AUTO_SAVE_COMMIT_MESSAGE: 'clawt:auto-save',
|
|
30
31
|
}));
|
|
@@ -46,6 +47,11 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
46
47
|
printInfo: vi.fn(),
|
|
47
48
|
printWarning: vi.fn(),
|
|
48
49
|
resolveTargetWorktree: vi.fn(),
|
|
50
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
51
|
+
getMainWorkBranch: vi.fn().mockReturnValue('main'),
|
|
52
|
+
rebuildValidateBranch: vi.fn(),
|
|
53
|
+
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
54
|
+
ensureOnMainWorkBranch: vi.fn(),
|
|
49
55
|
}));
|
|
50
56
|
|
|
51
57
|
import { registerSyncCommand } from '../../../src/commands/sync.js';
|
|
@@ -40,6 +40,11 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
40
40
|
VALIDATE_PARALLEL_CMD_FAILED: (cmd: string, code: number) => ` ✗ ${cmd}(退出码: ${code})`,
|
|
41
41
|
VALIDATE_PARALLEL_CMD_ERROR: (cmd: string, msg: string) => ` ✗ ${cmd}(错误: ${msg})`,
|
|
42
42
|
SEPARATOR: '────',
|
|
43
|
+
VALIDATE_BRANCH_NOT_FOUND: (validateBranch: string, branch: string) => `验证分支 ${validateBranch} 不存在`,
|
|
44
|
+
VALIDATE_SUCCESS_WITH_BRANCH: (branch: string, validateBranch: string) => `✓ 已切换到验证分支 ${validateBranch} 并验证 ${branch}`,
|
|
45
|
+
VALIDATE_CONFIRM_AUTO_SYNC: (branch: string) => `是否自动 sync ${branch}`,
|
|
46
|
+
VALIDATE_AUTO_SYNC_DECLINED: (branch: string) => `已跳过 ${branch} 的自动 sync`,
|
|
47
|
+
VALIDATE_AUTO_SYNC_START: (branch: string) => `正在自动 sync ${branch}`,
|
|
43
48
|
},
|
|
44
49
|
}));
|
|
45
50
|
|
|
@@ -79,6 +84,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
79
84
|
writeSnapshot: vi.fn(),
|
|
80
85
|
removeSnapshot: vi.fn(),
|
|
81
86
|
confirmDestructiveAction: vi.fn(),
|
|
87
|
+
confirmAction: vi.fn(),
|
|
82
88
|
printSuccess: vi.fn(),
|
|
83
89
|
printWarning: vi.fn(),
|
|
84
90
|
printInfo: vi.fn(),
|
|
@@ -88,6 +94,13 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
88
94
|
printSeparator: vi.fn(),
|
|
89
95
|
parseParallelCommands: vi.fn(),
|
|
90
96
|
runParallelCommands: vi.fn(),
|
|
97
|
+
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
98
|
+
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
99
|
+
gitCheckout: vi.fn(),
|
|
100
|
+
ensureOnMainWorkBranch: vi.fn(),
|
|
101
|
+
handleDirtyWorkingDir: vi.fn(),
|
|
102
|
+
checkBranchExists: vi.fn().mockReturnValue(true),
|
|
103
|
+
getCurrentBranch: vi.fn().mockReturnValue('main'),
|
|
91
104
|
}));
|
|
92
105
|
|
|
93
106
|
import { registerValidateCommand } from '../../../src/commands/validate.js';
|
|
@@ -90,9 +90,9 @@ describe('writeConfig', () => {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
describe('ensureClawtDirs', () => {
|
|
93
|
-
it('
|
|
93
|
+
it('确保四个全局目录存在', () => {
|
|
94
94
|
ensureClawtDirs();
|
|
95
|
-
expect(mockedEnsureDir).toHaveBeenCalledTimes(
|
|
95
|
+
expect(mockedEnsureDir).toHaveBeenCalledTimes(4);
|
|
96
96
|
});
|
|
97
97
|
});
|
|
98
98
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock node:fs
|
|
4
|
+
vi.mock('node:fs', () => ({
|
|
5
|
+
existsSync: vi.fn(),
|
|
6
|
+
readFileSync: vi.fn(),
|
|
7
|
+
writeFileSync: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// mock logger
|
|
11
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
12
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// mock errors
|
|
16
|
+
vi.mock('../../../src/errors/index.js', () => ({
|
|
17
|
+
ClawtError: class ClawtError extends Error {
|
|
18
|
+
exitCode: number;
|
|
19
|
+
constructor(message: string, exitCode = 1) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.exitCode = exitCode;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// mock constants
|
|
27
|
+
vi.mock('../../../src/constants/index.js', () => ({
|
|
28
|
+
PROJECTS_CONFIG_DIR: '/mock/.clawt/projects',
|
|
29
|
+
MESSAGES: {
|
|
30
|
+
PROJECT_NOT_INITIALIZED: '项目尚未初始化,请先执行 clawt init 设置主工作分支',
|
|
31
|
+
PROJECT_CONFIG_MISSING_BRANCH: '项目配置缺少主工作分支信息,请重新执行 clawt init 设置主工作分支',
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// mock git
|
|
36
|
+
vi.mock('../../../src/utils/git.js', () => ({
|
|
37
|
+
getProjectName: vi.fn().mockReturnValue('test-project'),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// mock fs utils
|
|
41
|
+
vi.mock('../../../src/utils/fs.js', () => ({
|
|
42
|
+
ensureDir: vi.fn(),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
46
|
+
import {
|
|
47
|
+
getProjectConfigPath,
|
|
48
|
+
loadProjectConfig,
|
|
49
|
+
saveProjectConfig,
|
|
50
|
+
requireProjectConfig,
|
|
51
|
+
getMainWorkBranch,
|
|
52
|
+
} from '../../../src/utils/project-config.js';
|
|
53
|
+
|
|
54
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
55
|
+
const mockedReadFileSync = vi.mocked(readFileSync);
|
|
56
|
+
const mockedWriteFileSync = vi.mocked(writeFileSync);
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
mockedExistsSync.mockReset();
|
|
60
|
+
mockedReadFileSync.mockReset();
|
|
61
|
+
mockedWriteFileSync.mockReset();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('getProjectConfigPath', () => {
|
|
65
|
+
it('返回正确的配置文件路径', () => {
|
|
66
|
+
const path = getProjectConfigPath('my-project');
|
|
67
|
+
// 路径格式:<PROJECTS_CONFIG_DIR>/<projectName>/config.json
|
|
68
|
+
expect(path).toContain('my-project');
|
|
69
|
+
expect(path).toContain('config.json');
|
|
70
|
+
expect(path).toContain('projects');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('loadProjectConfig', () => {
|
|
75
|
+
it('配置文件不存在时返回 null', () => {
|
|
76
|
+
mockedExistsSync.mockReturnValue(false);
|
|
77
|
+
expect(loadProjectConfig()).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('配置文件存在时正确解析', () => {
|
|
81
|
+
mockedExistsSync.mockReturnValue(true);
|
|
82
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({ clawtMainWorkBranch: 'develop' }));
|
|
83
|
+
const config = loadProjectConfig();
|
|
84
|
+
expect(config).toEqual({ clawtMainWorkBranch: 'develop' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('配置文件损坏时返回 null', () => {
|
|
88
|
+
mockedExistsSync.mockReturnValue(true);
|
|
89
|
+
mockedReadFileSync.mockReturnValue('invalid json');
|
|
90
|
+
expect(loadProjectConfig()).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('saveProjectConfig', () => {
|
|
95
|
+
it('将配置写入文件', () => {
|
|
96
|
+
saveProjectConfig({ clawtMainWorkBranch: 'main' });
|
|
97
|
+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
|
|
98
|
+
expect.stringContaining('config.json'),
|
|
99
|
+
JSON.stringify({ clawtMainWorkBranch: 'main' }, null, 2),
|
|
100
|
+
'utf-8',
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('requireProjectConfig', () => {
|
|
106
|
+
it('配置存在且包含 clawtMainWorkBranch 时返回配置', () => {
|
|
107
|
+
mockedExistsSync.mockReturnValue(true);
|
|
108
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({ clawtMainWorkBranch: 'main' }));
|
|
109
|
+
expect(requireProjectConfig()).toEqual({ clawtMainWorkBranch: 'main' });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('配置不存在时抛出错误', () => {
|
|
113
|
+
mockedExistsSync.mockReturnValue(false);
|
|
114
|
+
expect(() => requireProjectConfig()).toThrow();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('配置存在但缺少 clawtMainWorkBranch 字段时抛出错误', () => {
|
|
118
|
+
mockedExistsSync.mockReturnValue(true);
|
|
119
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({}));
|
|
120
|
+
expect(() => requireProjectConfig()).toThrow('项目配置缺少主工作分支信息');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('配置存在但 clawtMainWorkBranch 为空字符串时抛出错误', () => {
|
|
124
|
+
mockedExistsSync.mockReturnValue(true);
|
|
125
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({ clawtMainWorkBranch: '' }));
|
|
126
|
+
expect(() => requireProjectConfig()).toThrow('项目配置缺少主工作分支信息');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('getMainWorkBranch', () => {
|
|
131
|
+
it('返回主工作分支名', () => {
|
|
132
|
+
mockedExistsSync.mockReturnValue(true);
|
|
133
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({ clawtMainWorkBranch: 'develop' }));
|
|
134
|
+
expect(getMainWorkBranch()).toBe('develop');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -19,13 +19,34 @@ vi.mock('node:https', () => ({
|
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
21
|
// mock chalk(测试环境已通过 FORCE_COLOR=0 禁用颜色,但仍需确保不产生转义码)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
// 使用 Proxy 实现链式调用,支持 chalk.bold.hex('#color')('text') 等任意嵌套
|
|
23
|
+
vi.mock('chalk', () => {
|
|
24
|
+
/**
|
|
25
|
+
* 创建可链式调用的 chalk Proxy
|
|
26
|
+
* - 属性访问(如 .bold、.red)返回新 Proxy
|
|
27
|
+
* - 函数调用返回新 Proxy,但记录最后传入的字符串参数
|
|
28
|
+
* - toString / Symbol.toPrimitive 返回最后记录的字符串,支持模板字符串插值
|
|
29
|
+
* @param {string} [value] - 内部记录的字符串值
|
|
30
|
+
* @returns {unknown} 链式 Proxy 对象
|
|
31
|
+
*/
|
|
32
|
+
const createChainProxy = (value = ''): unknown => {
|
|
33
|
+
const fn = (..._args: unknown[]) => {};
|
|
34
|
+
return new Proxy(fn, {
|
|
35
|
+
get: (_target, prop) => {
|
|
36
|
+
if (prop === '__esModule') return true;
|
|
37
|
+
if (prop === Symbol.toPrimitive || prop === 'toString') return () => value;
|
|
38
|
+
if (prop === 'length') return 0;
|
|
39
|
+
return createChainProxy(value);
|
|
40
|
+
},
|
|
41
|
+
apply: (_target, _thisArg, args) => {
|
|
42
|
+
// 记录最后传入的字符串参数作为输出值
|
|
43
|
+
const newValue = typeof args[0] === 'string' ? args[0] : value;
|
|
44
|
+
return createChainProxy(newValue);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
return { default: createChainProxy(), __esModule: true };
|
|
49
|
+
});
|
|
29
50
|
|
|
30
51
|
// mock string-width(纯 ASCII 场景下直接返回字符串长度即可)
|
|
31
52
|
vi.mock('string-width', () => ({
|