Files
ai-terminal-assistant/packages/server/tests/unit/routes/providers.test.ts
T
kurihada 5835799b69 test(server): 添加 server 模块单元测试
- 新增 vitest 配置和测试基础设施
- 添加 adapter.test.ts: 测试 Core 模块初始化和 Agent 管理 (18 tests)
- 添加 token.test.ts: 测试 Token 生成、验证和中间件 (25 tests)
- 添加 handler.test.ts: 测试权限处理器 (18 tests)
- 添加 ws.test.ts: 测试 WebSocket 连接和消息处理 (19 tests)
- 添加 sse.test.ts: 测试 SSE 事件发送 (14 tests)
- 添加 sessions.test.ts: 测试会话路由 (16 tests)
- 添加 config.test.ts: 测试配置路由 (10 tests)
- 添加 context.test.ts: 测试上下文压缩路由 (9 tests)
- 添加 providers.test.ts: 测试 Provider 管理路由 (18 tests)
- 添加 manager.test.ts: 测试 SessionManager (48 tests)

总计 195 个测试,覆盖率从 0% 提升至 29.59%
2025-12-15 00:07:32 +08:00

331 lines
9.9 KiB
TypeScript

/**
* Providers Route 测试
*
* 测试模型提供商管理 REST API 端点
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Hono } from 'hono';
// Use vi.hoisted to create mocks
const mockRegistry = vi.hoisted(() => ({
listForApi: vi.fn(),
getDetail: vi.fn(),
getModels: vi.fn(),
has: vi.fn(),
testConnection: vi.fn(),
registerCustom: vi.fn(),
setConfig: vi.fn(),
saveConfig: vi.fn(),
removeCustom: vi.fn(),
addCustomModel: vi.fn(),
removeCustomModel: vi.fn(),
}));
const mockCoreModule = vi.hoisted(() => ({
getProviderRegistry: vi.fn(() => mockRegistry),
}));
// Track if core module should be available
let coreModuleAvailable = true;
vi.mock('@ai-assistant/core', () => {
if (!coreModuleAvailable) {
throw new Error('Module not found');
}
return mockCoreModule;
});
import { providersRouter } from '../../../src/routes/providers.js';
// Create test app
const app = new Hono();
app.route('/providers', providersRouter);
describe('Providers Route', () => {
beforeEach(() => {
vi.clearAllMocks();
coreModuleAvailable = true;
});
describe('GET /providers - 列出所有提供商', () => {
it('返回提供商列表', async () => {
const providers = [
{ id: 'anthropic', name: 'Anthropic', builtin: true, enabled: true, hasApiKey: true, modelCount: 5 },
{ id: 'openai', name: 'OpenAI', builtin: true, enabled: false, hasApiKey: false, modelCount: 10 },
];
mockRegistry.listForApi.mockReturnValue(providers);
const res = await app.request('/providers');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data).toEqual(providers);
});
it('返回空列表', async () => {
mockRegistry.listForApi.mockReturnValue([]);
const res = await app.request('/providers');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.data).toEqual([]);
});
});
describe('GET /providers/:id - 获取提供商详情', () => {
it('返回提供商详情', async () => {
const detail = {
id: 'anthropic',
name: 'Anthropic',
builtin: true,
models: [{ id: 'claude-3', name: 'Claude 3' }],
config: { enabled: true, hasApiKey: true },
};
mockRegistry.getDetail.mockReturnValue(detail);
const res = await app.request('/providers/anthropic');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data).toEqual(detail);
});
it('提供商不存在返回 404', async () => {
mockRegistry.getDetail.mockReturnValue(undefined);
const res = await app.request('/providers/non-existent');
const json = await res.json();
expect(res.status).toBe(404);
expect(json.success).toBe(false);
expect(json.error).toContain('not found');
});
});
describe('GET /providers/:id/models - 获取模型列表', () => {
it('返回模型列表', async () => {
const models = [
{ id: 'claude-3-opus', name: 'Claude 3 Opus', contextWindow: 200000 },
{ id: 'claude-3-sonnet', name: 'Claude 3 Sonnet', contextWindow: 200000 },
];
mockRegistry.has.mockReturnValue(true);
mockRegistry.getModels.mockReturnValue(models);
const res = await app.request('/providers/anthropic/models');
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data).toEqual(models);
});
it('提供商不存在返回 404', async () => {
mockRegistry.has.mockReturnValue(false);
const res = await app.request('/providers/non-existent/models');
const json = await res.json();
expect(res.status).toBe(404);
expect(json.success).toBe(false);
});
});
describe('POST /providers/:id/test - 测试连接', () => {
it('测试连接成功', async () => {
mockRegistry.testConnection.mockResolvedValue({
success: true,
latency: 150,
});
const res = await app.request('/providers/anthropic/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey: 'test-key' }),
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data.success).toBe(true);
expect(json.data.latency).toBe(150);
});
it('测试连接失败', async () => {
mockRegistry.testConnection.mockResolvedValue({
success: false,
error: 'Invalid API key',
});
const res = await app.request('/providers/anthropic/test', {
method: 'POST',
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json.data.success).toBe(false);
expect(json.data.error).toBe('Invalid API key');
});
});
describe('POST /providers - 注册自定义提供商', () => {
it('注册成功', async () => {
mockRegistry.registerCustom.mockReturnValue(undefined);
mockRegistry.saveConfig.mockResolvedValue(undefined);
const res = await app.request('/providers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: 'custom-provider',
name: 'Custom Provider',
baseUrl: 'https://api.custom.com',
}),
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(mockRegistry.registerCustom).toHaveBeenCalled();
expect(mockRegistry.saveConfig).toHaveBeenCalled();
});
it('缺少必需字段返回 400', async () => {
const res = await app.request('/providers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 'custom' }),
});
const json = await res.json();
expect(res.status).toBe(400);
expect(json.success).toBe(false);
expect(json.error).toContain('Missing required fields');
});
});
describe('PUT /providers/:id - 更新配置', () => {
it('更新成功', async () => {
mockRegistry.has.mockReturnValue(true);
mockRegistry.setConfig.mockReturnValue(undefined);
mockRegistry.saveConfig.mockResolvedValue(undefined);
const res = await app.request('/providers/anthropic', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: true, apiKey: 'new-key' }),
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(mockRegistry.setConfig).toHaveBeenCalled();
});
it('提供商不存在返回 404', async () => {
mockRegistry.has.mockReturnValue(false);
const res = await app.request('/providers/non-existent', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: true }),
});
const json = await res.json();
expect(res.status).toBe(404);
expect(json.success).toBe(false);
});
});
describe('DELETE /providers/:id - 删除自定义提供商', () => {
it('删除成功', async () => {
mockRegistry.removeCustom.mockReturnValue(true);
mockRegistry.saveConfig.mockResolvedValue(undefined);
const res = await app.request('/providers/custom-provider', {
method: 'DELETE',
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
});
it('提供商不存在返回 404', async () => {
mockRegistry.removeCustom.mockReturnValue(false);
const res = await app.request('/providers/non-existent', {
method: 'DELETE',
});
const json = await res.json();
expect(res.status).toBe(404);
expect(json.success).toBe(false);
});
});
describe('POST /providers/:id/models - 添加自定义模型', () => {
it('添加成功', async () => {
mockRegistry.addCustomModel.mockReturnValue(undefined);
mockRegistry.saveConfig.mockResolvedValue(undefined);
const res = await app.request('/providers/anthropic/models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: 'custom-model',
name: 'Custom Model',
contextWindow: 100000,
}),
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
expect(mockRegistry.addCustomModel).toHaveBeenCalledWith('anthropic', expect.any(Object));
});
it('缺少必需字段返回 400', async () => {
const res = await app.request('/providers/anthropic/models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 'model' }),
});
const json = await res.json();
expect(res.status).toBe(400);
expect(json.error).toContain('Missing required fields');
});
});
describe('DELETE /providers/:id/models/:modelId - 删除自定义模型', () => {
it('删除成功', async () => {
mockRegistry.removeCustomModel.mockReturnValue(true);
mockRegistry.saveConfig.mockResolvedValue(undefined);
const res = await app.request('/providers/anthropic/models/custom-model', {
method: 'DELETE',
});
const json = await res.json();
expect(res.status).toBe(200);
expect(json.success).toBe(true);
});
it('模型不存在返回 404', async () => {
mockRegistry.removeCustomModel.mockReturnValue(false);
const res = await app.request('/providers/anthropic/models/non-existent', {
method: 'DELETE',
});
const json = await res.json();
expect(res.status).toBe(404);
expect(json.success).toBe(false);
});
});
});