Files
ai-terminal-assistant/packages/server/tests/unit/routes/providers.test.ts
T
kurihada 1b7d55848d refactor(server): 消除与 Core 的重复类型定义
- 删除 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: 添加类型导入
2025-12-16 20:19:24 +08:00

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);
});
});
});