clawt 2.8.2 → 2.9.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 +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/config.test.ts +55 -0
- package/tests/unit/errors/index.test.ts +32 -0
- package/tests/unit/utils/branch.test.ts +92 -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/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.0",
|
|
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,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,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ClawtError } from '../../../src/errors/index.js';
|
|
3
|
+
import { EXIT_CODES } from '../../../src/constants/index.js';
|
|
4
|
+
|
|
5
|
+
describe('ClawtError', () => {
|
|
6
|
+
it('默认退出码为 EXIT_CODES.ERROR (1)', () => {
|
|
7
|
+
const error = new ClawtError('测试错误');
|
|
8
|
+
expect(error.exitCode).toBe(EXIT_CODES.ERROR);
|
|
9
|
+
expect(error.exitCode).toBe(1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('自定义退出码正确传递', () => {
|
|
13
|
+
const error = new ClawtError('参数错误', EXIT_CODES.ARGUMENT_ERROR);
|
|
14
|
+
expect(error.exitCode).toBe(2);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('name 属性为 ClawtError', () => {
|
|
18
|
+
const error = new ClawtError('测试');
|
|
19
|
+
expect(error.name).toBe('ClawtError');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('message 正确设置', () => {
|
|
23
|
+
const error = new ClawtError('具体的错误消息');
|
|
24
|
+
expect(error.message).toBe('具体的错误消息');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('是 Error 的实例', () => {
|
|
28
|
+
const error = new ClawtError('测试');
|
|
29
|
+
expect(error).toBeInstanceOf(Error);
|
|
30
|
+
expect(error).toBeInstanceOf(ClawtError);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } 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 formatter(避免终端输出)
|
|
9
|
+
vi.mock('../../../src/utils/formatter.js', () => ({
|
|
10
|
+
printWarning: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// mock git(checkBranchExists)
|
|
14
|
+
vi.mock('../../../src/utils/git.js', () => ({
|
|
15
|
+
checkBranchExists: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from '../../../src/utils/branch.js';
|
|
19
|
+
import { printWarning } from '../../../src/utils/formatter.js';
|
|
20
|
+
import { checkBranchExists } from '../../../src/utils/git.js';
|
|
21
|
+
import { ClawtError } from '../../../src/errors/index.js';
|
|
22
|
+
|
|
23
|
+
const mockedCheckBranchExists = vi.mocked(checkBranchExists);
|
|
24
|
+
const mockedPrintWarning = vi.mocked(printWarning);
|
|
25
|
+
|
|
26
|
+
describe('sanitizeBranchName', () => {
|
|
27
|
+
it('合法分支名原样返回', () => {
|
|
28
|
+
expect(sanitizeBranchName('feature-add-login')).toBe('feature-add-login');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('非法字符替换为 -', () => {
|
|
32
|
+
expect(sanitizeBranchName('feature/add login')).toBe('feature-add-login');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('连续非法字符压缩为单个 -', () => {
|
|
36
|
+
expect(sanitizeBranchName('feature...add')).toBe('feature-add');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('首尾 - 去除', () => {
|
|
40
|
+
expect(sanitizeBranchName('.feature-add.')).toBe('feature-add');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('全部非法字符时抛出 ClawtError', () => {
|
|
44
|
+
expect(() => sanitizeBranchName('...')).toThrow(ClawtError);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('转换时触发 printWarning', () => {
|
|
48
|
+
sanitizeBranchName('feature/test');
|
|
49
|
+
expect(mockedPrintWarning).toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('合法分支名不触发 printWarning', () => {
|
|
53
|
+
sanitizeBranchName('feature-test');
|
|
54
|
+
expect(mockedPrintWarning).not.toHaveBeenCalled();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('generateBranchNames', () => {
|
|
59
|
+
it('count=1 返回 [branchName]', () => {
|
|
60
|
+
expect(generateBranchNames('feature', 1)).toEqual(['feature']);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('count=3 返回带序号后缀的数组', () => {
|
|
64
|
+
expect(generateBranchNames('feature', 3)).toEqual([
|
|
65
|
+
'feature-1',
|
|
66
|
+
'feature-2',
|
|
67
|
+
'feature-3',
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('count=2 返回带序号后缀的数组', () => {
|
|
72
|
+
expect(generateBranchNames('test', 2)).toEqual(['test-1', 'test-2']);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('validateBranchesNotExist', () => {
|
|
77
|
+
it('所有分支不存在时正常通过', () => {
|
|
78
|
+
mockedCheckBranchExists.mockReturnValue(false);
|
|
79
|
+
expect(() => validateBranchesNotExist(['a', 'b', 'c'])).not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('有分支存在时抛出 ClawtError', () => {
|
|
83
|
+
mockedCheckBranchExists.mockReturnValueOnce(false).mockReturnValueOnce(true);
|
|
84
|
+
expect(() => validateBranchesNotExist(['a', 'b'])).toThrow(ClawtError);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('第一个分支存在时立即抛出', () => {
|
|
88
|
+
mockedCheckBranchExists.mockReturnValue(true);
|
|
89
|
+
expect(() => validateBranchesNotExist(['existing'])).toThrow(ClawtError);
|
|
90
|
+
expect(mockedCheckBranchExists).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect, vi } 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 fs 工具
|
|
16
|
+
vi.mock('../../../src/utils/fs.js', () => ({
|
|
17
|
+
ensureDir: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { loadConfig, getConfigValue } from '../../../src/utils/config.js';
|
|
22
|
+
import { DEFAULT_CONFIG } from '../../../src/constants/index.js';
|
|
23
|
+
|
|
24
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
25
|
+
const mockedReadFileSync = vi.mocked(readFileSync);
|
|
26
|
+
const mockedWriteFileSync = vi.mocked(writeFileSync);
|
|
27
|
+
|
|
28
|
+
describe('loadConfig', () => {
|
|
29
|
+
it('配置文件不存在时返回默认配置', () => {
|
|
30
|
+
mockedExistsSync.mockReturnValue(false);
|
|
31
|
+
const config = loadConfig();
|
|
32
|
+
expect(config).toEqual(DEFAULT_CONFIG);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('配置文件存在时正确合并', () => {
|
|
36
|
+
mockedExistsSync.mockReturnValue(true);
|
|
37
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({ autoDeleteBranch: true }));
|
|
38
|
+
const config = loadConfig();
|
|
39
|
+
expect(config.autoDeleteBranch).toBe(true);
|
|
40
|
+
// 未覆盖的字段保持默认值
|
|
41
|
+
expect(config.claudeCodeCommand).toBe(DEFAULT_CONFIG.claudeCodeCommand);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('配置文件损坏时返回默认配置并重写', () => {
|
|
45
|
+
mockedExistsSync.mockReturnValue(true);
|
|
46
|
+
mockedReadFileSync.mockReturnValue('invalid json {{{');
|
|
47
|
+
const config = loadConfig();
|
|
48
|
+
expect(config).toEqual(DEFAULT_CONFIG);
|
|
49
|
+
// 应该重写默认配置
|
|
50
|
+
expect(mockedWriteFileSync).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('getConfigValue', () => {
|
|
55
|
+
it('获取指定 key 的值', () => {
|
|
56
|
+
mockedExistsSync.mockReturnValue(true);
|
|
57
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({ autoPullPush: true }));
|
|
58
|
+
expect(getConfigValue('autoPullPush')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('未设置时返回默认值', () => {
|
|
62
|
+
mockedExistsSync.mockReturnValue(false);
|
|
63
|
+
expect(getConfigValue('confirmDestructiveOps')).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { formatWorktreeStatus, printSuccess, printError, printWarning, printInfo } from '../../../src/utils/formatter.js';
|
|
3
|
+
import { createWorktreeStatus } from '../../helpers/fixtures.js';
|
|
4
|
+
|
|
5
|
+
describe('formatWorktreeStatus', () => {
|
|
6
|
+
it('有 insertions 和 deletions 时正确格式化', () => {
|
|
7
|
+
const status = createWorktreeStatus({ commitCount: 5, insertions: 100, deletions: 20 });
|
|
8
|
+
const result = formatWorktreeStatus(status);
|
|
9
|
+
expect(result).toContain('5 个提交');
|
|
10
|
+
expect(result).toContain('+100');
|
|
11
|
+
expect(result).toContain('-20');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('无变更时显示"无变更"', () => {
|
|
15
|
+
const status = createWorktreeStatus({ insertions: 0, deletions: 0 });
|
|
16
|
+
const result = formatWorktreeStatus(status);
|
|
17
|
+
expect(result).toContain('无变更');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('hasDirtyFiles 时显示"(未提交修改)"', () => {
|
|
21
|
+
const status = createWorktreeStatus({ hasDirtyFiles: true });
|
|
22
|
+
const result = formatWorktreeStatus(status);
|
|
23
|
+
expect(result).toContain('(未提交修改)');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('hasDirtyFiles 为 false 时不显示"(未提交修改)"', () => {
|
|
27
|
+
const status = createWorktreeStatus({ hasDirtyFiles: false });
|
|
28
|
+
const result = formatWorktreeStatus(status);
|
|
29
|
+
expect(result).not.toContain('(未提交修改)');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('仅有 insertions 时不显示"无变更"', () => {
|
|
33
|
+
const status = createWorktreeStatus({ insertions: 10, deletions: 0 });
|
|
34
|
+
const result = formatWorktreeStatus(status);
|
|
35
|
+
expect(result).not.toContain('无变更');
|
|
36
|
+
expect(result).toContain('+10');
|
|
37
|
+
expect(result).toContain('-0');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('print 函数', () => {
|
|
42
|
+
it('printSuccess 调用 console.log', () => {
|
|
43
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
44
|
+
printSuccess('成功消息');
|
|
45
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(spy.mock.calls[0][0]).toContain('成功消息');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('printError 调用 console.error', () => {
|
|
50
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
51
|
+
printError('错误消息');
|
|
52
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(spy.mock.calls[0][0]).toContain('错误消息');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('printWarning 调用 console.log', () => {
|
|
57
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
58
|
+
printWarning('警告消息');
|
|
59
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
60
|
+
expect(spy.mock.calls[0][0]).toContain('警告消息');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('printInfo 调用 console.log', () => {
|
|
64
|
+
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
65
|
+
printInfo('普通消息');
|
|
66
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(spy.mock.calls[0][0]).toBe('普通消息');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock node:fs
|
|
4
|
+
vi.mock('node:fs', () => ({
|
|
5
|
+
existsSync: vi.fn(),
|
|
6
|
+
mkdirSync: vi.fn(),
|
|
7
|
+
readdirSync: vi.fn(),
|
|
8
|
+
rmdirSync: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readdirSync, rmdirSync } from 'node:fs';
|
|
12
|
+
import { ensureDir, removeEmptyDir } from '../../../src/utils/fs.js';
|
|
13
|
+
|
|
14
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
15
|
+
const mockedMkdirSync = vi.mocked(mkdirSync);
|
|
16
|
+
const mockedReaddirSync = vi.mocked(readdirSync);
|
|
17
|
+
const mockedRmdirSync = vi.mocked(rmdirSync);
|
|
18
|
+
|
|
19
|
+
describe('ensureDir', () => {
|
|
20
|
+
it('目录不存在时创建', () => {
|
|
21
|
+
mockedExistsSync.mockReturnValue(false);
|
|
22
|
+
ensureDir('/tmp/test-dir');
|
|
23
|
+
expect(mockedMkdirSync).toHaveBeenCalledWith('/tmp/test-dir', { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('目录已存在时不操作', () => {
|
|
27
|
+
mockedExistsSync.mockReturnValue(true);
|
|
28
|
+
ensureDir('/tmp/test-dir');
|
|
29
|
+
expect(mockedMkdirSync).not.toHaveBeenCalled();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('removeEmptyDir', () => {
|
|
34
|
+
it('空目录删除返回 true', () => {
|
|
35
|
+
mockedExistsSync.mockReturnValue(true);
|
|
36
|
+
mockedReaddirSync.mockReturnValue([]);
|
|
37
|
+
expect(removeEmptyDir('/tmp/empty-dir')).toBe(true);
|
|
38
|
+
expect(mockedRmdirSync).toHaveBeenCalledWith('/tmp/empty-dir');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('非空目录不删除返回 false', () => {
|
|
42
|
+
mockedExistsSync.mockReturnValue(true);
|
|
43
|
+
// @ts-expect-error readdirSync 返回的类型比较复杂,这里简化处理
|
|
44
|
+
mockedReaddirSync.mockReturnValue(['file.txt']);
|
|
45
|
+
expect(removeEmptyDir('/tmp/non-empty-dir')).toBe(false);
|
|
46
|
+
expect(mockedRmdirSync).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('目录不存在时返回 false', () => {
|
|
50
|
+
mockedExistsSync.mockReturnValue(false);
|
|
51
|
+
expect(removeEmptyDir('/tmp/no-dir')).toBe(false);
|
|
52
|
+
expect(mockedReaddirSync).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
});
|