5835799b69
- 新增 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%
331 lines
9.9 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|