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,288 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user