feat: 添加完整的单元测试套件

- 新增 vitest 测试框架配置
- 添加 54 个测试文件,共 951 个测试用例
- 覆盖核心模块:
  - Agent: executor, registry, config-loader, permission-merger
  - Context: manager, compaction, prune, token-counter
  - Permission: manager, bash/file/git/web checkers, wildcard
  - Session: manager, storage
  - Tools: filesystem (12个), git (10个), web, shell, todo, task
  - LSP: client, server, language
  - Utils: config, diff
  - UI: terminal
This commit is contained in:
2025-12-11 14:45:24 +08:00
parent f4df6483a6
commit 729fb2d42a
58 changed files with 14320 additions and 3 deletions
+160
View File
@@ -0,0 +1,160 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: '',
stderr: '',
};
// Mock child_process
vi.mock('child_process', () => ({
exec: vi.fn(),
}));
// Mock util
vi.mock('util', () => ({
promisify: vi.fn(() => vi.fn(async () => {
if (mockExecAsyncResult instanceof Error) {
throw mockExecAsyncResult;
}
return mockExecAsyncResult;
})),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => 'Git add 命令'),
}));
import { gitAddTool } from '../../../../src/tools/git/git_add.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitAddTool - Git Add 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: '',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitAddTool.name).toBe('git_add');
});
it('有正确的元数据', () => {
expect(gitAddTool.metadata.category).toBe('git');
expect(gitAddTool.metadata.keywords).toContain('add');
expect(gitAddTool.metadata.keywords).toContain('stage');
});
it('定义了可选参数', () => {
expect(gitAddTool.parameters.files.required).toBe(false);
expect(gitAddTool.parameters.all.required).toBe(false);
expect(gitAddTool.parameters.update.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('暂存所有文件 (all: true)', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitAddTool.execute({ all: true });
expect(result.success).toBe(true);
expect(result.output).toContain('文件已暂存');
});
it('暂存指定文件', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitAddTool.execute({
files: ['file1.txt', 'file2.txt'],
});
expect(result.success).toBe(true);
});
it('暂存单个文件(字符串)', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitAddTool.execute({ files: 'single.txt' });
expect(result.success).toBe(true);
});
it('使用 update 选项', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitAddTool.execute({ update: true });
expect(result.success).toBe(true);
});
it('无参数返回错误', async () => {
const result = await gitAddTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('请指定要暂存的文件');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '操作不被允许',
}),
} as any);
const result = await gitAddTool.execute({ all: true });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitAddTool.execute({ all: true });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('命令执行失败返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'fatal: not a git repository', message: 'Command failed' }
);
const result = await gitAddTool.execute({ all: true });
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+240
View File
@@ -0,0 +1,240 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock child_process
const mockExec = vi.fn();
vi.mock('child_process', () => ({
exec: (cmd: string, opts: any, cb?: Function) => {
if (typeof opts === 'function') {
cb = opts;
}
// 使用 setImmediate 模拟异步
setImmediate(() => {
const result = mockExec(cmd);
if (result.error) {
const err = result.error;
err.stdout = result.stdout || '';
err.stderr = result.stderr || '';
cb?.(err, result.stdout || '', result.stderr || '');
} else {
cb?.(null, result.stdout || '', result.stderr || '');
}
});
},
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '管理 Git 分支'),
}));
import { gitBranchTool } from '../../../../src/tools/git/git_branch.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitBranchTool - Git 分支工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExec.mockReturnValue({ stdout: '* main\n develop', stderr: '' });
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitBranchTool.name).toBe('git_branch');
});
it('有正确的元数据', () => {
expect(gitBranchTool.metadata.category).toBe('git');
expect(gitBranchTool.metadata.keywords).toContain('branch');
expect(gitBranchTool.metadata.keywords).toContain('create');
expect(gitBranchTool.metadata.keywords).toContain('delete');
});
it('定义了可选参数', () => {
expect(gitBranchTool.parameters.action.required).toBe(false);
expect(gitBranchTool.parameters.name.required).toBe(false);
expect(gitBranchTool.parameters.new_name.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('列出分支(默认操作)', async () => {
const result = await gitBranchTool.execute({});
expect(result.success).toBe(true);
// 源代码使用 -v 参数,输出可能包含 main 或在空结果时返回空字符串
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch'));
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('-v'));
});
it('创建新分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitBranchTool.execute({
action: 'create',
name: 'feature/new-branch',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch feature/new-branch'));
});
it('删除分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitBranchTool.execute({
action: 'delete',
name: 'old-branch',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -d old-branch'));
});
it('强制删除分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitBranchTool.execute({
action: 'delete',
name: 'unmerged-branch',
force: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -D unmerged-branch'));
});
it('重命名分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitBranchTool.execute({
action: 'rename',
name: 'old-name',
new_name: 'new-name',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -m old-name new-name'));
});
it('显示远程分支', async () => {
mockExec.mockReturnValue({ stdout: 'origin/main\norigin/develop', stderr: '' });
const result = await gitBranchTool.execute({
action: 'list',
remote: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -r'));
});
it('显示所有分支', async () => {
const result = await gitBranchTool.execute({
action: 'list',
all: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -a'));
});
it('创建分支缺少名称返回错误', async () => {
const result = await gitBranchTool.execute({
action: 'create',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要提供分支名称');
});
it('删除分支缺少名称返回错误', async () => {
const result = await gitBranchTool.execute({
action: 'delete',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要提供分支名称');
});
it('重命名缺少参数返回错误', async () => {
const result = await gitBranchTool.execute({
action: 'rename',
name: 'old-name',
});
expect(result.success).toBe(false);
expect(result.error).toContain('新名称');
});
it('未知操作返回错误', async () => {
const result = await gitBranchTool.execute({
action: 'unknown',
});
expect(result.success).toBe(false);
expect(result.error).toContain('未知操作');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许操作分支',
}),
} as any);
const result = await gitBranchTool.execute({
action: 'create',
name: 'test',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitBranchTool.execute({
action: 'delete',
name: 'branch',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('Git 命令失败返回错误', async () => {
// 恢复权限检查
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'fatal: not a git repository',
});
const result = await gitBranchTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+192
View File
@@ -0,0 +1,192 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock child_process
const mockExec = vi.fn();
vi.mock('child_process', () => ({
exec: (cmd: string, opts: any, cb?: Function) => {
if (typeof opts === 'function') {
cb = opts;
}
setImmediate(() => {
const result = mockExec(cmd);
if (result.error) {
const err = result.error;
err.stdout = result.stdout || '';
err.stderr = result.stderr || '';
cb?.(err, result.stdout || '', result.stderr || '');
} else {
cb?.(null, result.stdout || '', result.stderr || '');
}
});
},
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '切换分支或恢复文件'),
}));
import { gitCheckoutTool } from '../../../../src/tools/git/git_checkout.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitCheckoutTool - Git Checkout 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExec.mockReturnValue({ stdout: '', stderr: "Switched to branch 'develop'" });
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitCheckoutTool.name).toBe('git_checkout');
});
it('有正确的元数据', () => {
expect(gitCheckoutTool.metadata.category).toBe('git');
expect(gitCheckoutTool.metadata.keywords).toContain('checkout');
expect(gitCheckoutTool.metadata.keywords).toContain('switch');
});
it('定义了必需和可选参数', () => {
expect(gitCheckoutTool.parameters.target.required).toBe(true);
expect(gitCheckoutTool.parameters.create.required).toBe(false);
expect(gitCheckoutTool.parameters.force.required).toBe(false);
expect(gitCheckoutTool.parameters.file.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功切换分支', async () => {
const result = await gitCheckoutTool.execute({
target: 'develop',
});
expect(result.success).toBe(true);
expect(result.output).toContain('develop');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git checkout develop'));
});
it('创建并切换到新分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: "Switched to a new branch 'feature/test'" });
const result = await gitCheckoutTool.execute({
target: 'feature/test',
create: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git checkout -b feature/test'));
});
it('强制切换分支', async () => {
const result = await gitCheckoutTool.execute({
target: 'develop',
force: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git checkout -f develop'));
});
it('恢复文件', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitCheckoutTool.execute({
target: 'src/index.ts',
file: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git checkout -- src/index.ts'));
});
it('缺少 target 返回错误', async () => {
const result = await gitCheckoutTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('请指定目标');
});
it('本地变更冲突时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'error: Your local changes would be overwritten by checkout',
});
const result = await gitCheckoutTool.execute({
target: 'main',
});
expect(result.success).toBe(false);
expect(result.error).toContain('本地有未提交的变更');
expect(result.error).toContain('force: true');
});
it('分支不存在时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: "error: pathspec 'nonexistent' did not match",
});
const result = await gitCheckoutTool.execute({
target: 'nonexistent',
});
expect(result.success).toBe(false);
expect(result.error).toContain('找不到分支或文件');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许切换分支',
}),
} as any);
const result = await gitCheckoutTool.execute({
target: 'main',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitCheckoutTool.execute({
target: 'develop',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
});
});
+156
View File
@@ -0,0 +1,156 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: '[main abc1234] test commit\n1 file changed',
stderr: '',
};
// Mock child_process
vi.mock('child_process', () => ({
exec: vi.fn(),
}));
// Mock util - 返回一个函数,该函数使用外部变量
vi.mock('util', () => ({
promisify: vi.fn(() => vi.fn(async () => {
if (mockExecAsyncResult instanceof Error) {
throw mockExecAsyncResult;
}
return mockExecAsyncResult;
})),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '提交 Git 变更'),
}));
import { gitCommitTool } from '../../../../src/tools/git/git_commit.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitCommitTool - Git 提交工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: '[main abc1234] test commit\n1 file changed',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitCommitTool.name).toBe('git_commit');
});
it('有正确的元数据', () => {
expect(gitCommitTool.metadata.category).toBe('git');
expect(gitCommitTool.metadata.keywords).toContain('commit');
});
it('定义了必需的 message 参数', () => {
expect(gitCommitTool.parameters.message).toBeDefined();
expect(gitCommitTool.parameters.message.required).toBe(true);
});
it('定义了可选参数', () => {
expect(gitCommitTool.parameters.amend).toBeDefined();
expect(gitCommitTool.parameters.amend.required).toBe(false);
expect(gitCommitTool.parameters.all).toBeDefined();
expect(gitCommitTool.parameters.all.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功提交', async () => {
const result = await gitCommitTool.execute({ message: 'test commit' });
expect(result.success).toBe(true);
expect(result.output).toContain('test commit');
});
it('无消息且无 amend 返回错误', async () => {
const result = await gitCommitTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('提交信息是必填的');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '操作不被允许',
}),
} as any);
const result = await gitCommitTool.execute({ message: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitCommitTool.execute({ message: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('无变更可提交时返回友好提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('nothing to commit'),
{ stderr: 'nothing to commit, working tree clean', stdout: '', message: 'nothing to commit' }
);
const result = await gitCommitTool.execute({ message: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('没有变更需要提交');
});
it('传递操作类型给权限检查', async () => {
const mockCheck = vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
});
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: mockCheck,
} as any);
await gitCommitTool.execute({ message: 'test message' });
expect(mockCheck).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'commit',
message: 'test message',
})
);
});
});
});
+172
View File
@@ -0,0 +1,172 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: '',
stderr: '',
};
// Mock child_process
vi.mock('child_process', () => ({
exec: vi.fn(),
}));
// Mock util
vi.mock('util', () => ({
promisify: vi.fn(() => vi.fn(async () => {
if (mockExecAsyncResult instanceof Error) {
throw mockExecAsyncResult;
}
return mockExecAsyncResult;
})),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => 'Git diff 命令'),
}));
import { gitDiffTool } from '../../../../src/tools/git/git_diff.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitDiffTool - Git Diff 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: 'diff --git a/file.txt b/file.txt\n-old\n+new',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitDiffTool.name).toBe('git_diff');
});
it('有正确的元数据', () => {
expect(gitDiffTool.metadata.category).toBe('git');
expect(gitDiffTool.metadata.keywords).toContain('diff');
expect(gitDiffTool.metadata.keywords).toContain('compare');
});
it('定义了可选参数', () => {
expect(gitDiffTool.parameters.path.required).toBe(false);
expect(gitDiffTool.parameters.staged.required).toBe(false);
expect(gitDiffTool.parameters.commit.required).toBe(false);
expect(gitDiffTool.parameters.stat.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功获取差异', async () => {
const result = await gitDiffTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('diff');
});
it('无差异时显示提示', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitDiffTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('无差异');
});
it('显示暂存区差异', async () => {
mockExecAsyncResult = { stdout: 'staged changes', stderr: '' };
const result = await gitDiffTool.execute({ staged: true });
expect(result.success).toBe(true);
expect(result.output).toContain('staged changes');
});
it('与指定提交对比', async () => {
mockExecAsyncResult = { stdout: 'commit diff', stderr: '' };
const result = await gitDiffTool.execute({ commit: 'HEAD~1' });
expect(result.success).toBe(true);
});
it('指定文件路径', async () => {
mockExecAsyncResult = { stdout: 'file diff', stderr: '' };
const result = await gitDiffTool.execute({ path: 'src/file.ts' });
expect(result.success).toBe(true);
});
it('仅显示统计信息', async () => {
mockExecAsyncResult = {
stdout: ' file.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)',
stderr: '',
};
const result = await gitDiffTool.execute({ stat: true });
expect(result.success).toBe(true);
expect(result.output).toContain('file changed');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '操作不被允许',
}),
} as any);
const result = await gitDiffTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitDiffTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('命令执行失败返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'fatal: not a git repository', message: 'Command failed' }
);
const result = await gitDiffTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+173
View File
@@ -0,0 +1,173 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: 'abc123 Fix bug (John, 2 days ago)\ndef456 Add feature (Jane, 3 days ago)',
stderr: '',
};
// Mock child_process
vi.mock('child_process', () => ({
exec: vi.fn(),
}));
// Mock util - 返回一个函数,该函数使用外部变量
vi.mock('util', () => ({
promisify: vi.fn(() => vi.fn(async () => {
if (mockExecAsyncResult instanceof Error) {
throw mockExecAsyncResult;
}
return mockExecAsyncResult;
})),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '查看 Git 提交历史'),
}));
import { gitLogTool } from '../../../../src/tools/git/git_log.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitLogTool - Git Log 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: 'abc123 Fix bug (John, 2 days ago)\ndef456 Add feature (Jane, 3 days ago)',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitLogTool.name).toBe('git_log');
});
it('有正确的元数据', () => {
expect(gitLogTool.metadata.category).toBe('git');
expect(gitLogTool.metadata.keywords).toContain('log');
expect(gitLogTool.metadata.keywords).toContain('history');
expect(gitLogTool.metadata.keywords).toContain('commit');
});
it('所有参数都是可选的', () => {
expect(gitLogTool.parameters.limit.required).toBe(false);
expect(gitLogTool.parameters.oneline.required).toBe(false);
expect(gitLogTool.parameters.file.required).toBe(false);
expect(gitLogTool.parameters.author.required).toBe(false);
expect(gitLogTool.parameters.since.required).toBe(false);
expect(gitLogTool.parameters.graph.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功获取提交历史', async () => {
const result = await gitLogTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('abc123');
expect(result.output).toContain('Fix bug');
});
it('使用自定义 limit', async () => {
const result = await gitLogTool.execute({ limit: 5 });
expect(result.success).toBe(true);
});
it('使用 oneline 格式', async () => {
const result = await gitLogTool.execute({ oneline: true });
expect(result.success).toBe(true);
});
it('显示分支图', async () => {
const result = await gitLogTool.execute({ graph: true });
expect(result.success).toBe(true);
});
it('按作者筛选', async () => {
const result = await gitLogTool.execute({ author: 'John' });
expect(result.success).toBe(true);
});
it('按日期筛选', async () => {
const result = await gitLogTool.execute({ since: '2024-01-01' });
expect(result.success).toBe(true);
});
it('查看指定文件的历史', async () => {
const result = await gitLogTool.execute({ file: 'src/index.ts' });
expect(result.success).toBe(true);
});
it('没有提交记录时返回提示', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitLogTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('无提交记录');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许查看历史',
}),
} as any);
const result = await gitLogTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitLogTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('Git 命令失败返回错误', async () => {
// 恢复权限检查
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'fatal: not a git repository' }
);
const result = await gitLogTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+163
View File
@@ -0,0 +1,163 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: 'Already up to date.',
stderr: '',
};
// Mock child_process
vi.mock('child_process', () => ({
exec: vi.fn(),
}));
// Mock util - 返回一个函数,该函数使用外部变量
vi.mock('util', () => ({
promisify: vi.fn(() => vi.fn(async () => {
if (mockExecAsyncResult instanceof Error) {
throw mockExecAsyncResult;
}
return mockExecAsyncResult;
})),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '从远程仓库拉取更新'),
}));
import { gitPullTool } from '../../../../src/tools/git/git_pull.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitPullTool - Git Pull 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: 'Already up to date.',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitPullTool.name).toBe('git_pull');
});
it('有正确的元数据', () => {
expect(gitPullTool.metadata.category).toBe('git');
expect(gitPullTool.metadata.keywords).toContain('pull');
expect(gitPullTool.metadata.keywords).toContain('fetch');
});
it('所有参数都是可选的', () => {
expect(gitPullTool.parameters.remote.required).toBe(false);
expect(gitPullTool.parameters.branch.required).toBe(false);
expect(gitPullTool.parameters.rebase.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功拉取更新', async () => {
const result = await gitPullTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('Already up to date');
});
it('指定远程仓库和分支', async () => {
const result = await gitPullTool.execute({
remote: 'upstream',
branch: 'develop',
});
expect(result.success).toBe(true);
});
it('使用 rebase 模式', async () => {
const result = await gitPullTool.execute({ rebase: true });
expect(result.success).toBe(true);
});
it('合并冲突时返回友好错误', async () => {
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'CONFLICT (content): Merge conflict in file.txt' }
);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('合并冲突');
expect(result.error).toContain('手动解决');
});
it('本地变更时返回友好错误', async () => {
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'error: Your local changes would be overwritten by merge' }
);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('未提交的变更');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许拉取',
}),
} as any);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('Git 命令失败返回错误', async () => {
// 恢复权限检查(因为之前的测试可能修改了 mock)
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'fatal: not a git repository' }
);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+195
View File
@@ -0,0 +1,195 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock child_process
const mockExec = vi.fn();
vi.mock('child_process', () => ({
exec: (cmd: string, opts: any, cb?: Function) => {
if (typeof opts === 'function') {
cb = opts;
}
setImmediate(() => {
const result = mockExec(cmd);
if (result.error) {
const err = result.error;
err.stdout = result.stdout || '';
err.stderr = result.stderr || '';
cb?.(err, result.stdout || '', result.stderr || '');
} else {
cb?.(null, result.stdout || '', result.stderr || '');
}
});
},
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '推送 Git 变更到远程仓库'),
}));
import { gitPushTool } from '../../../../src/tools/git/git_push.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitPushTool - Git Push 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExec.mockReturnValue({
stdout: '',
stderr: 'Everything up-to-date',
});
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitPushTool.name).toBe('git_push');
});
it('有正确的元数据', () => {
expect(gitPushTool.metadata.category).toBe('git');
expect(gitPushTool.metadata.keywords).toContain('push');
expect(gitPushTool.metadata.keywords).toContain('upload');
});
it('所有参数都是可选的', () => {
expect(gitPushTool.parameters.remote.required).toBe(false);
expect(gitPushTool.parameters.branch.required).toBe(false);
expect(gitPushTool.parameters.force.required).toBe(false);
expect(gitPushTool.parameters.set_upstream.required).toBe(false);
expect(gitPushTool.parameters.tags.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功推送', async () => {
const result = await gitPushTool.execute({});
expect(result.success).toBe(true);
// 源代码: stdout || stderr || '推送成功'
expect(result.output).toContain('推送成功');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push origin'));
});
it('指定远程仓库和分支', async () => {
const result = await gitPushTool.execute({
remote: 'upstream',
branch: 'develop',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push upstream develop'));
});
it('设置上游分支', async () => {
const result = await gitPushTool.execute({ set_upstream: true });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push -u'));
});
it('强制推送', async () => {
const result = await gitPushTool.execute({ force: true });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push --force'));
});
it('推送标签', async () => {
const result = await gitPushTool.execute({ tags: true });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push --tags'));
});
it('推送被拒绝时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: '! [rejected] main -> main (fetch first)',
});
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('推送被拒绝');
expect(result.error).toContain('git_pull');
});
it('没有上游分支时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'fatal: The current branch has no upstream branch',
});
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('没有设置上游分支');
expect(result.error).toContain('set_upstream: true');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许推送',
}),
} as any);
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('Git 命令失败返回错误', async () => {
// 恢复权限检查
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'fatal: not a git repository',
});
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+224
View File
@@ -0,0 +1,224 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock child_process
const mockExec = vi.fn();
vi.mock('child_process', () => ({
exec: (cmd: string, opts: any, cb?: Function) => {
if (typeof opts === 'function') {
cb = opts;
}
setImmediate(() => {
const result = mockExec(cmd);
if (result.error) {
const err = result.error;
err.stdout = result.stdout || '';
err.stderr = result.stderr || '';
cb?.(err, result.stdout || '', result.stderr || '');
} else {
cb?.(null, result.stdout || '', result.stderr || '');
}
});
},
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '暂存工作区变更'),
}));
import { gitStashTool } from '../../../../src/tools/git/git_stash.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitStashTool - Git Stash 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExec.mockReturnValue({ stdout: 'Saved working directory', stderr: '' });
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitStashTool.name).toBe('git_stash');
});
it('有正确的元数据', () => {
expect(gitStashTool.metadata.category).toBe('git');
expect(gitStashTool.metadata.keywords).toContain('stash');
expect(gitStashTool.metadata.keywords).toContain('save');
});
it('所有参数都是可选的', () => {
expect(gitStashTool.parameters.action.required).toBe(false);
expect(gitStashTool.parameters.message.required).toBe(false);
expect(gitStashTool.parameters.index.required).toBe(false);
expect(gitStashTool.parameters.include_untracked.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('默认 push 操作暂存变更', async () => {
const result = await gitStashTool.execute({});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash push'));
});
it('带消息暂存', async () => {
const result = await gitStashTool.execute({
message: 'WIP: feature',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('-m'));
});
it('包含未跟踪文件', async () => {
const result = await gitStashTool.execute({
include_untracked: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('-u'));
});
it('列出暂存', async () => {
mockExec.mockReturnValue({
stdout: 'stash@{0}: WIP on main\nstash@{1}: feature',
stderr: '',
});
const result = await gitStashTool.execute({ action: 'list' });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash list'));
});
it('恢复暂存 (pop)', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitStashTool.execute({ action: 'pop' });
expect(result.success).toBe(true);
expect(result.output).toContain('暂存已恢复并删除');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash pop'));
});
it('应用暂存 (apply)', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitStashTool.execute({ action: 'apply' });
expect(result.success).toBe(true);
expect(result.output).toContain('暂存已恢复');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash apply'));
});
it('删除暂存 (drop)', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitStashTool.execute({ action: 'drop', index: 1 });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash drop stash@{1}'));
});
it('清除所有暂存', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitStashTool.execute({ action: 'clear' });
expect(result.success).toBe(true);
expect(result.output).toContain('所有暂存已清除');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash clear'));
});
it('显示暂存内容', async () => {
mockExec.mockReturnValue({ stdout: 'diff --git a/file.ts', stderr: '' });
const result = await gitStashTool.execute({ action: 'show' });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash show -p'));
});
it('未知操作返回错误', async () => {
const result = await gitStashTool.execute({ action: 'unknown' });
expect(result.success).toBe(false);
expect(result.error).toContain('未知操作');
});
it('没有变更时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'No local changes to save',
});
const result = await gitStashTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('没有需要暂存的变更');
});
it('恢复冲突时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'CONFLICT (content): Merge conflict',
});
const result = await gitStashTool.execute({ action: 'pop' });
expect(result.success).toBe(false);
expect(result.error).toContain('冲突');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许暂存操作',
}),
} as any);
const result = await gitStashTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitStashTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
});
});
+139
View File
@@ -0,0 +1,139 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: 'On branch main\nnothing to commit',
stderr: '',
};
// Mock child_process
vi.mock('child_process', () => ({
exec: vi.fn(),
}));
// Mock util - 返回一个函数,该函数使用外部变量
vi.mock('util', () => ({
promisify: vi.fn(() => vi.fn(async () => {
if (mockExecAsyncResult instanceof Error) {
throw mockExecAsyncResult;
}
return mockExecAsyncResult;
})),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '查看 Git 仓库状态'),
}));
import { gitStatusTool } from '../../../../src/tools/git/git_status.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitStatusTool - Git 状态工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: 'On branch main\nnothing to commit',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitStatusTool.name).toBe('git_status');
});
it('有正确的元数据', () => {
expect(gitStatusTool.metadata.category).toBe('git');
expect(gitStatusTool.metadata.keywords).toContain('git');
expect(gitStatusTool.metadata.keywords).toContain('status');
});
it('定义了可选参数', () => {
expect(gitStatusTool.parameters.short).toBeDefined();
expect(gitStatusTool.parameters.short.required).toBe(false);
expect(gitStatusTool.parameters.branch).toBeDefined();
expect(gitStatusTool.parameters.branch.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功获取状态', async () => {
const result = await gitStatusTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('On branch main');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '操作不被允许',
}),
} as any);
const result = await gitStatusTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitStatusTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('非 git 仓库返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stderr: 'fatal: not a git repository', stdout: '' }
);
const result = await gitStatusTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
it('包含 stderr 输出', async () => {
mockExecAsyncResult = {
stdout: 'main output',
stderr: 'warning message',
};
const result = await gitStatusTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('main output');
expect(result.output).toContain('warning message');
});
});
});