clawt 2.14.1 → 2.15.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 +23 -0
- package/dist/index.js +153 -45
- package/dist/postinstall.js +17 -1
- package/docs/spec.md +46 -0
- package/package.json +1 -1
- package/src/commands/run.ts +68 -21
- package/src/constants/messages/run.ts +16 -0
- package/src/types/command.ts +2 -0
- package/src/utils/config.ts +20 -0
- package/src/utils/dry-run.ts +89 -0
- package/src/utils/index.ts +3 -2
- package/src/utils/task-file.ts +18 -0
- package/tests/unit/commands/resume.test.ts +9 -9
- package/tests/unit/commands/run.test.ts +160 -6
- package/tests/unit/constants/messages.test.ts +5 -1
- package/tests/unit/utils/config.test.ts +23 -1
- package/tests/unit/utils/dry-run.test.ts +124 -0
- package/tests/unit/utils/task-file.test.ts +26 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('../../../src/constants/index.js', () => ({
|
|
4
|
+
MESSAGES: {
|
|
5
|
+
DRY_RUN_TITLE: 'Dry Run 预览',
|
|
6
|
+
DRY_RUN_TASK_COUNT: (count: number) => `任务数: ${count}`,
|
|
7
|
+
DRY_RUN_CONCURRENCY: (concurrency: number) => `并发数: ${concurrency === 0 ? '不限制' : concurrency}`,
|
|
8
|
+
DRY_RUN_WORKTREE_DIR: (dir: string) => `Worktree: ${dir}`,
|
|
9
|
+
DRY_RUN_BRANCH_EXISTS_WARNING: (name: string) => `分支 ${name} 已存在`,
|
|
10
|
+
DRY_RUN_INTERACTIVE_MODE: '模式: 交互式(无预设任务)',
|
|
11
|
+
DRY_RUN_READY: '预览完成,无冲突。移除 --dry-run 即可正式执行。',
|
|
12
|
+
DRY_RUN_HAS_CONFLICT: '存在分支冲突,实际执行时将会报错。请先处理冲突的分支。',
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('../../../src/utils/git.js', () => ({
|
|
17
|
+
checkBranchExists: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('../../../src/utils/worktree.js', () => ({
|
|
21
|
+
getProjectWorktreeDir: vi.fn().mockReturnValue('/mock/.clawt/worktrees/test-project'),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('../../../src/utils/formatter.js', () => ({
|
|
25
|
+
printInfo: vi.fn(),
|
|
26
|
+
printDoubleSeparator: vi.fn(),
|
|
27
|
+
printSeparator: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
import { truncateTaskDesc, printDryRunPreview } from '../../../src/utils/dry-run.js';
|
|
31
|
+
import { checkBranchExists } from '../../../src/utils/git.js';
|
|
32
|
+
import { printInfo } from '../../../src/utils/formatter.js';
|
|
33
|
+
|
|
34
|
+
const mockedCheckBranchExists = vi.mocked(checkBranchExists);
|
|
35
|
+
const mockedPrintInfo = vi.mocked(printInfo);
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
mockedCheckBranchExists.mockReset();
|
|
39
|
+
mockedPrintInfo.mockReset();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('truncateTaskDesc', () => {
|
|
43
|
+
it('短文本原样返回', () => {
|
|
44
|
+
expect(truncateTaskDesc('实现登录功能')).toBe('实现登录功能');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('超过80字符时截断并加省略号', () => {
|
|
48
|
+
const longTask = 'a'.repeat(100);
|
|
49
|
+
const result = truncateTaskDesc(longTask);
|
|
50
|
+
expect(result.length).toBe(83); // 80 + '...'
|
|
51
|
+
expect(result.endsWith('...')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('恰好80字符时不截断', () => {
|
|
55
|
+
const task = 'a'.repeat(80);
|
|
56
|
+
expect(truncateTaskDesc(task)).toBe(task);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('多行文本合并为单行', () => {
|
|
60
|
+
const multiLine = '第一行\n第二行\n第三行';
|
|
61
|
+
expect(truncateTaskDesc(multiLine)).toBe('第一行 第二行 第三行');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('首尾空白被去除', () => {
|
|
65
|
+
expect(truncateTaskDesc(' hello ')).toBe('hello');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('printDryRunPreview', () => {
|
|
70
|
+
it('无冲突时输出预览完成结论', () => {
|
|
71
|
+
mockedCheckBranchExists.mockReturnValue(false);
|
|
72
|
+
|
|
73
|
+
printDryRunPreview(['feat-1', 'feat-2'], ['任务1', '任务2'], 0);
|
|
74
|
+
|
|
75
|
+
// 正常分支应包含 ✓ 标识
|
|
76
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('✓'));
|
|
77
|
+
// 应输出预览完成结论
|
|
78
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('预览完成'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('有分支冲突时输出冲突警告', () => {
|
|
82
|
+
mockedCheckBranchExists
|
|
83
|
+
.mockReturnValueOnce(false)
|
|
84
|
+
.mockReturnValueOnce(true);
|
|
85
|
+
|
|
86
|
+
printDryRunPreview(['feat-1', 'feat-2'], ['任务1', '任务2'], 0);
|
|
87
|
+
|
|
88
|
+
// 冲突分支的序号行应同时包含分支名和"已存在"警告
|
|
89
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('已存在'));
|
|
90
|
+
// 冲突分支应包含 ⚠ 标识
|
|
91
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('⚠'));
|
|
92
|
+
// 应输出冲突结论
|
|
93
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('分支冲突'));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('交互式模式显示交互提示', () => {
|
|
97
|
+
mockedCheckBranchExists.mockReturnValue(false);
|
|
98
|
+
|
|
99
|
+
printDryRunPreview(['feat'], [], 0);
|
|
100
|
+
|
|
101
|
+
// 交互式模式下应显示交互模式提示
|
|
102
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('交互式'));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('非交互式模式显示任务描述', () => {
|
|
106
|
+
mockedCheckBranchExists.mockReturnValue(false);
|
|
107
|
+
|
|
108
|
+
printDryRunPreview(['feat-1'], ['实现登录功能'], 2);
|
|
109
|
+
|
|
110
|
+
// 应包含任务描述
|
|
111
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('实现登录功能'));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('摘要行合并为一行并包含关键信息', () => {
|
|
115
|
+
mockedCheckBranchExists.mockReturnValue(false);
|
|
116
|
+
|
|
117
|
+
printDryRunPreview(['feat-1', 'feat-2'], ['任务1', '任务2'], 5);
|
|
118
|
+
|
|
119
|
+
// 摘要行应同时包含任务数、并发数和 Worktree 路径
|
|
120
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('任务数: 2'));
|
|
121
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('并发数: 5'));
|
|
122
|
+
expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('Worktree:'));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { parseTaskFile, loadTaskFile } from '../../../src/utils/task-file.js';
|
|
2
|
+
import { parseTaskFile, loadTaskFile, parseTasksFromOptions } from '../../../src/utils/task-file.js';
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
|
|
5
5
|
vi.mock('node:fs', () => ({
|
|
@@ -234,3 +234,28 @@ describe('loadTaskFile', () => {
|
|
|
234
234
|
expect(entries[0].task).toBe('实现登录功能');
|
|
235
235
|
});
|
|
236
236
|
});
|
|
237
|
+
|
|
238
|
+
describe('parseTasksFromOptions', () => {
|
|
239
|
+
it('正常解析任务列表', () => {
|
|
240
|
+
const tasks = parseTasksFromOptions(['task1', 'task2', 'task3']);
|
|
241
|
+
expect(tasks).toEqual(['task1', 'task2', 'task3']);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('去除首尾空白', () => {
|
|
245
|
+
const tasks = parseTasksFromOptions([' task1 ', ' task2 ']);
|
|
246
|
+
expect(tasks).toEqual(['task1', 'task2']);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('过滤空字符串', () => {
|
|
250
|
+
const tasks = parseTasksFromOptions(['task1', '', ' ', 'task2']);
|
|
251
|
+
expect(tasks).toEqual(['task1', 'task2']);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('全部为空时抛出错误', () => {
|
|
255
|
+
expect(() => parseTasksFromOptions(['', ' '])).toThrow('任务列表不能为空');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('空数组时抛出错误', () => {
|
|
259
|
+
expect(() => parseTasksFromOptions([])).toThrow('任务列表不能为空');
|
|
260
|
+
});
|
|
261
|
+
});
|