5e32375f0e
架构变更: - 采用 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 - 配置管理
289 lines
7.5 KiB
TypeScript
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();
|
|
});
|
|
});
|