1b7d55848d
- 删除 Server 中 60+ 个与 Core 重复的类型定义 - 将动态导入 (await import) 改为静态类型导入 (import type) - 保留必要的运行时静态导入 - 修复测试文件中的 mock 初始化问题 - 净删除约 960 行重复代码 重构文件: - routes/checkpoints.ts: 删除 155 行重复类型 - routes/agents.ts: 删除 93 行重复类型 - routes/commands.ts: 删除 83 行重复类型 - routes/mcp.ts: 修复类型窄化 - routes/hooks.ts: 已使用静态导入 - routes/providers.ts: 删除 63 行重复类型 - session/manager.ts: 删除 41 行重复类型 - routes/sessions.ts: 添加类型导入 - permission/handler.ts: 添加类型导入
322 lines
9.7 KiB
TypeScript
322 lines
9.7 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),
|
|
}));
|
|
|
|
vi.mock('@ai-assistant/core', () => 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();
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|