bca19b7741
新增测试文件: - agent/executor-extended.test.ts, presets/ - context/manager-extended.test.ts - core/agent.test.ts, providers.test.ts - lsp/cli.test.ts, client-extended.test.ts, index.test.ts - permission/file-prompt.test.ts, prompt.test.ts - skills/builtin/ - tools/filesystem/write_file-extended.test.ts - tools/git/git_commit-extended.test.ts - tools/load_description.test.ts - tools/todo/todo-manager.test.ts - tools/tool-search.test.ts - types/ - utils/config-extended.test.ts, diff-extended.test.ts 修改现有测试: - agent/manager.test.ts - tools/skill/skill.test.ts - utils/config.test.ts, diff.test.ts, image.test.ts
412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||
import { Agent } from '../../../src/core/agent.js';
|
||
import { ToolRegistry } from '../../../src/tools/registry.js';
|
||
import { SessionManager } from '../../../src/session/index.js';
|
||
import type { AgentConfig, Tool } from '../../../src/types/index.js';
|
||
import type { AgentInfo } from '../../../src/agent/types.js';
|
||
|
||
// Mock providers
|
||
vi.mock('../../../src/core/providers.js', () => ({
|
||
getModelFactory: vi.fn(() => {
|
||
return (model: string) => ({
|
||
modelId: `mock:${model}`,
|
||
doGenerate: vi.fn(),
|
||
doStream: vi.fn(),
|
||
});
|
||
}),
|
||
}));
|
||
|
||
// Mock AI SDK
|
||
vi.mock('ai', () => ({
|
||
generateText: vi.fn().mockResolvedValue({
|
||
text: 'Mock response',
|
||
response: { messages: [] },
|
||
steps: [],
|
||
}),
|
||
streamText: vi.fn(() => ({
|
||
textStream: (async function* () {
|
||
yield 'Mock ';
|
||
yield 'streamed ';
|
||
yield 'response';
|
||
})(),
|
||
response: Promise.resolve({ messages: [] }),
|
||
})),
|
||
stepCountIs: vi.fn(() => () => false),
|
||
}));
|
||
|
||
// Mock agent registry and executor
|
||
vi.mock('../../../src/agent/index.js', () => ({
|
||
agentRegistry: {
|
||
get: vi.fn(),
|
||
},
|
||
AgentExecutor: vi.fn().mockImplementation(() => ({
|
||
execute: vi.fn().mockResolvedValue({
|
||
success: true,
|
||
text: 'Vision analysis result',
|
||
steps: 1,
|
||
sessionId: 'test',
|
||
}),
|
||
})),
|
||
}));
|
||
|
||
// Mock vision config
|
||
vi.mock('../../../src/utils/config.js', () => ({
|
||
loadVisionConfig: vi.fn(() => null),
|
||
}));
|
||
|
||
// Create mock tool
|
||
function createMockTool(name: string, category = 'core'): Tool & { metadata?: { deferLoading?: boolean } } {
|
||
return {
|
||
name,
|
||
description: `Mock ${name} tool`,
|
||
parameters: { type: 'object', properties: {}, required: [] },
|
||
execute: vi.fn().mockResolvedValue({ success: true, output: `${name} result` }),
|
||
metadata: { deferLoading: category !== 'core' },
|
||
};
|
||
}
|
||
|
||
// Default test config
|
||
const testConfig: AgentConfig = {
|
||
provider: 'anthropic',
|
||
apiKey: 'test-api-key',
|
||
model: 'claude-3-sonnet',
|
||
systemPrompt: 'You are a helpful assistant.',
|
||
maxTokens: 4096,
|
||
};
|
||
|
||
describe('Agent', () => {
|
||
let agent: Agent;
|
||
let registry: ToolRegistry;
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
registry = new ToolRegistry();
|
||
|
||
// Register some tools
|
||
registry.register(createMockTool('tool_search', 'core') as any);
|
||
registry.register(createMockTool('bash', 'core') as any);
|
||
registry.register(createMockTool('read_file', 'filesystem') as any);
|
||
registry.register(createMockTool('write_file', 'filesystem') as any);
|
||
|
||
agent = new Agent(testConfig);
|
||
agent.setRegistry(registry);
|
||
});
|
||
|
||
describe('constructor', () => {
|
||
it('初始化 Agent 配置', () => {
|
||
const config = agent.getConfig();
|
||
expect(config.provider).toBe('anthropic');
|
||
expect(config.model).toBe('claude-3-sonnet');
|
||
expect(config.systemPrompt).toBe('You are a helpful assistant.');
|
||
});
|
||
|
||
it('支持自定义压缩配置', () => {
|
||
const agentWithCompression = new Agent(testConfig, {
|
||
maxContextTokens: 50000,
|
||
compressionThreshold: 0.7,
|
||
});
|
||
|
||
expect(agentWithCompression.getCompressionManager()).toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe('setRegistry', () => {
|
||
it('设置工具注册表', () => {
|
||
const newAgent = new Agent(testConfig);
|
||
expect(newAgent.getToolCount()).toEqual({ core: 0, discovered: 0, total: 0 });
|
||
|
||
newAgent.setRegistry(registry);
|
||
const counts = newAgent.getToolCount();
|
||
expect(counts.core).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('setSessionManager', () => {
|
||
it('设置会话管理器', async () => {
|
||
// Mock SessionManager
|
||
const mockSessionManager = {
|
||
getSession: vi.fn().mockReturnValue(null),
|
||
setMessages: vi.fn(),
|
||
setDiscoveredTools: vi.fn(),
|
||
newSession: vi.fn(),
|
||
} as unknown as SessionManager;
|
||
|
||
agent.setSessionManager(mockSessionManager);
|
||
expect(agent.getSessionManager()).toBe(mockSessionManager);
|
||
});
|
||
|
||
it('从会话恢复状态', async () => {
|
||
// Mock SessionManager with existing session
|
||
const mockSessionManager = {
|
||
getSession: vi.fn().mockReturnValue({
|
||
id: 'test-session',
|
||
messages: [{ role: 'user', content: 'Hello' }],
|
||
discoveredTools: ['read_file', 'write_file'],
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
}),
|
||
setMessages: vi.fn(),
|
||
setDiscoveredTools: vi.fn(),
|
||
newSession: vi.fn(),
|
||
} as unknown as SessionManager;
|
||
|
||
agent.setSessionManager(mockSessionManager);
|
||
|
||
const counts = agent.getToolCount();
|
||
expect(counts.discovered).toBe(2);
|
||
});
|
||
});
|
||
|
||
describe('getToolCount', () => {
|
||
it('返回工具数量统计', () => {
|
||
const counts = agent.getToolCount();
|
||
expect(counts).toHaveProperty('core');
|
||
expect(counts).toHaveProperty('discovered');
|
||
expect(counts).toHaveProperty('total');
|
||
expect(counts.total).toBe(counts.core + counts.discovered);
|
||
});
|
||
|
||
it('registry 未设置时返回 0', () => {
|
||
const newAgent = new Agent(testConfig);
|
||
const counts = newAgent.getToolCount();
|
||
expect(counts).toEqual({ core: 0, discovered: 0, total: 0 });
|
||
});
|
||
});
|
||
|
||
describe('getHistory', () => {
|
||
it('初始历史为空', () => {
|
||
expect(agent.getHistory()).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('clearHistory', () => {
|
||
it('清空对话历史和发现的工具', async () => {
|
||
// Add some history by calling chat (mocked)
|
||
await agent.chat('Hello');
|
||
|
||
await agent.clearHistory();
|
||
expect(agent.getHistory()).toEqual([]);
|
||
expect(agent.getToolCount().discovered).toBe(0);
|
||
});
|
||
|
||
it('有会话管理器时创建新会话', async () => {
|
||
const mockSessionManager = {
|
||
getSession: vi.fn().mockReturnValue(null),
|
||
setMessages: vi.fn(),
|
||
setDiscoveredTools: vi.fn(),
|
||
newSession: vi.fn().mockResolvedValue(undefined),
|
||
} as unknown as SessionManager;
|
||
|
||
agent.setSessionManager(mockSessionManager);
|
||
await agent.clearHistory();
|
||
|
||
expect(mockSessionManager.newSession).toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('Agent Mode', () => {
|
||
const exploreAgent: AgentInfo = {
|
||
name: 'explore',
|
||
description: '代码探索 Agent',
|
||
mode: 'subagent',
|
||
prompt: 'You are an explore agent.',
|
||
tools: {
|
||
enabled: ['read_file', 'tool_search'],
|
||
noTask: true,
|
||
},
|
||
};
|
||
|
||
it('设置 Agent 模式', () => {
|
||
agent.setAgentMode(exploreAgent);
|
||
expect(agent.getAgentMode()).toBe(exploreAgent);
|
||
expect(agent.getAgentModeName()).toBe('explore');
|
||
});
|
||
|
||
it('切换 Agent 时更新 system prompt', () => {
|
||
agent.setAgentMode(exploreAgent);
|
||
expect(agent.getConfig().systemPrompt).toBe('You are an explore agent.');
|
||
});
|
||
|
||
it('切换回 default 时恢复原始 prompt', () => {
|
||
agent.setAgentMode(exploreAgent);
|
||
agent.setAgentMode(null);
|
||
|
||
expect(agent.getAgentModeName()).toBe('default');
|
||
expect(agent.getConfig().systemPrompt).toBe('You are a helpful assistant.');
|
||
});
|
||
|
||
it('Agent 没有自定义 prompt 时保持原始 prompt', () => {
|
||
const agentWithoutPrompt: AgentInfo = {
|
||
name: 'simple',
|
||
description: 'Simple agent',
|
||
mode: 'subagent',
|
||
};
|
||
|
||
agent.setAgentMode(agentWithoutPrompt);
|
||
expect(agent.getConfig().systemPrompt).toBe('You are a helpful assistant.');
|
||
});
|
||
});
|
||
|
||
describe('supportsVision', () => {
|
||
it('Anthropic Claude-3 支持 vision', () => {
|
||
const claudeAgent = new Agent({
|
||
...testConfig,
|
||
provider: 'anthropic',
|
||
model: 'claude-3-opus',
|
||
});
|
||
expect(claudeAgent.supportsVision()).toBe(true);
|
||
});
|
||
|
||
it('Anthropic Claude-4 支持 vision', () => {
|
||
const claudeAgent = new Agent({
|
||
...testConfig,
|
||
provider: 'anthropic',
|
||
model: 'claude-4-sonnet',
|
||
});
|
||
expect(claudeAgent.supportsVision()).toBe(true);
|
||
});
|
||
|
||
it('OpenAI GPT-4 支持 vision', () => {
|
||
const gptAgent = new Agent({
|
||
...testConfig,
|
||
provider: 'openai',
|
||
model: 'gpt-4-turbo',
|
||
});
|
||
expect(gptAgent.supportsVision()).toBe(true);
|
||
});
|
||
|
||
it('DeepSeek 不支持 vision', () => {
|
||
const deepseekAgent = new Agent({
|
||
...testConfig,
|
||
provider: 'deepseek',
|
||
model: 'deepseek-chat',
|
||
});
|
||
expect(deepseekAgent.supportsVision()).toBe(false);
|
||
});
|
||
|
||
it('旧版 Claude 不支持 vision', () => {
|
||
const oldClaudeAgent = new Agent({
|
||
...testConfig,
|
||
provider: 'anthropic',
|
||
model: 'claude-2',
|
||
});
|
||
expect(oldClaudeAgent.supportsVision()).toBe(false);
|
||
});
|
||
|
||
it('GPT-3.5 不支持 vision', () => {
|
||
const gpt35Agent = new Agent({
|
||
...testConfig,
|
||
provider: 'openai',
|
||
model: 'gpt-3.5-turbo',
|
||
});
|
||
expect(gpt35Agent.supportsVision()).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('getContextUsage', () => {
|
||
it('返回 token 使用情况', () => {
|
||
const usage = agent.getContextUsage();
|
||
expect(usage).toHaveProperty('input');
|
||
expect(usage).toHaveProperty('contextLimit');
|
||
expect(usage).toHaveProperty('available');
|
||
expect(usage).toHaveProperty('usagePercent');
|
||
});
|
||
});
|
||
|
||
describe('getContextUsageFormatted', () => {
|
||
it('返回格式化的使用情况字符串', () => {
|
||
const formatted = agent.getContextUsageFormatted();
|
||
expect(typeof formatted).toBe('string');
|
||
expect(formatted).toContain('k');
|
||
});
|
||
});
|
||
|
||
describe('getCompressionManager', () => {
|
||
it('返回压缩管理器实例', () => {
|
||
const manager = agent.getCompressionManager();
|
||
expect(manager).toBeDefined();
|
||
expect(manager).toHaveProperty('compress');
|
||
expect(manager).toHaveProperty('forceCompress');
|
||
});
|
||
});
|
||
|
||
describe('compactHistory', () => {
|
||
it('手动压缩对话历史', async () => {
|
||
const result = await agent.compactHistory();
|
||
expect(result).toHaveProperty('freedTokens');
|
||
expect(result).toHaveProperty('type');
|
||
});
|
||
});
|
||
|
||
describe('getConfig', () => {
|
||
it('返回配置副本', () => {
|
||
const config = agent.getConfig();
|
||
expect(config).toEqual(testConfig);
|
||
|
||
// 修改返回值不影响原始配置
|
||
config.model = 'modified';
|
||
expect(agent.getConfig().model).toBe('claude-3-sonnet');
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('Agent - getAvailableTools error handling', () => {
|
||
it('registry 未设置时抛出错误', () => {
|
||
const agent = new Agent(testConfig);
|
||
|
||
// 使用 getToolCount 间接测试(因为 getAvailableTools 是 private)
|
||
// 当 registry 为 null 时,getToolCount 返回 0
|
||
const counts = agent.getToolCount();
|
||
expect(counts.total).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('Agent - chat with images', () => {
|
||
let agent: Agent;
|
||
let registry: ToolRegistry;
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
registry = new ToolRegistry();
|
||
registry.register(createMockTool('tool_search', 'core') as any);
|
||
|
||
agent = new Agent(testConfig);
|
||
agent.setRegistry(registry);
|
||
});
|
||
|
||
it('支持 vision 的模型直接处理图片', async () => {
|
||
const visionAgent = new Agent({
|
||
...testConfig,
|
||
model: 'claude-3-opus',
|
||
});
|
||
visionAgent.setRegistry(registry);
|
||
|
||
const result = await visionAgent.chat({
|
||
text: 'What is in this image?',
|
||
images: [
|
||
{ data: 'base64data', mimeType: 'image/png' },
|
||
],
|
||
});
|
||
|
||
expect(result).toBeDefined();
|
||
});
|
||
|
||
it('不支持 vision 时返回错误消息(Vision 未配置)', async () => {
|
||
const deepseekAgent = new Agent({
|
||
...testConfig,
|
||
provider: 'deepseek',
|
||
model: 'deepseek-chat',
|
||
});
|
||
deepseekAgent.setRegistry(registry);
|
||
|
||
const result = await deepseekAgent.chat({
|
||
text: 'What is in this image?',
|
||
images: [
|
||
{ data: 'base64data', mimeType: 'image/png' },
|
||
],
|
||
});
|
||
|
||
expect(result).toContain('无法处理图片');
|
||
});
|
||
});
|