6342a46e59
问题:Server 端只存储最终文本响应,工具调用的中间消息丢失。 解决方案: - Agent.chat() 返回 ChatResult,包含完整消息链 - Server SessionManager 简化为只管理会话元数据 - 消息 API 改为从 Core Storage 读取 - 移除 Server 端的消息存储和 addMessage 方法 影响范围: - core: Agent.chat() 返回类型变更 - server: SessionManager 接口变更,移除消息存储 - server: GET /sessions/:id/messages 从 Core 读取 - server: 移除 POST /sessions/:id/messages 端点
557 lines
18 KiB
TypeScript
557 lines
18 KiB
TypeScript
/**
|
|
* Agent Adapter 测试
|
|
*
|
|
* 测试 initCore、getOrCreateAgent、配置错误处理等关键功能
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
createMockCoreModule,
|
|
createMockProviderRegistry,
|
|
createMockAgentRegistry,
|
|
createConfigurationErrorMock,
|
|
} from '../../mocks/core.mock.js';
|
|
|
|
// 由于 adapter.ts 使用动态 import,需要特殊处理
|
|
// 我们需要在每个测试中重新导入模块以确保 mock 生效
|
|
|
|
describe('Agent Adapter', () => {
|
|
// 存储原始模块状态
|
|
let originalCwd: typeof process.cwd;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.resetModules();
|
|
originalCwd = process.cwd;
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.cwd = originalCwd;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('initCore - Core 模块初始化', () => {
|
|
it('成功加载 Core 模块时返回 true', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const { initCore } = await import('../../../src/agent/adapter.js');
|
|
const result = await initCore();
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('Core 模块缺少必要导出时返回 false', async () => {
|
|
const incompleteCore = {
|
|
Agent: undefined,
|
|
toolRegistry: undefined,
|
|
loadConfig: undefined,
|
|
};
|
|
|
|
vi.doMock('@ai-assistant/core', () => incompleteCore);
|
|
|
|
const { initCore } = await import('../../../src/agent/adapter.js');
|
|
const result = await initCore();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('Core 模块加载失败时返回 false', async () => {
|
|
vi.doMock('@ai-assistant/core', () => {
|
|
throw new Error('Module not found');
|
|
});
|
|
|
|
const { initCore } = await import('../../../src/agent/adapter.js');
|
|
const result = await initCore();
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it('调用 ProviderRegistry.init()', async () => {
|
|
const mockProviderRegistry = createMockProviderRegistry({
|
|
isInitialized: vi.fn().mockReturnValue(false),
|
|
});
|
|
const mockCore = createMockCoreModule({
|
|
providerRegistry: mockProviderRegistry,
|
|
});
|
|
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const { initCore } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
expect(mockCore.getProviderRegistry().init).toHaveBeenCalled();
|
|
});
|
|
|
|
it('ProviderRegistry 已初始化时不重复调用 init()', async () => {
|
|
const mockProviderRegistry = createMockProviderRegistry({
|
|
isInitialized: vi.fn().mockReturnValue(true),
|
|
});
|
|
const mockCore = createMockCoreModule({
|
|
providerRegistry: mockProviderRegistry,
|
|
});
|
|
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const { initCore } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
expect(mockCore.getProviderRegistry().init).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('调用 AgentRegistry.init() 并传入 process.cwd()', async () => {
|
|
const testCwd = '/test/workdir';
|
|
process.cwd = vi.fn().mockReturnValue(testCwd);
|
|
|
|
const mockAgentRegistry = createMockAgentRegistry({
|
|
isInitialized: vi.fn().mockReturnValue(false),
|
|
});
|
|
const mockCore = createMockCoreModule({
|
|
agentRegistry: mockAgentRegistry,
|
|
});
|
|
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const { initCore } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
expect(mockCore.agentRegistry.init).toHaveBeenCalledWith(testCwd);
|
|
});
|
|
|
|
it('AgentRegistry 已初始化时不重复调用 init()', async () => {
|
|
const mockAgentRegistry = createMockAgentRegistry({
|
|
isInitialized: vi.fn().mockReturnValue(true),
|
|
});
|
|
const mockCore = createMockCoreModule({
|
|
agentRegistry: mockAgentRegistry,
|
|
});
|
|
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const { initCore } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
expect(mockCore.agentRegistry.init).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('isCoreAvailable - Core 模块可用性检查', () => {
|
|
it('Core 未初始化时返回 false', async () => {
|
|
vi.resetModules();
|
|
const { isCoreAvailable } = await import('../../../src/agent/adapter.js');
|
|
expect(isCoreAvailable()).toBe(false);
|
|
});
|
|
|
|
it('Core 初始化后返回 true', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const { initCore, isCoreAvailable } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
expect(isCoreAvailable()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('getOrCreateAgent - Agent 创建与缓存', () => {
|
|
it('Core 未初始化时返回 null', async () => {
|
|
vi.resetModules();
|
|
const { getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
|
const agent = getOrCreateAgent('session-1');
|
|
expect(agent).toBeNull();
|
|
});
|
|
|
|
it('成功创建 Agent 实例', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
// Mock session manager
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue({
|
|
exists: vi.fn().mockReturnValue(true),
|
|
get: vi.fn().mockReturnValue({ id: 'session-1' }),
|
|
}),
|
|
}));
|
|
|
|
// Mock permission handler
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const { initCore, getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
const agent = getOrCreateAgent('session-1');
|
|
|
|
expect(agent).not.toBeNull();
|
|
expect(mockCore.Agent).toHaveBeenCalled();
|
|
});
|
|
|
|
it('重复获取同一 session 返回缓存的 Agent', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue({
|
|
exists: vi.fn().mockReturnValue(true),
|
|
}),
|
|
}));
|
|
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const { initCore, getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
const agent1 = getOrCreateAgent('session-1');
|
|
const agent2 = getOrCreateAgent('session-1');
|
|
|
|
expect(agent1).toBe(agent2);
|
|
// Agent 构造函数只应被调用一次
|
|
expect(mockCore.Agent).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('ConfigurationError 时返回 null', async () => {
|
|
const mockCore = createMockCoreModule({
|
|
loadConfig: createConfigurationErrorMock('未配置 API Key', 'deepseek'),
|
|
});
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue({
|
|
exists: vi.fn().mockReturnValue(true),
|
|
}),
|
|
}));
|
|
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const { initCore, getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
const agent = getOrCreateAgent('session-1');
|
|
|
|
expect(agent).toBeNull();
|
|
});
|
|
|
|
it('非 ConfigurationError 时重新抛出', async () => {
|
|
const mockCore = createMockCoreModule({
|
|
loadConfig: vi.fn().mockImplementation(() => {
|
|
throw new Error('Other error');
|
|
}),
|
|
});
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue({
|
|
exists: vi.fn().mockReturnValue(true),
|
|
}),
|
|
}));
|
|
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const { initCore, getOrCreateAgent } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
expect(() => getOrCreateAgent('session-1')).toThrow('Other error');
|
|
});
|
|
});
|
|
|
|
describe('destroyAgent - Agent 销毁', () => {
|
|
it('从缓存中移除 Agent', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue({
|
|
exists: vi.fn().mockReturnValue(true),
|
|
}),
|
|
}));
|
|
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const { initCore, getOrCreateAgent, destroyAgent } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
// 创建 Agent
|
|
getOrCreateAgent('session-1');
|
|
expect(mockCore.Agent).toHaveBeenCalledTimes(1);
|
|
|
|
// 销毁 Agent
|
|
destroyAgent('session-1');
|
|
|
|
// 再次获取应创建新 Agent
|
|
getOrCreateAgent('session-1');
|
|
expect(mockCore.Agent).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe('getAgentStats - Agent 统计信息', () => {
|
|
it('Core 未初始化时返回 available: false', async () => {
|
|
vi.resetModules();
|
|
const { getAgentStats } = await import('../../../src/agent/adapter.js');
|
|
const stats = getAgentStats('session-1');
|
|
expect(stats).toEqual({ available: false });
|
|
});
|
|
|
|
it('Agent 不存在时返回 available: true 但无其他信息', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const { initCore, getAgentStats } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
const stats = getAgentStats('non-existent-session');
|
|
expect(stats).toEqual({ available: true });
|
|
});
|
|
|
|
it('Agent 存在时返回完整统计信息', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue({
|
|
exists: vi.fn().mockReturnValue(true),
|
|
}),
|
|
}));
|
|
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const { initCore, getOrCreateAgent, getAgentStats } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
// 创建 Agent
|
|
getOrCreateAgent('session-1');
|
|
|
|
const stats = getAgentStats('session-1');
|
|
expect(stats.available).toBe(true);
|
|
expect(stats.toolCount).toEqual({ core: 5, discovered: 0, total: 5 });
|
|
expect(stats.contextUsage).toBe('10k / 200k');
|
|
});
|
|
});
|
|
|
|
describe('cancelProcessing - 取消处理', () => {
|
|
it('更新会话状态为 idle', async () => {
|
|
const mockSessionManager = {
|
|
exists: vi.fn().mockReturnValue(true),
|
|
updateStatus: vi.fn(),
|
|
};
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue(mockSessionManager),
|
|
}));
|
|
|
|
vi.doMock('../../../src/sse.js', () => ({
|
|
emitStatusEvent: vi.fn(),
|
|
emitLogEvent: vi.fn(),
|
|
}));
|
|
|
|
const { cancelProcessing } = await import('../../../src/agent/adapter.js');
|
|
cancelProcessing('session-1');
|
|
|
|
expect(mockSessionManager.updateStatus).toHaveBeenCalledWith('session-1', 'idle');
|
|
});
|
|
});
|
|
|
|
describe('getContextUsage - 获取上下文使用情况', () => {
|
|
it('Core 未初始化时返回 null', async () => {
|
|
vi.resetModules();
|
|
const { getContextUsage } = await import('../../../src/agent/adapter.js');
|
|
const usage = getContextUsage('session-1');
|
|
expect(usage).toBeNull();
|
|
});
|
|
|
|
it('Agent 不存在时返回 null', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const { initCore, getContextUsage } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
const usage = getContextUsage('non-existent-session');
|
|
expect(usage).toBeNull();
|
|
});
|
|
|
|
it('Agent 存在时返回使用情况', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue({
|
|
exists: vi.fn().mockReturnValue(true),
|
|
}),
|
|
}));
|
|
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const { initCore, getOrCreateAgent, getContextUsage } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
getOrCreateAgent('session-1');
|
|
|
|
const usage = getContextUsage('session-1');
|
|
expect(usage).not.toBeNull();
|
|
expect(usage?.formatted).toBe('10k / 200k');
|
|
expect(usage?.input).toBe(10000);
|
|
});
|
|
});
|
|
|
|
describe('compressContext - 上下文压缩', () => {
|
|
it('Core 未初始化时返回 null', async () => {
|
|
vi.resetModules();
|
|
const { compressContext } = await import('../../../src/agent/adapter.js');
|
|
const result = await compressContext('session-1');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('Agent 不存在时返回 null', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const { initCore, compressContext } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
const result = await compressContext('non-existent-session');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('压缩成功时返回结果', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue({
|
|
exists: vi.fn().mockReturnValue(true),
|
|
}),
|
|
}));
|
|
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const { initCore, getOrCreateAgent, compressContext } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
getOrCreateAgent('session-1');
|
|
|
|
const result = await compressContext('session-1');
|
|
expect(result).not.toBeNull();
|
|
expect(result?.type).toBe('prune');
|
|
expect(result?.freedTokens).toBe(1000);
|
|
});
|
|
|
|
it('压缩失败时返回错误信息', async () => {
|
|
const mockCore = createMockCoreModule({
|
|
agent: {
|
|
compactHistory: vi.fn().mockRejectedValue(new Error('Compression failed')),
|
|
},
|
|
});
|
|
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue({
|
|
exists: vi.fn().mockReturnValue(true),
|
|
}),
|
|
}));
|
|
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const { initCore, getOrCreateAgent, compressContext } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
|
|
getOrCreateAgent('session-1');
|
|
|
|
const result = await compressContext('session-1');
|
|
expect(result).not.toBeNull();
|
|
expect(result?.status).toBe('failed_error');
|
|
expect(result?.error).toBe('Compression failed');
|
|
});
|
|
});
|
|
|
|
describe('processMessage - 消息处理', () => {
|
|
it('Agent 不可用时发送占位消息', async () => {
|
|
const mockSessionManager = {
|
|
exists: vi.fn().mockReturnValue(true),
|
|
updateStatus: vi.fn(),
|
|
};
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue(mockSessionManager),
|
|
}));
|
|
|
|
const mockBroadcast = vi.fn();
|
|
vi.doMock('../../../src/ws.js', () => ({
|
|
broadcastToSession: mockBroadcast,
|
|
}));
|
|
|
|
vi.doMock('../../../src/sse.js', () => ({
|
|
emitStatusEvent: vi.fn(),
|
|
emitLogEvent: vi.fn(),
|
|
}));
|
|
|
|
// 不初始化 core,使 agent 为 null
|
|
vi.resetModules();
|
|
const { processMessage } = await import('../../../src/agent/adapter.js');
|
|
await processMessage('session-1', 'Hello');
|
|
|
|
// 应该发送占位消息
|
|
expect(mockBroadcast).toHaveBeenCalledWith('session-1', expect.objectContaining({
|
|
type: 'chunk',
|
|
}));
|
|
});
|
|
|
|
it('成功处理消息并返回响应', async () => {
|
|
const mockCore = createMockCoreModule();
|
|
vi.doMock('@ai-assistant/core', () => mockCore);
|
|
|
|
const mockSessionManager = {
|
|
exists: vi.fn().mockReturnValue(true),
|
|
get: vi.fn().mockReturnValue({ id: 'session-1', name: 'Test' }),
|
|
updateStatus: vi.fn(),
|
|
updateMessageCount: vi.fn(),
|
|
};
|
|
|
|
vi.doMock('../../../src/session/manager.js', () => ({
|
|
getSessionManager: vi.fn().mockReturnValue(mockSessionManager),
|
|
}));
|
|
|
|
vi.doMock('../../../src/permission/handler.js', () => ({
|
|
createServerPermissionCallback: vi.fn().mockReturnValue(async () => ({ allow: true })),
|
|
}));
|
|
|
|
const mockBroadcast = vi.fn();
|
|
vi.doMock('../../../src/ws.js', () => ({
|
|
broadcastToSession: mockBroadcast,
|
|
}));
|
|
|
|
vi.doMock('../../../src/sse.js', () => ({
|
|
emitStatusEvent: vi.fn(),
|
|
emitLogEvent: vi.fn(),
|
|
}));
|
|
|
|
const { initCore, processMessage } = await import('../../../src/agent/adapter.js');
|
|
await initCore();
|
|
await processMessage('session-1', 'Hello');
|
|
|
|
// 应该发送 done 消息
|
|
expect(mockBroadcast).toHaveBeenCalledWith('session-1', expect.objectContaining({
|
|
type: 'done',
|
|
}));
|
|
});
|
|
});
|
|
});
|