clawt 2.8.2 → 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/README.md +18 -0
- package/docs/spec.md +26 -1
- package/package.json +7 -2
- package/tests/helpers/fixtures.ts +41 -0
- package/tests/helpers/setup.ts +2 -0
- package/tests/unit/constants/branch.test.ts +58 -0
- package/tests/unit/constants/config.test.ts +55 -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/errors/index.test.ts +32 -0
- package/tests/unit/utils/branch.test.ts +92 -0
- package/tests/unit/utils/claude.test.ts +172 -0
- package/tests/unit/utils/config.test.ts +65 -0
- package/tests/unit/utils/formatter.test.ts +69 -0
- package/tests/unit/utils/fs.test.ts +54 -0
- package/tests/unit/utils/git.test.ts +532 -0
- package/tests/unit/utils/prompt.test.ts +53 -0
- package/tests/unit/utils/shell.test.ts +108 -0
- package/tests/unit/utils/validate-snapshot.test.ts +151 -0
- package/tests/unit/utils/validation.test.ts +65 -0
- package/tests/unit/utils/worktree-matcher.test.ts +116 -0
- package/tests/unit/utils/worktree.test.ts +181 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +22 -0
package/README.md
CHANGED
|
@@ -331,3 +331,21 @@ feature/a.b → feature-a-b
|
|
|
331
331
|
## 日志
|
|
332
332
|
|
|
333
333
|
日志保存在 `~/.clawt/logs/` 目录,按日期滚动,保留 30 天。使用 `--debug` 全局选项可在终端实时查看调试日志。
|
|
334
|
+
|
|
335
|
+
## 开发
|
|
336
|
+
|
|
337
|
+
### 测试
|
|
338
|
+
|
|
339
|
+
项目使用 [Vitest](https://vitest.dev/) 作为测试框架,搭配 `@vitest/coverage-v8` 生成覆盖率报告。
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
# 执行全部测试
|
|
343
|
+
npm test
|
|
344
|
+
|
|
345
|
+
# 监听模式(文件变更后自动重新运行)
|
|
346
|
+
npm run test:watch
|
|
347
|
+
|
|
348
|
+
# 执行测试并生成覆盖率报告
|
|
349
|
+
npm run test:coverage
|
|
350
|
+
```
|
|
351
|
+
|
package/docs/spec.md
CHANGED
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
- [5.13 重置主 Worktree 工作区和暂存区](#513-重置主-worktree-工作区和暂存区)
|
|
28
28
|
- [6. 错误处理规范](#6-错误处理规范)
|
|
29
29
|
- [7. 非功能性需求](#7-非功能性需求)
|
|
30
|
+
- [7.1 性能](#71-性能)
|
|
31
|
+
- [7.2 兼容性](#72-兼容性)
|
|
32
|
+
- [7.3 测试](#73-测试)
|
|
33
|
+
- [7.4 安全性](#74-安全性)
|
|
30
34
|
|
|
31
35
|
---
|
|
32
36
|
|
|
@@ -40,6 +44,7 @@
|
|
|
40
44
|
| CLI 框架 | Commander.js |
|
|
41
45
|
| 日志库 | winston (按日期滚动文件) |
|
|
42
46
|
| 交互式 | enquirer (选项选择/确认对话) |
|
|
47
|
+
| 测试 | Vitest + @vitest/coverage-v8 |
|
|
43
48
|
| 构建 | tsup / tsc |
|
|
44
49
|
| 分发 | pnpm 全局安装 (`pnpm add -g clawt`) |
|
|
45
50
|
|
|
@@ -1155,7 +1160,27 @@ clawt reset
|
|
|
1155
1160
|
- Node.js >= 18
|
|
1156
1161
|
- Git >= 2.15(worktree 功能稳定版本)
|
|
1157
1162
|
|
|
1158
|
-
### 7.3
|
|
1163
|
+
### 7.3 测试
|
|
1164
|
+
|
|
1165
|
+
- 测试框架:Vitest,配置文件为 `vitest.config.ts`
|
|
1166
|
+
- 覆盖率工具:@vitest/coverage-v8,覆盖率报告格式为 text、lcov、html
|
|
1167
|
+
- 测试目录结构:`tests/unit/` 下按模块分组(`constants/`、`errors/`、`utils/`)
|
|
1168
|
+
- 测试辅助文件:
|
|
1169
|
+
- `tests/helpers/setup.ts`:全局 setup,禁用 chalk 颜色输出避免 ANSI 转义码干扰断言
|
|
1170
|
+
- `tests/helpers/fixtures.ts`:测试数据工厂,提供 `createWorktreeInfo()`、`createWorktreeStatus()`、`createWorktreeList()` 等工厂函数
|
|
1171
|
+
- 覆盖范围:`src/` 下的 `utils/`、`errors/`、`constants/` 全部关键模块,共 12 个测试文件、163 个测试用例
|
|
1172
|
+
- 覆盖率统计排除项:`src/index.ts`(入口文件)、`src/types/**`(类型定义)、`src/logger/**`(日志模块)
|
|
1173
|
+
- npm 脚本:
|
|
1174
|
+
- `npm test`:执行全部测试(`vitest run`)
|
|
1175
|
+
- `npm run test:watch`:监听模式(`vitest`)
|
|
1176
|
+
- `npm run test:coverage`:执行测试并生成覆盖率报告(`vitest run --coverage`)
|
|
1177
|
+
- 测试配置特性:
|
|
1178
|
+
- `restoreMocks: true`:每个测试后自动恢复 mock
|
|
1179
|
+
- `clearMocks: true`:每个测试后自动清除 mock 调用记录
|
|
1180
|
+
- `testTimeout: 10000`:单个测试超时 10 秒
|
|
1181
|
+
- `environment: 'node'`:使用 Node.js 测试环境
|
|
1182
|
+
|
|
1183
|
+
### 7.4 安全性
|
|
1159
1184
|
|
|
1160
1185
|
- 不在日志中记录 Claude Code API 密钥等敏感信息
|
|
1161
1186
|
- `--permission-mode bypassPermissions` 仅在 worktree 隔离环境中使用
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawt",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
4
4
|
"description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,8 +24,10 @@
|
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^22.13.1",
|
|
27
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
27
28
|
"tsup": "^8.3.6",
|
|
28
|
-
"typescript": "^5.7.3"
|
|
29
|
+
"typescript": "^5.7.3",
|
|
30
|
+
"vitest": "^4.0.18"
|
|
29
31
|
},
|
|
30
32
|
"engines": {
|
|
31
33
|
"node": ">=18"
|
|
@@ -33,6 +35,9 @@
|
|
|
33
35
|
"scripts": {
|
|
34
36
|
"build": "tsup",
|
|
35
37
|
"dev": "tsup --watch",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"test:coverage": "vitest run --coverage",
|
|
36
41
|
"postinstall": "node dist/postinstall.js",
|
|
37
42
|
"release": "bash scripts/release.sh"
|
|
38
43
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { WorktreeInfo, WorktreeStatus } from '../../src/types/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 创建测试用 WorktreeInfo
|
|
5
|
+
* @param {Partial<WorktreeInfo>} overrides - 覆盖字段
|
|
6
|
+
* @returns {WorktreeInfo} 测试数据
|
|
7
|
+
*/
|
|
8
|
+
export function createWorktreeInfo(overrides: Partial<WorktreeInfo> = {}): WorktreeInfo {
|
|
9
|
+
return {
|
|
10
|
+
path: '/Users/test/.clawt/worktrees/my-project/feature-branch',
|
|
11
|
+
branch: 'feature-branch',
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 创建测试用 WorktreeStatus
|
|
18
|
+
* @param {Partial<WorktreeStatus>} overrides - 覆盖字段
|
|
19
|
+
* @returns {WorktreeStatus} 测试数据
|
|
20
|
+
*/
|
|
21
|
+
export function createWorktreeStatus(overrides: Partial<WorktreeStatus> = {}): WorktreeStatus {
|
|
22
|
+
return {
|
|
23
|
+
commitCount: 3,
|
|
24
|
+
insertions: 42,
|
|
25
|
+
deletions: 10,
|
|
26
|
+
hasDirtyFiles: false,
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 创建多个测试用 WorktreeInfo
|
|
33
|
+
* @param {number} count - 数量
|
|
34
|
+
* @returns {WorktreeInfo[]} 测试数据数组
|
|
35
|
+
*/
|
|
36
|
+
export function createWorktreeList(count: number): WorktreeInfo[] {
|
|
37
|
+
return Array.from({ length: count }, (_, i) => createWorktreeInfo({
|
|
38
|
+
path: `/Users/test/.clawt/worktrees/my-project/branch-${i + 1}`,
|
|
39
|
+
branch: `branch-${i + 1}`,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
@@ -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,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CONFIG_DEFINITIONS, DEFAULT_CONFIG, CONFIG_DESCRIPTIONS } from '../../../src/constants/config.js';
|
|
3
|
+
|
|
4
|
+
describe('CONFIG_DEFINITIONS', () => {
|
|
5
|
+
it('所有配置项都有 defaultValue 和 description', () => {
|
|
6
|
+
for (const [key, def] of Object.entries(CONFIG_DEFINITIONS)) {
|
|
7
|
+
expect(def).toHaveProperty('defaultValue');
|
|
8
|
+
expect(def).toHaveProperty('description');
|
|
9
|
+
expect(typeof def.description).toBe('string');
|
|
10
|
+
expect(def.description.length).toBeGreaterThan(0);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('DEFAULT_CONFIG', () => {
|
|
16
|
+
it('与 CONFIG_DEFINITIONS 的 key 一致', () => {
|
|
17
|
+
const definitionKeys = Object.keys(CONFIG_DEFINITIONS).sort();
|
|
18
|
+
const configKeys = Object.keys(DEFAULT_CONFIG).sort();
|
|
19
|
+
expect(configKeys).toEqual(definitionKeys);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('每个 key 的值等于对应 CONFIG_DEFINITIONS 的 defaultValue', () => {
|
|
23
|
+
for (const [key, def] of Object.entries(CONFIG_DEFINITIONS)) {
|
|
24
|
+
expect((DEFAULT_CONFIG as Record<string, unknown>)[key]).toBe(def.defaultValue);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('包含预期的配置项和默认值', () => {
|
|
29
|
+
expect(DEFAULT_CONFIG.autoDeleteBranch).toBe(false);
|
|
30
|
+
expect(DEFAULT_CONFIG.claudeCodeCommand).toBe('claude');
|
|
31
|
+
expect(DEFAULT_CONFIG.autoPullPush).toBe(false);
|
|
32
|
+
expect(DEFAULT_CONFIG.confirmDestructiveOps).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('CONFIG_DESCRIPTIONS', () => {
|
|
37
|
+
it('与 CONFIG_DEFINITIONS 的 key 一致', () => {
|
|
38
|
+
const definitionKeys = Object.keys(CONFIG_DEFINITIONS).sort();
|
|
39
|
+
const descriptionKeys = Object.keys(CONFIG_DESCRIPTIONS).sort();
|
|
40
|
+
expect(descriptionKeys).toEqual(definitionKeys);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('每个 key 的值等于对应 CONFIG_DEFINITIONS 的 description', () => {
|
|
44
|
+
for (const [key, def] of Object.entries(CONFIG_DEFINITIONS)) {
|
|
45
|
+
expect((CONFIG_DESCRIPTIONS as Record<string, string>)[key]).toBe(def.description);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('所有描述值都是非空字符串', () => {
|
|
50
|
+
for (const desc of Object.values(CONFIG_DESCRIPTIONS)) {
|
|
51
|
+
expect(typeof desc).toBe('string');
|
|
52
|
+
expect(desc.length).toBeGreaterThan(0);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -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
|
+
});
|