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
+322
View File
@@ -0,0 +1,322 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { FilePermissionChecker } from '../../../src/permission/checkers/file.js';
import type { PermissionDecision, FilePermissionContext } from '../../../src/permission/types.js';
// Mock fs 以避免实际文件操作
vi.mock('fs', () => ({
existsSync: vi.fn(() => false),
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
mkdirSync: vi.fn(),
}));
// Mock file-prompt
vi.mock('../../../src/permission/file-prompt.js', () => ({
promptFilePermission: vi.fn().mockResolvedValue({ allow: true, remember: false }),
}));
describe('FilePermissionChecker - 文件权限检查器', () => {
let checker: FilePermissionChecker;
const testProjectRoot = '/test/project';
beforeEach(() => {
checker = new FilePermissionChecker(testProjectRoot);
vi.clearAllMocks();
});
describe('默认配置', () => {
it('加载默认配置', () => {
const config = checker.getConfig();
expect(config.operations).toBeDefined();
expect(config.sensitivePaths).toBeDefined();
expect(config.externalDirectory).toBe('ask');
});
it('读操作默认允许', () => {
const config = checker.getConfig();
expect(config.operations.read).toBe('allow');
expect(config.operations.list).toBe('allow');
expect(config.operations.search).toBe('allow');
expect(config.operations.grep).toBe('allow');
expect(config.operations.info).toBe('allow');
});
it('写操作默认需要确认', () => {
const config = checker.getConfig();
expect(config.operations.write).toBe('ask');
expect(config.operations.edit).toBe('ask');
expect(config.operations.move).toBe('ask');
expect(config.operations.copy).toBe('ask');
expect(config.operations.delete).toBe('ask');
expect(config.operations.mkdir).toBe('ask');
});
});
describe('读操作权限', () => {
const readOperations: FilePermissionContext['operation'][] = [
'read',
'list',
'search',
'grep',
'info',
];
for (const operation of readOperations) {
it(`${operation} 操作在项目内默认允许`, async () => {
const result = await checker.checkFilePermission({
operation,
path: './src/index.ts',
workdir: testProjectRoot,
});
expect(result.allowed).toBe(true);
expect(result.action).toBe('allow');
});
}
});
describe('写操作权限', () => {
const writeOperations: FilePermissionContext['operation'][] = [
'write',
'edit',
'move',
'copy',
'delete',
'mkdir',
];
for (const operation of writeOperations) {
it(`${operation} 操作无回调时需要确认`, async () => {
const result = await checker.checkFilePermission({
operation,
path: './src/new-file.ts',
workdir: testProjectRoot,
});
expect(result.action).toBe('ask');
expect(result.needsConfirmation).toBe(true);
});
}
});
describe('敏感路径检查', () => {
it('系统路径拒绝访问', async () => {
const sensitivePaths = [
'/etc/passwd',
'/usr/bin/node',
'/bin/sh',
'/var/log/syslog',
];
for (const testPath of sensitivePaths) {
const result = await checker.checkFilePermission({
operation: 'read',
path: testPath,
workdir: testProjectRoot,
});
expect(result.allowed).toBe(false);
expect(result.action).toBe('deny');
}
});
it('用户敏感文件需要确认', async () => {
const sensitivePaths = [
'~/.ssh/id_rsa',
'~/.aws/credentials',
'./.env',
];
for (const testPath of sensitivePaths) {
const result = await checker.checkFilePermission({
operation: 'read',
path: testPath,
workdir: testProjectRoot,
});
// 敏感路径会触发 ask
expect(result.action === 'ask' || result.action === 'deny').toBe(true);
}
});
});
describe('外部目录访问', () => {
it('项目外路径需要确认', async () => {
const result = await checker.checkFilePermission({
operation: 'read',
path: '/home/other/file.txt',
workdir: testProjectRoot,
});
// 外部目录会触发 ask 或 deny
expect(result.action === 'ask' || result.action === 'deny').toBe(true);
});
it('项目内路径正常检查', async () => {
const result = await checker.checkFilePermission({
operation: 'read',
path: './src/index.ts',
workdir: testProjectRoot,
});
expect(result.allowed).toBe(true);
});
});
describe('波浪号展开', () => {
it('展开 ~ 为 home 目录', async () => {
const result = await checker.checkFilePermission({
operation: 'read',
path: '~/projects/file.txt',
workdir: testProjectRoot,
});
// 外部路径会触发 ask
expect(result).toBeDefined();
});
});
describe('回调处理', () => {
it('用户允许时返回允许', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: true,
remember: false,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
const result = await checker.checkFilePermission({
operation: 'write',
path: './src/new-file.ts',
workdir: testProjectRoot,
});
expect(result.allowed).toBe(true);
});
it('用户拒绝时返回拒绝', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: false,
remember: false,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
const result = await checker.checkFilePermission({
operation: 'delete',
path: './src/file.ts',
workdir: testProjectRoot,
});
expect(result.allowed).toBe(false);
expect(result.action).toBe('deny');
});
});
describe('会话权限管理', () => {
it('记住允许决定', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: true,
remember: true,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
// 第一次调用
await checker.checkFilePermission({
operation: 'write',
path: './src/test.ts',
workdir: testProjectRoot,
});
// 第二次调用同一操作和路径
const result = await checker.checkFilePermission({
operation: 'write',
path: './src/test.ts',
workdir: testProjectRoot,
});
expect(result.allowed).toBe(true);
// 第二次不应该调用回调
expect(mockCallback).toHaveBeenCalledTimes(1);
});
it('记住拒绝决定', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: false,
remember: true,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
// 第一次调用
await checker.checkFilePermission({
operation: 'delete',
path: './src/important.ts',
workdir: testProjectRoot,
});
// 第二次调用
const result = await checker.checkFilePermission({
operation: 'delete',
path: './src/important.ts',
workdir: testProjectRoot,
});
expect(result.allowed).toBe(false);
expect(mockCallback).toHaveBeenCalledTimes(1);
});
it('清除会话权限后重新询问', async () => {
const mockCallback = vi.fn().mockResolvedValue({
allow: true,
remember: true,
} as PermissionDecision);
checker.setAskCallback(mockCallback);
// 第一次调用
await checker.checkFilePermission({
operation: 'write',
path: './src/test.ts',
workdir: testProjectRoot,
});
// 清除权限
checker.clearSessionPermissions();
// 再次调用
await checker.checkFilePermission({
operation: 'write',
path: './src/test.ts',
workdir: testProjectRoot,
});
// 应该调用两次
expect(mockCallback).toHaveBeenCalledTimes(2);
});
});
describe('check 接口(兼容 PermissionChecker', () => {
it('解析 read 操作', async () => {
const result = await checker.check({
command: 'read ./src/index.ts',
workdir: testProjectRoot,
});
expect(result.allowed).toBe(true);
});
it('解析 write 操作', async () => {
const result = await checker.check({
command: 'write ./src/new.ts',
workdir: testProjectRoot,
});
expect(result.action).toBe('ask');
});
});
});