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,167 @@
|
||||
/**
|
||||
* Core 模块 Mock 工厂
|
||||
*
|
||||
* 提供 @ai-assistant/core 模块的 mock 实现
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* 创建 Mock Agent 实例
|
||||
*/
|
||||
export function createMockAgent() {
|
||||
return {
|
||||
setRegistry: vi.fn(),
|
||||
chat: vi.fn().mockResolvedValue('mock response'),
|
||||
getToolCount: vi.fn().mockReturnValue({ core: 5, discovered: 0, total: 5 }),
|
||||
getContextUsageFormatted: vi.fn().mockReturnValue('10k / 200k'),
|
||||
getContextUsage: vi.fn().mockReturnValue({
|
||||
input: 10000,
|
||||
contextLimit: 200000,
|
||||
available: 190000,
|
||||
usagePercent: 5,
|
||||
}),
|
||||
compactHistory: vi.fn().mockResolvedValue({ freedTokens: 1000, type: 'prune' }),
|
||||
getCompressionManager: vi.fn().mockReturnValue({
|
||||
shouldCompress: vi.fn().mockReturnValue(false),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock ProviderRegistry
|
||||
*/
|
||||
export function createMockProviderRegistry(overrides: Partial<ReturnType<typeof createMockProviderRegistry>> = {}) {
|
||||
return {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
isInitialized: vi.fn().mockReturnValue(false),
|
||||
list: vi.fn().mockReturnValue([]),
|
||||
listForApi: vi.fn().mockReturnValue([]),
|
||||
get: vi.fn(),
|
||||
getInfo: vi.fn(),
|
||||
getDetail: vi.fn(),
|
||||
has: vi.fn().mockReturnValue(false),
|
||||
getConfig: vi.fn(),
|
||||
setConfig: vi.fn(),
|
||||
getAllConfigs: vi.fn().mockReturnValue({}),
|
||||
getModels: vi.fn().mockReturnValue([]),
|
||||
getModelInfo: vi.fn(),
|
||||
addCustomModel: vi.fn(),
|
||||
removeCustomModel: vi.fn().mockReturnValue(false),
|
||||
testConnection: vi.fn().mockResolvedValue({ success: true }),
|
||||
getModelFactory: vi.fn().mockReturnValue(() => ({})),
|
||||
saveConfig: vi.fn().mockResolvedValue(undefined),
|
||||
reloadConfig: vi.fn().mockResolvedValue(undefined),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock AgentRegistry
|
||||
*/
|
||||
export function createMockAgentRegistry(overrides: Partial<ReturnType<typeof createMockAgentRegistry>> = {}) {
|
||||
return {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
isInitialized: vi.fn().mockReturnValue(false),
|
||||
get: vi.fn(),
|
||||
list: vi.fn().mockReturnValue([]),
|
||||
listSubagents: vi.fn().mockReturnValue([]),
|
||||
listPrimaryAgents: vi.fn().mockReturnValue([]),
|
||||
getInternal: vi.fn(),
|
||||
listInternalAgents: vi.fn().mockReturnValue([]),
|
||||
register: vi.fn(),
|
||||
remove: vi.fn().mockReturnValue(false),
|
||||
has: vi.fn().mockReturnValue(false),
|
||||
size: 0,
|
||||
getNames: vi.fn().mockReturnValue([]),
|
||||
getGlobalConfig: vi.fn().mockReturnValue(null),
|
||||
generateSubagentDescription: vi.fn().mockReturnValue(''),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock PermissionManager
|
||||
*/
|
||||
export function createMockPermissionManager(overrides: Partial<ReturnType<typeof createMockPermissionManager>> = {}) {
|
||||
return {
|
||||
setAskCallback: vi.fn(),
|
||||
checkPermission: vi.fn().mockResolvedValue({ allowed: true }),
|
||||
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
|
||||
checkBashPermission: vi.fn().mockResolvedValue({ allowed: true }),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock ToolRegistry
|
||||
*/
|
||||
export function createMockToolRegistry(overrides: Partial<ReturnType<typeof createMockToolRegistry>> = {}) {
|
||||
return {
|
||||
getCoreTools: vi.fn().mockReturnValue([]),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
get: vi.fn(),
|
||||
has: vi.fn().mockReturnValue(false),
|
||||
register: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建完整的 Core 模块 mock
|
||||
*/
|
||||
export function createMockCoreModule(overrides: {
|
||||
agent?: Partial<ReturnType<typeof createMockAgent>>;
|
||||
providerRegistry?: Partial<ReturnType<typeof createMockProviderRegistry>>;
|
||||
agentRegistry?: Partial<ReturnType<typeof createMockAgentRegistry>>;
|
||||
permissionManager?: Partial<ReturnType<typeof createMockPermissionManager>>;
|
||||
toolRegistry?: Partial<ReturnType<typeof createMockToolRegistry>>;
|
||||
loadConfig?: ReturnType<typeof vi.fn>;
|
||||
saveConfig?: ReturnType<typeof vi.fn>;
|
||||
} = {}) {
|
||||
const mockAgent = { ...createMockAgent(), ...overrides.agent };
|
||||
const mockProviderRegistry = createMockProviderRegistry(overrides.providerRegistry);
|
||||
const mockAgentRegistry = createMockAgentRegistry(overrides.agentRegistry);
|
||||
const mockPermissionManager = createMockPermissionManager(overrides.permissionManager);
|
||||
const mockToolRegistry = createMockToolRegistry(overrides.toolRegistry);
|
||||
|
||||
// 创建一个真正的类来模拟 Agent 构造函数
|
||||
const MockAgentClass = vi.fn().mockImplementation(function (this: any) {
|
||||
Object.assign(this, mockAgent);
|
||||
return this;
|
||||
});
|
||||
|
||||
return {
|
||||
Agent: MockAgentClass,
|
||||
toolRegistry: mockToolRegistry,
|
||||
loadConfig: overrides.loadConfig ?? vi.fn().mockReturnValue({
|
||||
provider: 'anthropic',
|
||||
apiKey: 'test-api-key',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
maxTokens: 4096,
|
||||
systemPrompt: 'test prompt',
|
||||
}),
|
||||
saveConfig: overrides.saveConfig ?? vi.fn(),
|
||||
getPermissionManager: vi.fn().mockReturnValue(mockPermissionManager),
|
||||
getProviderRegistry: vi.fn().mockReturnValue(mockProviderRegistry),
|
||||
agentRegistry: mockAgentRegistry,
|
||||
// 额外暴露内部 mock 以便测试验证
|
||||
_mockAgent: mockAgent,
|
||||
_mockProviderRegistry: mockProviderRegistry,
|
||||
_mockAgentRegistry: mockAgentRegistry,
|
||||
_mockPermissionManager: mockPermissionManager,
|
||||
_mockToolRegistry: mockToolRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建抛出 ConfigurationError 的 loadConfig mock
|
||||
*/
|
||||
export function createConfigurationErrorMock(message: string, provider: string = 'anthropic') {
|
||||
return vi.fn().mockImplementation(() => {
|
||||
const error = new Error(message) as Error & { provider: string };
|
||||
error.name = 'ConfigurationError';
|
||||
error.provider = provider;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Hono 框架 Mock 工厂
|
||||
*
|
||||
* 提供 Hono Context 和 WebSocket Context 的 mock 实现
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* 创建 Mock Hono Context
|
||||
*/
|
||||
export function createMockHonoContext(options: {
|
||||
params?: Record<string, string>;
|
||||
body?: unknown;
|
||||
query?: Record<string, string>;
|
||||
headers?: Record<string, string>;
|
||||
} = {}) {
|
||||
const jsonMock = vi.fn((data: unknown, status?: number) => {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: status ?? 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
});
|
||||
|
||||
const textMock = vi.fn((text: string, status?: number) => {
|
||||
return new Response(text, {
|
||||
status: status ?? 200,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
req: {
|
||||
param: vi.fn((key: string) => options.params?.[key]),
|
||||
json: vi.fn().mockResolvedValue(options.body ?? {}),
|
||||
query: vi.fn((key: string) => options.query?.[key]),
|
||||
header: vi.fn((key: string) => options.headers?.[key.toLowerCase()]),
|
||||
url: 'http://localhost:3000/api/test',
|
||||
method: 'GET',
|
||||
path: '/api/test',
|
||||
},
|
||||
json: jsonMock,
|
||||
text: textMock,
|
||||
status: vi.fn().mockReturnThis(),
|
||||
header: vi.fn().mockReturnThis(),
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === 'sessionId') return options.params?.sessionId;
|
||||
return undefined;
|
||||
}),
|
||||
set: vi.fn(),
|
||||
// 用于测试验证
|
||||
_jsonMock: jsonMock,
|
||||
_textMock: textMock,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock WebSocket Context
|
||||
*/
|
||||
export function createMockWSContext() {
|
||||
const sendMock = vi.fn();
|
||||
const closeMock = vi.fn();
|
||||
|
||||
return {
|
||||
send: sendMock,
|
||||
close: closeMock,
|
||||
readyState: 1, // WebSocket.OPEN
|
||||
// 用于测试验证
|
||||
_sendMock: sendMock,
|
||||
_closeMock: closeMock,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock SSE Stream
|
||||
*/
|
||||
export function createMockSSEStream() {
|
||||
const writeMock = vi.fn();
|
||||
const closeMock = vi.fn();
|
||||
|
||||
return {
|
||||
write: writeMock,
|
||||
close: closeMock,
|
||||
writeSSE: vi.fn((data: { event?: string; data: string; id?: string }) => {
|
||||
writeMock(`event: ${data.event ?? 'message'}\ndata: ${data.data}\n\n`);
|
||||
}),
|
||||
// 用于测试验证
|
||||
_writeMock: writeMock,
|
||||
_closeMock: closeMock,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock Next 函数
|
||||
*/
|
||||
export function createMockNext() {
|
||||
return vi.fn().mockResolvedValue(undefined);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* SessionManager Mock 工厂
|
||||
*
|
||||
* 提供会话管理器的 mock 实现
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest';
|
||||
|
||||
export interface MockSession {
|
||||
id: string;
|
||||
name?: string;
|
||||
status: 'idle' | 'busy';
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
workdir?: string;
|
||||
}
|
||||
|
||||
export interface MockMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Mock SessionManager
|
||||
*/
|
||||
export function createMockSessionManager() {
|
||||
const sessions = new Map<string, MockSession>();
|
||||
const messages = new Map<string, MockMessage[]>();
|
||||
let messageIdCounter = 1;
|
||||
|
||||
const manager = {
|
||||
// 初始化
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
|
||||
// Session CRUD
|
||||
exists: vi.fn((id: string) => sessions.has(id)),
|
||||
|
||||
get: vi.fn((id: string) => sessions.get(id)),
|
||||
|
||||
create: vi.fn((data?: Partial<MockSession>) => {
|
||||
const id = data?.id ?? `session-${Date.now()}`;
|
||||
const session: MockSession = {
|
||||
id,
|
||||
name: data?.name,
|
||||
status: 'idle',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
workdir: data?.workdir ?? process.cwd(),
|
||||
};
|
||||
sessions.set(id, session);
|
||||
messages.set(id, []);
|
||||
return session;
|
||||
}),
|
||||
|
||||
delete: vi.fn(async (id: string) => {
|
||||
const existed = sessions.has(id);
|
||||
sessions.delete(id);
|
||||
messages.delete(id);
|
||||
return existed;
|
||||
}),
|
||||
|
||||
list: vi.fn(() => Array.from(sessions.values())),
|
||||
|
||||
// 消息管理
|
||||
addMessage: vi.fn(async (sessionId: string, msg: { role: 'user' | 'assistant'; content: string }) => {
|
||||
const msgList = messages.get(sessionId) || [];
|
||||
const newMsg: MockMessage = {
|
||||
id: `msg-${messageIdCounter++}`,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
msgList.push(newMsg);
|
||||
messages.set(sessionId, msgList);
|
||||
return newMsg;
|
||||
}),
|
||||
|
||||
getMessages: vi.fn((id: string) => messages.get(id) || []),
|
||||
|
||||
// 状态更新
|
||||
updateStatus: vi.fn((id: string, status: 'idle' | 'busy') => {
|
||||
const session = sessions.get(id);
|
||||
if (session) {
|
||||
session.status = status;
|
||||
session.updatedAt = Date.now();
|
||||
}
|
||||
}),
|
||||
|
||||
updateSessionName: vi.fn(async (id: string, name: string) => {
|
||||
const session = sessions.get(id);
|
||||
if (session) {
|
||||
session.name = name;
|
||||
session.updatedAt = Date.now();
|
||||
return session;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
// 测试辅助方法
|
||||
_addSession: (session: MockSession) => {
|
||||
sessions.set(session.id, session);
|
||||
messages.set(session.id, []);
|
||||
},
|
||||
|
||||
_addMessage: (sessionId: string, message: MockMessage) => {
|
||||
const msgList = messages.get(sessionId) || [];
|
||||
msgList.push(message);
|
||||
messages.set(sessionId, msgList);
|
||||
},
|
||||
|
||||
_clear: () => {
|
||||
sessions.clear();
|
||||
messages.clear();
|
||||
messageIdCounter = 1;
|
||||
},
|
||||
|
||||
_getSessions: () => sessions,
|
||||
_getMessages: () => messages,
|
||||
};
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试用的 Session 数据
|
||||
*/
|
||||
export function createTestSession(overrides: Partial<MockSession> = {}): MockSession {
|
||||
return {
|
||||
id: 'test-session-1',
|
||||
name: 'Test Session',
|
||||
status: 'idle',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
workdir: '/test/workdir',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试用的 Message 数据
|
||||
*/
|
||||
export function createTestMessage(overrides: Partial<MockMessage> = {}): MockMessage {
|
||||
return {
|
||||
id: 'test-msg-1',
|
||||
role: 'user',
|
||||
content: 'Test message',
|
||||
timestamp: Date.now(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user