Files
ai-terminal-assistant/packages/core/tests/unit/agent/config-loader.test.ts
T
kurihada 5e32375f0e 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 - 配置管理
2025-12-12 10:42:20 +08:00

289 lines
7.5 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
loadAgentConfig,
saveAgentConfig,
getConfigTemplate,
} from '../../../src/agent/config-loader.js';
import type { AgentConfigFile } from '../../../src/agent/types.js';
// Mock fs
vi.mock('fs', () => ({
existsSync: vi.fn(),
promises: {
readFile: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn(),
},
}));
// Mock js-yaml
vi.mock('js-yaml', () => ({
load: vi.fn((content: string) => JSON.parse(content)),
dump: vi.fn((obj: unknown) => JSON.stringify(obj, null, 2)),
}));
import * as fs from 'fs';
describe('loadAgentConfig - 加载 Agent 配置', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('无配置文件时返回 null', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const config = await loadAgentConfig('/test/project');
expect(config).toBeNull();
});
it('加载 JSON 配置文件', async () => {
const mockConfig: AgentConfigFile = {
defaults: {
maxSteps: 20,
},
agents: {
'custom-agent': {
description: '自定义 Agent',
mode: 'subagent',
prompt: '你是助手',
},
},
};
vi.mocked(fs.existsSync).mockImplementation((path: unknown) =>
String(path).endsWith('.json')
);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
expect(config?.defaults?.maxSteps).toBe(20);
expect(config?.agents?.['custom-agent']).toBeDefined();
});
it('加载 YAML 配置文件', async () => {
const mockConfig: AgentConfigFile = {
defaults: {
maxSteps: 15,
},
agents: {},
};
vi.mocked(fs.existsSync).mockImplementation((path: unknown) =>
String(path).endsWith('.yaml')
);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
});
it('无效配置格式返回 null', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue('invalid json');
// 会在解析时失败
const config = await loadAgentConfig('/test/project');
// 取决于实现,可能是 null 或抛出错误后 null
expect(config).toBeNull();
});
it('配置搜索顺序', async () => {
// 测试搜索多个路径
const calls: string[] = [];
vi.mocked(fs.existsSync).mockImplementation((path: unknown) => {
calls.push(String(path));
return false;
});
await loadAgentConfig('/test/project');
// 应该搜索多个配置路径
expect(calls.length).toBeGreaterThan(0);
expect(calls.some(p => p.includes('.ai-assist'))).toBe(true);
});
});
describe('saveAgentConfig - 保存 Agent 配置', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fs.existsSync).mockReturnValue(false);
});
it('保存 JSON 格式配置', async () => {
const config: AgentConfigFile = {
defaults: {
maxSteps: 10,
},
agents: {},
};
await saveAgentConfig('/test/project', config, 'json');
expect(fs.promises.mkdir).toHaveBeenCalled();
expect(fs.promises.writeFile).toHaveBeenCalledWith(
expect.stringContaining('agents.json'),
expect.any(String),
'utf-8'
);
});
it('保存 YAML 格式配置', async () => {
const config: AgentConfigFile = {
defaults: {
maxSteps: 10,
},
agents: {},
};
await saveAgentConfig('/test/project', config, 'yaml');
expect(fs.promises.writeFile).toHaveBeenCalledWith(
expect.stringContaining('agents.yaml'),
expect.any(String),
'utf-8'
);
});
it('创建配置目录如果不存在', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
await saveAgentConfig('/test/project', { agents: {} }, 'json');
expect(fs.promises.mkdir).toHaveBeenCalledWith(
expect.stringContaining('.ai-assist'),
{ recursive: true }
);
});
it('目录已存在时不重复创建', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
await saveAgentConfig('/test/project', { agents: {} }, 'json');
expect(fs.promises.mkdir).not.toHaveBeenCalled();
});
});
describe('getConfigTemplate - 获取配置模板', () => {
it('返回有效的配置模板', () => {
const template = getConfigTemplate();
expect(template).toBeDefined();
expect(template.defaults).toBeDefined();
expect(template.agents).toBeDefined();
});
it('模板包含默认配置', () => {
const template = getConfigTemplate();
expect(template.defaults?.maxSteps).toBeDefined();
expect(template.defaults?.model).toBeDefined();
});
it('模板包含示例 Agent', () => {
const template = getConfigTemplate();
expect(template.agents).toBeDefined();
expect(Object.keys(template.agents || {}).length).toBeGreaterThan(0);
});
it('示例 Agent 包含必要字段', () => {
const template = getConfigTemplate();
const agents = template.agents || {};
const firstAgent = Object.values(agents)[0];
expect(firstAgent).toBeDefined();
expect(firstAgent.description).toBeDefined();
expect(firstAgent.mode).toBeDefined();
});
it('模板包含权限配置示例', () => {
const template = getConfigTemplate();
expect(template.defaults?.permission).toBeDefined();
expect(template.defaults?.permission?.bash).toBeDefined();
});
});
describe('配置验证', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('空对象是有效配置', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify({}));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
});
it('只有 defaults 的配置有效', async () => {
const mockConfig = {
defaults: {
maxSteps: 10,
},
};
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
expect(config?.defaults?.maxSteps).toBe(10);
});
it('只有 agents 的配置有效', async () => {
const mockConfig = {
agents: {
'test-agent': {
description: 'Test',
mode: 'subagent',
},
},
};
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
expect(config?.agents?.['test-agent']).toBeDefined();
});
it('defaults 为非对象时配置无效', async () => {
const mockConfig = {
defaults: 'invalid',
};
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
// 应该返回 null(无效配置)
expect(config).toBeNull();
});
it('agents 为非对象时配置无效', async () => {
const mockConfig = {
agents: 'invalid',
};
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).toBeNull();
});
});