feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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('需要用户确认');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
// 定义可控的 mock 变量
|
||||
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
|
||||
stdout: '[main abc1234] Test commit\n 1 file changed, 1 insertion(+)',
|
||||
stderr: '',
|
||||
};
|
||||
|
||||
let mockPermissionResult = {
|
||||
allowed: true,
|
||||
action: 'allow' as const,
|
||||
reason: undefined as string | undefined,
|
||||
needsConfirmation: false,
|
||||
};
|
||||
|
||||
// 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(async () => mockPermissionResult),
|
||||
})),
|
||||
}));
|
||||
|
||||
// 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\n 1 file changed, 1 insertion(+)',
|
||||
stderr: '',
|
||||
};
|
||||
mockPermissionResult = {
|
||||
allowed: true,
|
||||
action: 'allow',
|
||||
reason: undefined,
|
||||
needsConfirmation: false,
|
||||
};
|
||||
});
|
||||
|
||||
describe('基本提交', () => {
|
||||
it('成功提交变更', async () => {
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'Test commit message',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('Test commit');
|
||||
});
|
||||
|
||||
it('没有 message 返回错误', async () => {
|
||||
const result = await gitCommitTool.execute({});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('提交信息是必填的');
|
||||
});
|
||||
|
||||
it('amend 模式无 message 允许', async () => {
|
||||
const result = await gitCommitTool.execute({
|
||||
amend: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('提交选项', () => {
|
||||
it('使用 -a 选项暂存所有变更', async () => {
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'Auto stage commit',
|
||||
all: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('amend 带 message', async () => {
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'Updated message',
|
||||
amend: true,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('转义 message 中的引号', async () => {
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'Message with "quotes"',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('权限检查', () => {
|
||||
it('权限拒绝返回错误', async () => {
|
||||
mockPermissionResult = {
|
||||
allowed: false,
|
||||
action: 'deny',
|
||||
reason: '不允许提交到此仓库',
|
||||
needsConfirmation: false,
|
||||
};
|
||||
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'Test commit',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('权限被拒绝');
|
||||
expect(result.error).toContain('不允许提交到此仓库');
|
||||
});
|
||||
|
||||
it('需要确认时返回提示', async () => {
|
||||
mockPermissionResult = {
|
||||
allowed: false,
|
||||
action: 'ask',
|
||||
reason: '首次提交',
|
||||
needsConfirmation: true,
|
||||
};
|
||||
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'First commit',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('需要用户确认');
|
||||
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: 'Check context',
|
||||
});
|
||||
|
||||
expect(mockCheck).toHaveBeenCalledWith({
|
||||
operation: 'commit',
|
||||
message: 'Check context',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理', () => {
|
||||
it('没有变更可提交', async () => {
|
||||
mockExecAsyncResult = Object.assign(
|
||||
new Error('nothing to commit, working tree clean'),
|
||||
{ stdout: '', stderr: '', message: 'nothing to commit, working tree clean' }
|
||||
);
|
||||
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'Empty commit',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('没有变更需要提交');
|
||||
expect(result.error).toContain('git_add');
|
||||
});
|
||||
|
||||
it('stderr 包含 nothing to commit', async () => {
|
||||
mockExecAsyncResult = Object.assign(
|
||||
new Error('Command failed'),
|
||||
{ stdout: '', stderr: 'nothing to commit', message: 'Command failed' }
|
||||
);
|
||||
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'Empty commit',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('没有变更需要提交');
|
||||
});
|
||||
|
||||
it('其他 Git 错误', async () => {
|
||||
mockExecAsyncResult = Object.assign(
|
||||
new Error('fatal: not a git repository'),
|
||||
{ stdout: '', stderr: 'fatal: not a git repository', message: 'fatal: not a git repository' }
|
||||
);
|
||||
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'Test commit',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('not a git repository');
|
||||
});
|
||||
|
||||
it('保留 stdout 在错误中', async () => {
|
||||
mockExecAsyncResult = Object.assign(
|
||||
new Error('error'),
|
||||
{ stdout: 'some output', stderr: 'error message', message: 'error' }
|
||||
);
|
||||
|
||||
const result = await gitCommitTool.execute({
|
||||
message: 'Test',
|
||||
});
|
||||
|
||||
expect(result.output).toBe('some output');
|
||||
expect(result.error).toBe('error message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('工具元数据', () => {
|
||||
it('包含正确的名称', () => {
|
||||
expect(gitCommitTool.name).toBe('git_commit');
|
||||
});
|
||||
|
||||
it('包含正确的类别', () => {
|
||||
expect(gitCommitTool.metadata.category).toBe('git');
|
||||
});
|
||||
|
||||
it('包含关键词', () => {
|
||||
expect(gitCommitTool.metadata.keywords).toContain('git');
|
||||
expect(gitCommitTool.metadata.keywords).toContain('commit');
|
||||
expect(gitCommitTool.metadata.keywords).toContain('提交');
|
||||
});
|
||||
|
||||
it('参数定义正确', () => {
|
||||
expect(gitCommitTool.parameters.message.required).toBe(true);
|
||||
expect(gitCommitTool.parameters.amend.required).toBe(false);
|
||||
expect(gitCommitTool.parameters.all.required).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('输出格式', () => {
|
||||
it('包含 stdout', async () => {
|
||||
mockExecAsyncResult = {
|
||||
stdout: 'commit output',
|
||||
stderr: '',
|
||||
};
|
||||
|
||||
const result = await gitCommitTool.execute({ message: 'test' });
|
||||
|
||||
expect(result.output).toBe('commit output');
|
||||
});
|
||||
|
||||
it('包含 stdout 和 stderr', async () => {
|
||||
mockExecAsyncResult = {
|
||||
stdout: 'commit output',
|
||||
stderr: 'warning message',
|
||||
};
|
||||
|
||||
const result = await gitCommitTool.execute({ message: 'test' });
|
||||
|
||||
expect(result.output).toContain('commit output');
|
||||
expect(result.output).toContain('warning message');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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('需要用户确认');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user