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%
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Config Route 测试
|
||||
*
|
||||
* 测试配置管理 REST API 端点
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Hono } from 'hono';
|
||||
import { configRouter, getConfig, setConfig } from '../../../src/routes/config.js';
|
||||
|
||||
// Create test app
|
||||
const app = new Hono();
|
||||
app.route('/config', configRouter);
|
||||
|
||||
describe('Config Route', () => {
|
||||
beforeEach(() => {
|
||||
// Reset config before each test
|
||||
setConfig({ workdir: '/default/workdir' });
|
||||
});
|
||||
|
||||
describe('GET /config - 获取配置', () => {
|
||||
it('返回当前配置', async () => {
|
||||
setConfig({ workdir: '/test/workdir' });
|
||||
|
||||
const res = await app.request('/config');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data.workdir).toBe('/test/workdir');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /config - 更新配置', () => {
|
||||
it('更新配置', async () => {
|
||||
const res = await app.request('/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workdir: '/new/workdir' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data.workdir).toBe('/new/workdir');
|
||||
});
|
||||
|
||||
it('合并配置而不是完全替换', async () => {
|
||||
setConfig({ workdir: '/original' });
|
||||
|
||||
const res = await app.request('/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workdir: '/updated' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.data.workdir).toBe('/updated');
|
||||
});
|
||||
|
||||
it('无效 JSON 返回 400', async () => {
|
||||
const res = await app.request('/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: 'invalid json',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(json.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /config - 部分更新配置', () => {
|
||||
it('部分更新配置', async () => {
|
||||
setConfig({ workdir: '/original' });
|
||||
|
||||
const res = await app.request('/config', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ workdir: '/patched' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data.workdir).toBe('/patched');
|
||||
});
|
||||
|
||||
it('忽略不存在的字段', async () => {
|
||||
setConfig({ workdir: '/original' });
|
||||
|
||||
const res = await app.request('/config', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ unknownField: 'value' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.data.workdir).toBe('/original');
|
||||
expect(json.data.unknownField).toBeUndefined();
|
||||
});
|
||||
|
||||
it('无效 JSON 返回 400', async () => {
|
||||
const res = await app.request('/config', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: 'invalid json',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(json.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfig / setConfig - 内部函数', () => {
|
||||
it('getConfig 返回当前配置', () => {
|
||||
setConfig({ workdir: '/test' });
|
||||
const config = getConfig();
|
||||
expect(config.workdir).toBe('/test');
|
||||
});
|
||||
|
||||
it('getConfig 返回副本,不影响原配置', () => {
|
||||
setConfig({ workdir: '/original' });
|
||||
const config = getConfig();
|
||||
config.workdir = '/modified';
|
||||
|
||||
const freshConfig = getConfig();
|
||||
expect(freshConfig.workdir).toBe('/original');
|
||||
});
|
||||
|
||||
it('setConfig 合并配置', () => {
|
||||
setConfig({ workdir: '/first' });
|
||||
setConfig({ workdir: '/second' });
|
||||
|
||||
const config = getConfig();
|
||||
expect(config.workdir).toBe('/second');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Context Route 测试
|
||||
*
|
||||
* 测试上下文压缩 REST API 端点
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
// Use vi.hoisted to create mocks before vi.mock is hoisted
|
||||
const { mockExists, mockGetContextUsage, mockCompressContext } = vi.hoisted(() => ({
|
||||
mockExists: vi.fn(),
|
||||
mockGetContextUsage: vi.fn(),
|
||||
mockCompressContext: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn(() => ({
|
||||
exists: mockExists,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/agent/adapter.js', () => ({
|
||||
getContextUsage: mockGetContextUsage,
|
||||
compressContext: mockCompressContext,
|
||||
}));
|
||||
|
||||
import { contextRouter } from '../../../src/routes/context.js';
|
||||
|
||||
// Create test app
|
||||
const app = new Hono();
|
||||
app.route('', contextRouter);
|
||||
|
||||
describe('Context Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /sessions/:id/context - 获取上下文使用情况', () => {
|
||||
it('返回上下文使用信息', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockGetContextUsage.mockReturnValue({
|
||||
input: 50000,
|
||||
contextLimit: 200000,
|
||||
available: 150000,
|
||||
usagePercent: 25,
|
||||
formatted: '50K/150K (25%)',
|
||||
shouldCompress: false,
|
||||
});
|
||||
|
||||
const res = await app.request('/sessions/session-1/context');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data.input).toBe(50000);
|
||||
expect(json.data.usagePercent).toBe(25);
|
||||
});
|
||||
|
||||
it('会话不存在返回 404', async () => {
|
||||
mockExists.mockReturnValue(false);
|
||||
|
||||
const res = await app.request('/sessions/non-existent/context');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.error).toBe('Session not found');
|
||||
});
|
||||
|
||||
it('Agent 未初始化时返回默认值', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockGetContextUsage.mockReturnValue(null);
|
||||
|
||||
const res = await app.request('/sessions/session-1/context');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data.input).toBe(0);
|
||||
expect(json.data.usagePercent).toBe(0);
|
||||
expect(json.data.shouldCompress).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /sessions/:id/compress - 触发手动压缩', () => {
|
||||
it('成功压缩上下文', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockCompressContext.mockResolvedValue({
|
||||
type: 'summarize',
|
||||
freedTokens: 10000,
|
||||
newTokenCount: 40000,
|
||||
});
|
||||
|
||||
const res = await app.request('/sessions/session-1/compress', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ force: false }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data.type).toBe('summarize');
|
||||
expect(json.data.freedTokens).toBe(10000);
|
||||
});
|
||||
|
||||
it('强制压缩', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockCompressContext.mockResolvedValue({
|
||||
type: 'summarize',
|
||||
freedTokens: 20000,
|
||||
newTokenCount: 30000,
|
||||
});
|
||||
|
||||
const res = await app.request('/sessions/session-1/compress', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ force: true }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockCompressContext).toHaveBeenCalledWith('session-1', true);
|
||||
});
|
||||
|
||||
it('无请求体时默认不强制压缩', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockCompressContext.mockResolvedValue({
|
||||
type: 'prune',
|
||||
freedTokens: 5000,
|
||||
});
|
||||
|
||||
const res = await app.request('/sessions/session-1/compress', {
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockCompressContext).toHaveBeenCalledWith('session-1', false);
|
||||
});
|
||||
|
||||
it('会话不存在返回 404', async () => {
|
||||
mockExists.mockReturnValue(false);
|
||||
|
||||
const res = await app.request('/sessions/non-existent/compress', {
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.error).toBe('Session not found');
|
||||
});
|
||||
|
||||
it('Agent 未初始化返回 400', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockCompressContext.mockResolvedValue(null);
|
||||
|
||||
const res = await app.request('/sessions/session-1/compress', {
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.error).toBe('Agent not initialized for this session');
|
||||
});
|
||||
|
||||
it('压缩失败返回 500', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockCompressContext.mockRejectedValue(new Error('Compression failed'));
|
||||
|
||||
const res = await app.request('/sessions/session-1/compress', {
|
||||
method: 'POST',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.error).toBe('Compression failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* Sessions Route 测试
|
||||
*
|
||||
* 测试会话管理 REST API 端点
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
// Use vi.hoisted to create mocks before vi.mock is hoisted
|
||||
const { mockList, mockCreate, mockGet, mockExists, mockDelete, mockGetMessages, mockAddMessage } = vi.hoisted(() => ({
|
||||
mockList: vi.fn(),
|
||||
mockCreate: vi.fn(),
|
||||
mockGet: vi.fn(),
|
||||
mockExists: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
mockGetMessages: vi.fn(),
|
||||
mockAddMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../../src/session/manager.js', () => ({
|
||||
getSessionManager: vi.fn(() => ({
|
||||
list: mockList,
|
||||
create: mockCreate,
|
||||
get: mockGet,
|
||||
exists: mockExists,
|
||||
delete: mockDelete,
|
||||
getMessages: mockGetMessages,
|
||||
addMessage: mockAddMessage,
|
||||
})),
|
||||
}));
|
||||
|
||||
import { sessionsRouter } from '../../../src/routes/sessions.js';
|
||||
|
||||
// Create test app
|
||||
const app = new Hono();
|
||||
app.route('/sessions', sessionsRouter);
|
||||
|
||||
describe('Sessions Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /sessions - 列出会话', () => {
|
||||
it('返回会话列表', async () => {
|
||||
const sessions = [
|
||||
{ id: 'session-1', name: 'Test 1', status: 'idle', createdAt: 1000, updatedAt: 1000 },
|
||||
{ id: 'session-2', name: 'Test 2', status: 'active', createdAt: 2000, updatedAt: 2000 },
|
||||
];
|
||||
mockList.mockReturnValue(sessions);
|
||||
|
||||
const res = await app.request('/sessions');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data).toEqual(sessions);
|
||||
});
|
||||
|
||||
it('空列表返回空数组', async () => {
|
||||
mockList.mockReturnValue([]);
|
||||
|
||||
const res = await app.request('/sessions');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /sessions - 创建会话', () => {
|
||||
it('创建新会话', async () => {
|
||||
const newSession = {
|
||||
id: 'new-session',
|
||||
name: 'My Session',
|
||||
status: 'idle',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
mockCreate.mockResolvedValue(newSession);
|
||||
|
||||
const res = await app.request('/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'My Session' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data).toEqual(newSession);
|
||||
});
|
||||
|
||||
it('无效输入返回 400', async () => {
|
||||
const res = await app.request('/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: 'invalid json',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(json.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /sessions/:id - 获取单个会话', () => {
|
||||
it('返回存在的会话', async () => {
|
||||
const session = { id: 'session-1', name: 'Test', status: 'idle' };
|
||||
mockGet.mockReturnValue(session);
|
||||
|
||||
const res = await app.request('/sessions/session-1');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data).toEqual(session);
|
||||
});
|
||||
|
||||
it('不存在的会话返回 404', async () => {
|
||||
mockGet.mockReturnValue(null);
|
||||
|
||||
const res = await app.request('/sessions/non-existent');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.error).toBe('Session not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /sessions/:id - 删除会话', () => {
|
||||
it('删除存在的会话', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockDelete.mockResolvedValue(true);
|
||||
|
||||
const res = await app.request('/sessions/session-1', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(mockDelete).toHaveBeenCalledWith('session-1');
|
||||
});
|
||||
|
||||
it('不存在的会话返回 404', async () => {
|
||||
mockExists.mockReturnValue(false);
|
||||
|
||||
const res = await app.request('/sessions/non-existent', {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.error).toBe('Session not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /sessions/:id/messages - 获取消息', () => {
|
||||
it('返回会话消息', async () => {
|
||||
const messages = [
|
||||
{ id: 'msg-1', role: 'user', content: 'Hello', timestamp: 1000 },
|
||||
{ id: 'msg-2', role: 'assistant', content: 'Hi!', timestamp: 2000 },
|
||||
];
|
||||
mockExists.mockReturnValue(true);
|
||||
mockGetMessages.mockReturnValue(messages);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data).toEqual(messages);
|
||||
});
|
||||
|
||||
it('不存在的会话返回 404', async () => {
|
||||
mockExists.mockReturnValue(false);
|
||||
|
||||
const res = await app.request('/sessions/non-existent/messages');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.error).toBe('Session not found');
|
||||
});
|
||||
|
||||
it('空消息返回空数组', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockGetMessages.mockReturnValue([]);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages');
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(json.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /sessions/:id/messages - 发送消息', () => {
|
||||
it('添加用户消息', async () => {
|
||||
const message = { id: 'msg-1', role: 'user', content: 'Hello', timestamp: Date.now() };
|
||||
mockExists.mockReturnValue(true);
|
||||
mockAddMessage.mockResolvedValue(message);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'user', content: 'Hello' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(json.success).toBe(true);
|
||||
expect(json.data).toEqual(message);
|
||||
});
|
||||
|
||||
it('添加助手消息', async () => {
|
||||
const message = { id: 'msg-2', role: 'assistant', content: 'Hi!', timestamp: Date.now() };
|
||||
mockExists.mockReturnValue(true);
|
||||
mockAddMessage.mockResolvedValue(message);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'assistant', content: 'Hi!' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(201);
|
||||
expect(json.success).toBe(true);
|
||||
});
|
||||
|
||||
it('不存在的会话返回 404', async () => {
|
||||
mockExists.mockReturnValue(false);
|
||||
|
||||
const res = await app.request('/sessions/non-existent/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'user', content: 'Hello' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(json.success).toBe(false);
|
||||
});
|
||||
|
||||
it('无效输入返回 400', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'invalid', content: 'Hello' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(json.success).toBe(false);
|
||||
});
|
||||
|
||||
it('添加消息失败返回 500', async () => {
|
||||
mockExists.mockReturnValue(true);
|
||||
mockAddMessage.mockResolvedValue(null);
|
||||
|
||||
const res = await app.request('/sessions/session-1/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: 'user', content: 'Hello' }),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
expect(res.status).toBe(500);
|
||||
expect(json.success).toBe(false);
|
||||
expect(json.error).toBe('Failed to add message');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user