clawt 2.9.0 → 2.9.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/package.json +1 -1
- package/tests/unit/constants/branch.test.ts +58 -0
- package/tests/unit/constants/exitCodes.test.ts +27 -0
- package/tests/unit/constants/git.test.ts +17 -0
- package/tests/unit/constants/logger.test.ts +25 -0
- package/tests/unit/constants/messages.test.ts +240 -0
- package/tests/unit/constants/paths.test.ts +51 -0
- package/tests/unit/constants/terminal.test.ts +41 -0
- package/tests/unit/utils/claude.test.ts +172 -0
- package/tests/unit/utils/prompt.test.ts +53 -0
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { INVALID_BRANCH_CHARS } from '../../../src/constants/branch.js';
|
|
3
|
+
|
|
4
|
+
describe('INVALID_BRANCH_CHARS', () => {
|
|
5
|
+
it('是一个全局正则表达式', () => {
|
|
6
|
+
expect(INVALID_BRANCH_CHARS).toBeInstanceOf(RegExp);
|
|
7
|
+
expect(INVALID_BRANCH_CHARS.global).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('匹配斜杠 /', () => {
|
|
11
|
+
expect('feature/test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('匹配反斜杠 \\', () => {
|
|
15
|
+
expect('feature\\test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('匹配点号 .', () => {
|
|
19
|
+
expect('feature.test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('匹配空格', () => {
|
|
23
|
+
expect('feature test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('匹配波浪号 ~', () => {
|
|
27
|
+
expect('feature~test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('匹配冒号 :', () => {
|
|
31
|
+
expect('feature:test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('匹配星号 *', () => {
|
|
35
|
+
expect('feature*test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('匹配问号 ?', () => {
|
|
39
|
+
expect('feature?test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('匹配方括号 []', () => {
|
|
43
|
+
expect('feature[test]'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test-');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('匹配 ^', () => {
|
|
47
|
+
expect('feature^test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('连续非法字符合并为一次匹配', () => {
|
|
51
|
+
expect('feature...test'.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe('feature-test');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('不匹配合法字符(字母、数字、-)', () => {
|
|
55
|
+
const legal = 'feature-add-login-123';
|
|
56
|
+
expect(legal.replace(new RegExp(INVALID_BRANCH_CHARS.source, 'g'), '-')).toBe(legal);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { EXIT_CODES } from '../../../src/constants/exitCodes.js';
|
|
3
|
+
|
|
4
|
+
describe('EXIT_CODES', () => {
|
|
5
|
+
it('SUCCESS 为 0', () => {
|
|
6
|
+
expect(EXIT_CODES.SUCCESS).toBe(0);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('ERROR 为 1', () => {
|
|
10
|
+
expect(EXIT_CODES.ERROR).toBe(1);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('ARGUMENT_ERROR 为 2', () => {
|
|
14
|
+
expect(EXIT_CODES.ARGUMENT_ERROR).toBe(2);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('包含且仅包含三个退出码', () => {
|
|
18
|
+
expect(Object.keys(EXIT_CODES)).toHaveLength(3);
|
|
19
|
+
expect(Object.keys(EXIT_CODES).sort()).toEqual(['ARGUMENT_ERROR', 'ERROR', 'SUCCESS']);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('所有值都是数字', () => {
|
|
23
|
+
for (const value of Object.values(EXIT_CODES)) {
|
|
24
|
+
expect(typeof value).toBe('number');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { AUTO_SAVE_COMMIT_MESSAGE } from '../../../src/constants/git.js';
|
|
3
|
+
|
|
4
|
+
describe('AUTO_SAVE_COMMIT_MESSAGE', () => {
|
|
5
|
+
it('值为预期的 commit message', () => {
|
|
6
|
+
expect(AUTO_SAVE_COMMIT_MESSAGE).toBe('chore: auto-save before sync');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('是非空字符串', () => {
|
|
10
|
+
expect(typeof AUTO_SAVE_COMMIT_MESSAGE).toBe('string');
|
|
11
|
+
expect(AUTO_SAVE_COMMIT_MESSAGE.length).toBeGreaterThan(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('符合 conventional commit 格式', () => {
|
|
15
|
+
expect(AUTO_SAVE_COMMIT_MESSAGE).toMatch(/^[a-z]+:\s.+$/);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DEBUG_LOG_PREFIX, DEBUG_TIMESTAMP_FORMAT } from '../../../src/constants/logger.js';
|
|
3
|
+
|
|
4
|
+
describe('DEBUG_LOG_PREFIX', () => {
|
|
5
|
+
it('值为 [DEBUG]', () => {
|
|
6
|
+
expect(DEBUG_LOG_PREFIX).toBe('[DEBUG]');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('包含方括号包裹的标识', () => {
|
|
10
|
+
expect(DEBUG_LOG_PREFIX).toMatch(/^\[.+\]$/);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('DEBUG_TIMESTAMP_FORMAT', () => {
|
|
15
|
+
it('值为 HH:mm:ss.SSS', () => {
|
|
16
|
+
expect(DEBUG_TIMESTAMP_FORMAT).toBe('HH:mm:ss.SSS');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('包含时分秒和毫秒', () => {
|
|
20
|
+
expect(DEBUG_TIMESTAMP_FORMAT).toContain('HH');
|
|
21
|
+
expect(DEBUG_TIMESTAMP_FORMAT).toContain('mm');
|
|
22
|
+
expect(DEBUG_TIMESTAMP_FORMAT).toContain('ss');
|
|
23
|
+
expect(DEBUG_TIMESTAMP_FORMAT).toContain('SSS');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { MESSAGES } from '../../../src/constants/messages.js';
|
|
3
|
+
|
|
4
|
+
describe('MESSAGES', () => {
|
|
5
|
+
describe('纯字符串消息', () => {
|
|
6
|
+
const stringKeys = [
|
|
7
|
+
'NOT_MAIN_WORKTREE',
|
|
8
|
+
'GIT_NOT_INSTALLED',
|
|
9
|
+
'CLAUDE_NOT_INSTALLED',
|
|
10
|
+
'NO_WORKTREES',
|
|
11
|
+
'MAIN_WORKTREE_DIRTY',
|
|
12
|
+
'TARGET_WORKTREE_CLEAN',
|
|
13
|
+
'MERGE_CONFLICT',
|
|
14
|
+
'COMMIT_MESSAGE_REQUIRED',
|
|
15
|
+
'TARGET_WORKTREE_DIRTY_NO_MESSAGE',
|
|
16
|
+
'TARGET_WORKTREE_NO_CHANGES',
|
|
17
|
+
'INTERRUPTED',
|
|
18
|
+
'INTERRUPT_CONFIRM_CLEANUP',
|
|
19
|
+
'INTERRUPT_KEPT',
|
|
20
|
+
'CONFIG_CORRUPTED',
|
|
21
|
+
'CONFIG_RESET_SUCCESS',
|
|
22
|
+
'SEPARATOR',
|
|
23
|
+
'DOUBLE_SEPARATOR',
|
|
24
|
+
'WORKTREE_STATUS_UNAVAILABLE',
|
|
25
|
+
'INCREMENTAL_VALIDATE_FALLBACK',
|
|
26
|
+
'MERGE_SQUASH_PROMPT',
|
|
27
|
+
'DESTRUCTIVE_OP_CANCELLED',
|
|
28
|
+
'RESET_SUCCESS',
|
|
29
|
+
'RESET_ALREADY_CLEAN',
|
|
30
|
+
'RESUME_NO_WORKTREES',
|
|
31
|
+
'RESUME_SELECT_BRANCH',
|
|
32
|
+
'VALIDATE_NO_WORKTREES',
|
|
33
|
+
'VALIDATE_SELECT_BRANCH',
|
|
34
|
+
'MERGE_NO_WORKTREES',
|
|
35
|
+
'MERGE_SELECT_BRANCH',
|
|
36
|
+
'SYNC_NO_WORKTREES',
|
|
37
|
+
'SYNC_SELECT_BRANCH',
|
|
38
|
+
'PULL_CONFLICT',
|
|
39
|
+
'PUSH_FAILED',
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
it.each(stringKeys)('%s 是非空字符串', (key) => {
|
|
43
|
+
const value = MESSAGES[key];
|
|
44
|
+
expect(typeof value).toBe('string');
|
|
45
|
+
expect((value as string).length).toBeGreaterThan(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('模板函数消息', () => {
|
|
50
|
+
it('BRANCH_EXISTS 包含分支名', () => {
|
|
51
|
+
const result = MESSAGES.BRANCH_EXISTS('feature-a');
|
|
52
|
+
expect(result).toContain('feature-a');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('BRANCH_EXISTS_USE_RESUME 包含分支名和 resume 提示', () => {
|
|
56
|
+
const result = MESSAGES.BRANCH_EXISTS_USE_RESUME('feature-a');
|
|
57
|
+
expect(result).toContain('feature-a');
|
|
58
|
+
expect(result).toContain('resume');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('BRANCH_NAME_EMPTY 包含原始分支名', () => {
|
|
62
|
+
const result = MESSAGES.BRANCH_NAME_EMPTY('...');
|
|
63
|
+
expect(result).toContain('...');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('BRANCH_SANITIZED 包含原始名和转换后的名称', () => {
|
|
67
|
+
const result = MESSAGES.BRANCH_SANITIZED('feat/test', 'feat-test');
|
|
68
|
+
expect(result).toContain('feat/test');
|
|
69
|
+
expect(result).toContain('feat-test');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('WORKTREE_CREATED 包含数量', () => {
|
|
73
|
+
const result = MESSAGES.WORKTREE_CREATED(3);
|
|
74
|
+
expect(result).toContain('3');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('WORKTREE_REMOVED 包含路径', () => {
|
|
78
|
+
const result = MESSAGES.WORKTREE_REMOVED('/path/to/wt');
|
|
79
|
+
expect(result).toContain('/path/to/wt');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('WORKTREE_NOT_FOUND 包含名称', () => {
|
|
83
|
+
const result = MESSAGES.WORKTREE_NOT_FOUND('test-branch');
|
|
84
|
+
expect(result).toContain('test-branch');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('VALIDATE_SUCCESS 包含分支名', () => {
|
|
88
|
+
const result = MESSAGES.VALIDATE_SUCCESS('feature-x');
|
|
89
|
+
expect(result).toContain('feature-x');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('MERGE_SUCCESS 包含分支名和提交信息', () => {
|
|
93
|
+
const result = MESSAGES.MERGE_SUCCESS('feat-a', 'fix bug', true);
|
|
94
|
+
expect(result).toContain('feat-a');
|
|
95
|
+
expect(result).toContain('fix bug');
|
|
96
|
+
expect(result).toContain('已推送');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('MERGE_SUCCESS 未推送时不包含推送提示', () => {
|
|
100
|
+
const result = MESSAGES.MERGE_SUCCESS('feat-a', 'fix bug', false);
|
|
101
|
+
expect(result).not.toContain('已推送');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('MERGE_SUCCESS_NO_MESSAGE 包含分支名', () => {
|
|
105
|
+
const result = MESSAGES.MERGE_SUCCESS_NO_MESSAGE('feat-a', false);
|
|
106
|
+
expect(result).toContain('feat-a');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('WORKTREE_CLEANED 包含分支名', () => {
|
|
110
|
+
const result = MESSAGES.WORKTREE_CLEANED('feature-clean');
|
|
111
|
+
expect(result).toContain('feature-clean');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('INTERRUPT_AUTO_CLEANED 包含数量', () => {
|
|
115
|
+
const result = MESSAGES.INTERRUPT_AUTO_CLEANED(2);
|
|
116
|
+
expect(result).toContain('2');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('INTERRUPT_CLEANED 包含数量', () => {
|
|
120
|
+
const result = MESSAGES.INTERRUPT_CLEANED(5);
|
|
121
|
+
expect(result).toContain('5');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('INVALID_COUNT 包含无效值', () => {
|
|
125
|
+
const result = MESSAGES.INVALID_COUNT('abc');
|
|
126
|
+
expect(result).toContain('abc');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('INCREMENTAL_VALIDATE_SUCCESS 包含分支名', () => {
|
|
130
|
+
const result = MESSAGES.INCREMENTAL_VALIDATE_SUCCESS('dev');
|
|
131
|
+
expect(result).toContain('dev');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('VALIDATE_CLEANED 包含分支名', () => {
|
|
135
|
+
const result = MESSAGES.VALIDATE_CLEANED('test');
|
|
136
|
+
expect(result).toContain('test');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('MERGE_VALIDATE_STATE_HINT 包含分支名', () => {
|
|
140
|
+
const result = MESSAGES.MERGE_VALIDATE_STATE_HINT('feat');
|
|
141
|
+
expect(result).toContain('feat');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('SYNC_AUTO_COMMITTED 包含分支名', () => {
|
|
145
|
+
const result = MESSAGES.SYNC_AUTO_COMMITTED('sync-branch');
|
|
146
|
+
expect(result).toContain('sync-branch');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('SYNC_MERGING 包含目标分支和主分支', () => {
|
|
150
|
+
const result = MESSAGES.SYNC_MERGING('feature', 'main');
|
|
151
|
+
expect(result).toContain('feature');
|
|
152
|
+
expect(result).toContain('main');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('SYNC_SUCCESS 包含目标分支和主分支', () => {
|
|
156
|
+
const result = MESSAGES.SYNC_SUCCESS('feature', 'main');
|
|
157
|
+
expect(result).toContain('feature');
|
|
158
|
+
expect(result).toContain('main');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('SYNC_CONFLICT 包含 worktree 路径', () => {
|
|
162
|
+
const result = MESSAGES.SYNC_CONFLICT('/path/to/wt');
|
|
163
|
+
expect(result).toContain('/path/to/wt');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('VALIDATE_PATCH_APPLY_FAILED 包含分支名', () => {
|
|
167
|
+
const result = MESSAGES.VALIDATE_PATCH_APPLY_FAILED('feat');
|
|
168
|
+
expect(result).toContain('feat');
|
|
169
|
+
expect(result).toContain('sync');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('MERGE_SQUASH_COMMITTED 包含分支名', () => {
|
|
173
|
+
const result = MESSAGES.MERGE_SQUASH_COMMITTED('feat');
|
|
174
|
+
expect(result).toContain('feat');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('MERGE_SQUASH_PENDING 包含路径和分支名', () => {
|
|
178
|
+
const result = MESSAGES.MERGE_SQUASH_PENDING('/path/wt', 'feat');
|
|
179
|
+
expect(result).toContain('/path/wt');
|
|
180
|
+
expect(result).toContain('feat');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('REMOVE_PARTIAL_FAILURE 包含失败信息', () => {
|
|
184
|
+
const failures = [
|
|
185
|
+
{ path: '/path/a', error: '权限不足' },
|
|
186
|
+
{ path: '/path/b', error: '不存在' },
|
|
187
|
+
];
|
|
188
|
+
const result = MESSAGES.REMOVE_PARTIAL_FAILURE(failures);
|
|
189
|
+
expect(result).toContain('/path/a');
|
|
190
|
+
expect(result).toContain('权限不足');
|
|
191
|
+
expect(result).toContain('/path/b');
|
|
192
|
+
expect(result).toContain('不存在');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('RESUME_NO_MATCH 包含名称和分支列表', () => {
|
|
196
|
+
const result = MESSAGES.RESUME_NO_MATCH('test', ['branch-1', 'branch-2']);
|
|
197
|
+
expect(result).toContain('test');
|
|
198
|
+
expect(result).toContain('branch-1');
|
|
199
|
+
expect(result).toContain('branch-2');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('RESUME_MULTIPLE_MATCHES 包含名称', () => {
|
|
203
|
+
const result = MESSAGES.RESUME_MULTIPLE_MATCHES('feat');
|
|
204
|
+
expect(result).toContain('feat');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('VALIDATE_NO_MATCH 包含名称和分支列表', () => {
|
|
208
|
+
const result = MESSAGES.VALIDATE_NO_MATCH('test', ['b1']);
|
|
209
|
+
expect(result).toContain('test');
|
|
210
|
+
expect(result).toContain('b1');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('VALIDATE_MULTIPLE_MATCHES 包含名称', () => {
|
|
214
|
+
const result = MESSAGES.VALIDATE_MULTIPLE_MATCHES('feat');
|
|
215
|
+
expect(result).toContain('feat');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('MERGE_NO_MATCH 包含名称和分支列表', () => {
|
|
219
|
+
const result = MESSAGES.MERGE_NO_MATCH('test', ['b1']);
|
|
220
|
+
expect(result).toContain('test');
|
|
221
|
+
expect(result).toContain('b1');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('MERGE_MULTIPLE_MATCHES 包含名称', () => {
|
|
225
|
+
const result = MESSAGES.MERGE_MULTIPLE_MATCHES('feat');
|
|
226
|
+
expect(result).toContain('feat');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('SYNC_NO_MATCH 包含名称和分支列表', () => {
|
|
230
|
+
const result = MESSAGES.SYNC_NO_MATCH('test', ['b1']);
|
|
231
|
+
expect(result).toContain('test');
|
|
232
|
+
expect(result).toContain('b1');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('SYNC_MULTIPLE_MATCHES 包含名称', () => {
|
|
236
|
+
const result = MESSAGES.SYNC_MULTIPLE_MATCHES('feat');
|
|
237
|
+
expect(result).toContain('feat');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR } from '../../../src/constants/paths.js';
|
|
5
|
+
|
|
6
|
+
describe('CLAWT_HOME', () => {
|
|
7
|
+
it('位于用户主目录下的 .clawt', () => {
|
|
8
|
+
expect(CLAWT_HOME).toBe(join(homedir(), '.clawt'));
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('包含 .clawt', () => {
|
|
12
|
+
expect(CLAWT_HOME).toContain('.clawt');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('CONFIG_PATH', () => {
|
|
17
|
+
it('位于 CLAWT_HOME 下的 config.json', () => {
|
|
18
|
+
expect(CONFIG_PATH).toBe(join(CLAWT_HOME, 'config.json'));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('以 .json 结尾', () => {
|
|
22
|
+
expect(CONFIG_PATH).toMatch(/\.json$/);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('LOGS_DIR', () => {
|
|
27
|
+
it('位于 CLAWT_HOME 下的 logs', () => {
|
|
28
|
+
expect(LOGS_DIR).toBe(join(CLAWT_HOME, 'logs'));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('WORKTREES_DIR', () => {
|
|
33
|
+
it('位于 CLAWT_HOME 下的 worktrees', () => {
|
|
34
|
+
expect(WORKTREES_DIR).toBe(join(CLAWT_HOME, 'worktrees'));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('VALIDATE_SNAPSHOTS_DIR', () => {
|
|
39
|
+
it('位于 CLAWT_HOME 下的 validate-snapshots', () => {
|
|
40
|
+
expect(VALIDATE_SNAPSHOTS_DIR).toBe(join(CLAWT_HOME, 'validate-snapshots'));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('路径层级关系', () => {
|
|
45
|
+
it('CONFIG_PATH、LOGS_DIR、WORKTREES_DIR、VALIDATE_SNAPSHOTS_DIR 都在 CLAWT_HOME 下', () => {
|
|
46
|
+
expect(CONFIG_PATH.startsWith(CLAWT_HOME)).toBe(true);
|
|
47
|
+
expect(LOGS_DIR.startsWith(CLAWT_HOME)).toBe(true);
|
|
48
|
+
expect(WORKTREES_DIR.startsWith(CLAWT_HOME)).toBe(true);
|
|
49
|
+
expect(VALIDATE_SNAPSHOTS_DIR.startsWith(CLAWT_HOME)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ENABLE_BRACKETED_PASTE, DISABLE_BRACKETED_PASTE, PASTE_THRESHOLD_MS } from '../../../src/constants/terminal.js';
|
|
3
|
+
|
|
4
|
+
describe('ENABLE_BRACKETED_PASTE', () => {
|
|
5
|
+
it('值为启用 Bracketed Paste Mode 的 ANSI 转义序列', () => {
|
|
6
|
+
expect(ENABLE_BRACKETED_PASTE).toBe('\x1b[?2004h');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('以 ESC[ 开头', () => {
|
|
10
|
+
expect(ENABLE_BRACKETED_PASTE.startsWith('\x1b[')).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('DISABLE_BRACKETED_PASTE', () => {
|
|
15
|
+
it('值为禁用 Bracketed Paste Mode 的 ANSI 转义序列', () => {
|
|
16
|
+
expect(DISABLE_BRACKETED_PASTE).toBe('\x1b[?2004l');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('以 ESC[ 开头', () => {
|
|
20
|
+
expect(DISABLE_BRACKETED_PASTE.startsWith('\x1b[')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('ENABLE_BRACKETED_PASTE 与 DISABLE_BRACKETED_PASTE', () => {
|
|
25
|
+
it('仅末尾字符不同(h vs l)', () => {
|
|
26
|
+
expect(ENABLE_BRACKETED_PASTE.slice(0, -1)).toBe(DISABLE_BRACKETED_PASTE.slice(0, -1));
|
|
27
|
+
expect(ENABLE_BRACKETED_PASTE.at(-1)).toBe('h');
|
|
28
|
+
expect(DISABLE_BRACKETED_PASTE.at(-1)).toBe('l');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('PASTE_THRESHOLD_MS', () => {
|
|
33
|
+
it('值为 10', () => {
|
|
34
|
+
expect(PASTE_THRESHOLD_MS).toBe(10);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('是正整数', () => {
|
|
38
|
+
expect(Number.isInteger(PASTE_THRESHOLD_MS)).toBe(true);
|
|
39
|
+
expect(PASTE_THRESHOLD_MS).toBeGreaterThan(0);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock logger(避免测试时写日志文件)
|
|
4
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
// mock node:child_process
|
|
9
|
+
vi.mock('node:child_process', () => ({
|
|
10
|
+
spawnSync: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// mock config
|
|
14
|
+
vi.mock('../../../src/utils/config.js', () => ({
|
|
15
|
+
getConfigValue: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// mock formatter
|
|
19
|
+
vi.mock('../../../src/utils/formatter.js', () => ({
|
|
20
|
+
printInfo: vi.fn(),
|
|
21
|
+
printWarning: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { launchInteractiveClaude } from '../../../src/utils/claude.js';
|
|
26
|
+
import { getConfigValue } from '../../../src/utils/config.js';
|
|
27
|
+
import { printInfo, printWarning } from '../../../src/utils/formatter.js';
|
|
28
|
+
import { ClawtError } from '../../../src/errors/index.js';
|
|
29
|
+
import { createWorktreeInfo } from '../../helpers/fixtures.js';
|
|
30
|
+
|
|
31
|
+
const mockedSpawnSync = vi.mocked(spawnSync);
|
|
32
|
+
const mockedGetConfigValue = vi.mocked(getConfigValue);
|
|
33
|
+
const mockedPrintInfo = vi.mocked(printInfo);
|
|
34
|
+
const mockedPrintWarning = vi.mocked(printWarning);
|
|
35
|
+
|
|
36
|
+
describe('launchInteractiveClaude', () => {
|
|
37
|
+
const worktree = createWorktreeInfo({
|
|
38
|
+
path: '/tmp/test-worktree',
|
|
39
|
+
branch: 'feature-test',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('正常启动 Claude Code(退出码为 0)', () => {
|
|
43
|
+
mockedGetConfigValue.mockReturnValue('claude');
|
|
44
|
+
mockedSpawnSync.mockReturnValue({
|
|
45
|
+
status: 0,
|
|
46
|
+
error: undefined,
|
|
47
|
+
stdout: '',
|
|
48
|
+
stderr: '',
|
|
49
|
+
pid: 1234,
|
|
50
|
+
output: [],
|
|
51
|
+
signal: null,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
launchInteractiveClaude(worktree);
|
|
55
|
+
|
|
56
|
+
expect(mockedGetConfigValue).toHaveBeenCalledWith('claudeCodeCommand');
|
|
57
|
+
expect(mockedSpawnSync).toHaveBeenCalledWith(
|
|
58
|
+
'claude',
|
|
59
|
+
expect.arrayContaining(['--append-system-prompt']),
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
cwd: '/tmp/test-worktree',
|
|
62
|
+
stdio: 'inherit',
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('输出分支和路径信息', () => {
|
|
68
|
+
mockedGetConfigValue.mockReturnValue('claude');
|
|
69
|
+
mockedSpawnSync.mockReturnValue({
|
|
70
|
+
status: 0,
|
|
71
|
+
error: undefined,
|
|
72
|
+
stdout: '',
|
|
73
|
+
stderr: '',
|
|
74
|
+
pid: 1234,
|
|
75
|
+
output: [],
|
|
76
|
+
signal: null,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
launchInteractiveClaude(worktree);
|
|
80
|
+
|
|
81
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('feature-test'));
|
|
82
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('/tmp/test-worktree'));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('支持带参数的命令(如 npx claude)', () => {
|
|
86
|
+
mockedGetConfigValue.mockReturnValue('npx claude');
|
|
87
|
+
mockedSpawnSync.mockReturnValue({
|
|
88
|
+
status: 0,
|
|
89
|
+
error: undefined,
|
|
90
|
+
stdout: '',
|
|
91
|
+
stderr: '',
|
|
92
|
+
pid: 1234,
|
|
93
|
+
output: [],
|
|
94
|
+
signal: null,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
launchInteractiveClaude(worktree);
|
|
98
|
+
|
|
99
|
+
expect(mockedSpawnSync).toHaveBeenCalledWith(
|
|
100
|
+
'npx',
|
|
101
|
+
expect.arrayContaining(['claude', '--append-system-prompt']),
|
|
102
|
+
expect.any(Object),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('spawnSync 返回 error 时抛出 ClawtError', () => {
|
|
107
|
+
mockedGetConfigValue.mockReturnValue('claude');
|
|
108
|
+
mockedSpawnSync.mockReturnValue({
|
|
109
|
+
status: null,
|
|
110
|
+
error: new Error('命令未找到'),
|
|
111
|
+
stdout: '',
|
|
112
|
+
stderr: '',
|
|
113
|
+
pid: 0,
|
|
114
|
+
output: [],
|
|
115
|
+
signal: null,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(() => launchInteractiveClaude(worktree)).toThrow(ClawtError);
|
|
119
|
+
expect(() => launchInteractiveClaude(worktree)).toThrow('启动 Claude Code 失败');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('非零退出码时调用 printWarning', () => {
|
|
123
|
+
mockedGetConfigValue.mockReturnValue('claude');
|
|
124
|
+
mockedSpawnSync.mockReturnValue({
|
|
125
|
+
status: 1,
|
|
126
|
+
error: undefined,
|
|
127
|
+
stdout: '',
|
|
128
|
+
stderr: '',
|
|
129
|
+
pid: 1234,
|
|
130
|
+
output: [],
|
|
131
|
+
signal: null,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
launchInteractiveClaude(worktree);
|
|
135
|
+
|
|
136
|
+
expect(mockedPrintWarning).toHaveBeenCalledWith(expect.stringContaining('退出码: 1'));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('退出码为 null 时不调用 printWarning', () => {
|
|
140
|
+
mockedGetConfigValue.mockReturnValue('claude');
|
|
141
|
+
mockedSpawnSync.mockReturnValue({
|
|
142
|
+
status: null,
|
|
143
|
+
error: undefined,
|
|
144
|
+
stdout: '',
|
|
145
|
+
stderr: '',
|
|
146
|
+
pid: 1234,
|
|
147
|
+
output: [],
|
|
148
|
+
signal: null,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
launchInteractiveClaude(worktree);
|
|
152
|
+
|
|
153
|
+
expect(mockedPrintWarning).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('退出码为 0 时不调用 printWarning', () => {
|
|
157
|
+
mockedGetConfigValue.mockReturnValue('claude');
|
|
158
|
+
mockedSpawnSync.mockReturnValue({
|
|
159
|
+
status: 0,
|
|
160
|
+
error: undefined,
|
|
161
|
+
stdout: '',
|
|
162
|
+
stderr: '',
|
|
163
|
+
pid: 1234,
|
|
164
|
+
output: [],
|
|
165
|
+
signal: null,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
launchInteractiveClaude(worktree);
|
|
169
|
+
|
|
170
|
+
expect(mockedPrintWarning).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock enquirer - 使用 vi.hoisted 确保变量在 vi.mock 提升后仍可访问
|
|
4
|
+
const { mockRun, MockInput } = vi.hoisted(() => {
|
|
5
|
+
const mockRun = vi.fn();
|
|
6
|
+
// 必须使用 function 声明,因为源码使用 new Enquirer.Input() 调用
|
|
7
|
+
function MockInput() {
|
|
8
|
+
return { run: mockRun };
|
|
9
|
+
}
|
|
10
|
+
return { mockRun, MockInput };
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
vi.mock('enquirer', () => ({
|
|
14
|
+
default: {
|
|
15
|
+
Input: MockInput,
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { multilineInput } from '../../../src/utils/prompt.js';
|
|
20
|
+
|
|
21
|
+
describe('multilineInput', () => {
|
|
22
|
+
it('返回用户输入的内容', async () => {
|
|
23
|
+
mockRun.mockResolvedValue('这是用户输入的内容');
|
|
24
|
+
|
|
25
|
+
const result = await multilineInput('请输入');
|
|
26
|
+
|
|
27
|
+
expect(result).toBe('这是用户输入的内容');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('返回空字符串', async () => {
|
|
31
|
+
mockRun.mockResolvedValue('');
|
|
32
|
+
|
|
33
|
+
const result = await multilineInput('请输入');
|
|
34
|
+
|
|
35
|
+
expect(result).toBe('');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('调用 prompt.run() 方法', async () => {
|
|
39
|
+
mockRun.mockResolvedValue('test');
|
|
40
|
+
|
|
41
|
+
await multilineInput('提示');
|
|
42
|
+
|
|
43
|
+
expect(mockRun).toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('返回多行文本', async () => {
|
|
47
|
+
mockRun.mockResolvedValue('第一行\n第二行\n第三行');
|
|
48
|
+
|
|
49
|
+
const result = await multilineInput('请输入多行');
|
|
50
|
+
|
|
51
|
+
expect(result).toBe('第一行\n第二行\n第三行');
|
|
52
|
+
});
|
|
53
|
+
});
|