Files
ai-terminal-assistant/tests/unit/permission/git-checker.test.ts
T
kurihada 729fb2d42a 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
2025-12-11 14:45:24 +08:00

298 lines
8.4 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { GitPermissionChecker } from '../../../src/permission/checkers/git.js';
import type { GitPermissionContext, PermissionDecision } from '../../../src/permission/types.js';
describe('GitPermissionChecker - Git 权限检查器', () => {
let checker: GitPermissionChecker;
beforeEach(() => {
checker = new GitPermissionChecker();
});
describe('读操作(默认允许)', () => {
const readOperations: GitPermissionContext['operation'][] = [
'status',
'diff',
'log',
'branch_list',
'show',
];
for (const operation of readOperations) {
it(`${operation} 默认允许`, async () => {
const result = await checker.checkGitPermission({ operation });
expect(result.allowed).toBe(true);
expect(result.action).toBe('allow');
});
}
});
describe('写操作(默认询问)', () => {
const writeOperations: GitPermissionContext['operation'][] = [
'add',
'commit',
'push',
'pull',
'checkout',
'branch_create',
'branch_delete',
'stash',
'stash_pop',
'merge',
'rebase',
];
for (const operation of writeOperations) {
it(`${operation} 无回调时需要确认`, async () => {
const result = await checker.checkGitPermission({ operation });
expect(result.action).toBe('ask');
expect(result.needsConfirmation).toBe(true);
});
}
});
describe('危险操作', () => {
it('reset 总是危险操作', async () => {
const result = await checker.checkGitPermission({ operation: 'reset' });
expect(result.action).toBe('ask');
expect(result.needsConfirmation).toBe(true);
});
it('push --force 是危险操作', async () => {
const result = await checker.checkGitPermission({
operation: 'push',
force: true,
});
expect(result.action).toBe('ask');
});
it('checkout --force 是危险操作', async () => {
const result = await checker.checkGitPermission({
operation: 'checkout',
force: true,
});
expect(result.action).toBe('ask');
});
it('rebase --force 是危险操作', async () => {
const result = await checker.checkGitPermission({
operation: 'rebase',
force: true,
});
expect(result.action).toBe('ask');
});
});
describe('回调处理', () => {
it('用户允许时返回允许', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: true,
remember: false,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
const result = await checker.checkGitPermission({ operation: 'commit' });
expect(result.allowed).toBe(true);
expect(mockCallback).toHaveBeenCalled();
});
it('用户拒绝时返回拒绝', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: false,
remember: false,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
const result = await checker.checkGitPermission({ operation: 'push' });
expect(result.allowed).toBe(false);
expect(result.action).toBe('deny');
});
it('remember=true 时记住决定', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: true,
remember: true,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
// 第一次调用
await checker.checkGitPermission({ operation: 'commit' });
// 第二次调用不应该再询问
const secondResult = await checker.checkGitPermission({ operation: 'push' });
expect(secondResult.allowed).toBe(true);
// 第二次不需要回调(因为记住了写操作的权限)
expect(mockCallback).toHaveBeenCalledTimes(1);
});
});
describe('会话权限管理', () => {
it('清除会话权限后重新询问', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: true,
remember: true,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
// 第一次调用并记住
await checker.checkGitPermission({ operation: 'commit' });
// 清除会话权限
checker.clearSessionPermissions();
// 再次调用应该重新询问
await checker.checkGitPermission({ operation: 'commit' });
expect(mockCallback).toHaveBeenCalledTimes(2);
});
it('拒绝决定也被记住', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: false,
remember: true,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
// 第一次拒绝并记住
await checker.checkGitPermission({ operation: 'push' });
// 第二次直接拒绝
const result = await checker.checkGitPermission({ operation: 'commit' });
expect(result.allowed).toBe(false);
expect(mockCallback).toHaveBeenCalledTimes(1);
});
});
describe('配置管理', () => {
it('获取默认配置', () => {
const config = checker.getConfig();
expect(config.readOperations).toBe('allow');
expect(config.writeOperations).toBe('ask');
expect(config.dangerousOperations).toBe('ask');
});
it('更新配置', () => {
checker.setConfig({ writeOperations: 'allow' });
const config = checker.getConfig();
expect(config.writeOperations).toBe('allow');
});
it('配置更改后影响权限检查', async () => {
checker.setConfig({ writeOperations: 'allow' });
const result = await checker.checkGitPermission({ operation: 'commit' });
expect(result.allowed).toBe(true);
});
it('配置危险操作为拒绝', async () => {
checker.setConfig({ dangerousOperations: 'deny' });
const result = await checker.checkGitPermission({ operation: 'reset' });
expect(result.allowed).toBe(false);
expect(result.action).toBe('deny');
});
});
describe('操作描述生成', () => {
it('带 target 的操作描述', async () => {
checker.setAskCallback(vi.fn().mockResolvedValue({ allow: true, remember: false }));
await checker.checkGitPermission({
operation: 'checkout',
target: 'feature-branch',
});
// 验证回调收到正确的描述
const callArg = vi.mocked(checker['askCallback']!).mock.calls[0][0];
expect(callArg.command).toContain('feature-branch');
});
it('带 remote 的操作描述', async () => {
checker.setAskCallback(vi.fn().mockResolvedValue({ allow: true, remember: false }));
await checker.checkGitPermission({
operation: 'push',
remote: 'origin',
});
const callArg = vi.mocked(checker['askCallback']!).mock.calls[0][0];
expect(callArg.command).toContain('origin');
});
it('带 commit message 的操作描述(截断)', async () => {
checker.setAskCallback(vi.fn().mockResolvedValue({ allow: true, remember: false }));
const longMessage = 'a'.repeat(100);
await checker.checkGitPermission({
operation: 'commit',
message: longMessage,
});
const callArg = vi.mocked(checker['askCallback']!).mock.calls[0][0];
expect(callArg.command).toContain('...');
expect(callArg.command.length).toBeLessThan(longMessage.length + 50);
});
it('--force 显示在描述中', async () => {
checker.setAskCallback(vi.fn().mockResolvedValue({ allow: true, remember: false }));
await checker.checkGitPermission({
operation: 'push',
force: true,
});
const callArg = vi.mocked(checker['askCallback']!).mock.calls[0][0];
expect(callArg.command).toContain('--force');
});
});
describe('check 接口(从命令解析)', () => {
it('解析 git status', async () => {
const result = await checker.check({
command: 'git status',
workdir: '/test',
});
expect(result.allowed).toBe(true);
});
it('解析 git_commit', async () => {
const result = await checker.check({
command: 'git_commit',
workdir: '/test',
});
expect(result.action).toBe('ask');
});
it('无法解析的命令返回拒绝', async () => {
const result = await checker.check({
command: 'invalid command',
workdir: '/test',
});
expect(result.allowed).toBe(false);
expect(result.reason).toContain('无法解析');
});
});
});