初始化

This commit is contained in:
2026-02-22 18:55:40 +08:00
commit 8392cdd861
496 changed files with 45020 additions and 0 deletions

View File

@@ -0,0 +1,397 @@
package com.easyagents.mcp.client;
import com.easyagents.core.message.ToolCall;
import com.easyagents.core.model.chat.tool.Tool;
import com.easyagents.core.model.chat.tool.ToolExecutor;
import io.modelcontextprotocol.client.McpSyncClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* McpClientManager unit test class
* McpClientManager单元测试类
*/
class McpClientManagerTest {
@Mock
private McpClientDescriptor mockDescriptor;
@Mock
private McpSyncClient mockClient;
@Mock
private Tool mockTool;
private McpClientManager mcpClientManager;
private AutoCloseable mockitoMocks;
// Test JSON configuration
// 测试用JSON配置
private static final String TEST_JSON_CONFIG = """
{
"mcpServers": {
"test-server": {
"transport": "stdio",
"env": {
"TEST_VAR": "test_value",
"INPUT_VAR": "${input:test_input}"
}
}
}
}
""";
@BeforeEach
void setUp() {
// 初始化Mockito mocks
mockitoMocks = MockitoAnnotations.openMocks(this);
// Reset singleton instance before each test
// 在每个测试前重置单例实例
try {
java.lang.reflect.Field instance = McpClientManager.class.getDeclaredField("INSTANCE");
instance.setAccessible(true);
instance.set(null, null);
} catch (Exception e) {
throw new RuntimeException(e);
}
mcpClientManager = McpClientManager.getInstance();
}
@AfterEach
void tearDown() throws Exception {
if (mcpClientManager != null) {
mcpClientManager.close();
}
// 关闭Mockito mocks
if (mockitoMocks != null) {
mockitoMocks.close();
}
}
@Test
@DisplayName("Test singleton pattern - 单例模式测试")
void testSingletonPattern() {
McpClientManager instance1 = McpClientManager.getInstance();
McpClientManager instance2 = McpClientManager.getInstance();
assertSame(instance1, instance2, "Should return same instance for singleton pattern");
assertNotNull(instance1, "Instance should not be null");
}
@Test
@DisplayName("Test registerFromJson - JSON配置注册测试")
void testRegisterFromJson() {
// Mock descriptor behavior
// 模拟描述符行为
when(mockDescriptor.getName()).thenReturn("test-server");
when(mockDescriptor.isAlive()).thenReturn(true);
when(mockDescriptor.getClient()).thenReturn(mockClient);
when(mockDescriptor.getMcpTool("test-tool")).thenReturn(mockTool);
// Use reflection to replace descriptor in registry
// 使用反射替换注册表中的描述符
try {
java.lang.reflect.Field registryField = McpClientManager.class.getDeclaredField("descriptorRegistry");
registryField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.Map<String, McpClientDescriptor> registry =
(java.util.Map<String, McpClientDescriptor>) registryField.get(mcpClientManager);
registry.put("test-server", mockDescriptor);
} catch (Exception e) {
throw new RuntimeException(e);
}
// Test client retrieval
// 测试客户端获取
McpSyncClient client = mcpClientManager.getMcpClient("test-server");
assertNotNull(client, "Client should not be null");
assertSame(mockClient, client, "Should return the mocked client");
// Test tool retrieval
// 测试工具获取
Tool tool = mcpClientManager.getMcpTool("test-server", "test-tool");
assertNotNull(tool, "Tool should not be null");
assertSame(mockTool, tool, "Should return the mocked tool");
// Test online status
// 测试在线状态
assertTrue(mcpClientManager.isClientOnline("test-server"), "Client should be online");
}
@Test
@DisplayName("Test getClient with non-existent client - 获取不存在客户端测试")
void testGetMcpClientNonExistent() {
assertThrows(IllegalArgumentException.class, () -> {
mcpClientManager.getMcpClient("non-existent-server");
}, "Should throw IllegalArgumentException for non-existent client");
}
@Test
@DisplayName("Test getMcpTool with non-existent client - 获取不存在客户端工具测试")
void testGetMcpToolNonExistent() {
assertThrows(IllegalArgumentException.class, () -> {
mcpClientManager.getMcpTool("non-existent-server", "test-tool");
}, "Should throw IllegalArgumentException for non-existent client");
}
@Test
@DisplayName("Test isClientOnline with non-existent client - 检查不存在客户端在线状态测试")
void testIsClientOnlineNonExistent() {
assertFalse(mcpClientManager.isClientOnline("non-existent-server"),
"Should return false for non-existent client");
}
@Test
@DisplayName("Test registerFromFile - 文件配置注册测试")
void testRegisterFromFile() throws IOException {
// Create a temporary file with test config
// 创建包含测试配置的临时文件
Path tempFile = Files.createTempFile("test-config", ".json");
Files.write(tempFile, TEST_JSON_CONFIG.getBytes(StandardCharsets.UTF_8));
assertDoesNotThrow(() -> {
mcpClientManager.registerFromFile(tempFile);
}, "Should not throw exception when registering from file");
// Clean up
// 清理
Files.deleteIfExists(tempFile);
}
@Test
@DisplayName("Test registerFromFile IOException - 文件配置注册IO异常测试")
void testRegisterFromFileIOException() {
Path nonExistentPath = Path.of("/non/existent/path.json");
assertThrows(IOException.class, () -> {
mcpClientManager.registerFromFile(nonExistentPath);
}, "Should throw IOException for non-existent file");
}
@Test
@DisplayName("Test registerFromResource - 资源配置注册测试")
void testRegisterFromResource() {
String resourcePath = "mcp-servers.json";
assertDoesNotThrow(() -> {
mcpClientManager.registerFromResource(resourcePath);
}, "Should not throw exception when registering from resource");
}
@Test
@DisplayName("Test registerFromResource with non-existent resource - 注册不存在资源测试")
void testRegisterFromResourceNonExistent() {
String resourcePath = "non-existent-config.json";
assertThrows(IllegalArgumentException.class, () -> {
mcpClientManager.registerFromResource(resourcePath);
}, "Should throw IllegalArgumentException for non-existent resource");
}
@Test
@DisplayName("Test reconnect functionality - 重新连接功能测试")
void testReconnect() {
// First register a descriptor
// 首先注册一个描述符
try {
java.lang.reflect.Field registryField = McpClientManager.class.getDeclaredField("descriptorRegistry");
registryField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.Map<String, McpClientDescriptor> registry =
(java.util.Map<String, McpClientDescriptor>) registryField.get(mcpClientManager);
registry.put("test-server", mockDescriptor);
} catch (Exception e) {
throw new RuntimeException(e);
}
// Mock close behavior
// 模拟关闭行为
doNothing().when(mockDescriptor).close();
when(mockDescriptor.getName()).thenReturn("test-server");
when(mockDescriptor.getSpec()).thenReturn(new McpConfig().getMcpServers().get("test-server"));
when(mockDescriptor.getResolvedEnv()).thenReturn(new HashMap<>());
assertDoesNotThrow(() -> {
mcpClientManager.reconnect("test-server");
}, "Reconnect should not throw exception");
// Verify close was called
// 验证关闭被调用
verify(mockDescriptor, times(1)).close();
}
@Test
@DisplayName("Test reconnect with non-existent client - 重新连接不存在客户端测试")
void testReconnectNonExistent() {
assertDoesNotThrow(() -> {
mcpClientManager.reconnect("non-existent-server");
}, "Reconnect should not throw exception for non-existent client");
}
@Test
@DisplayName("Test reloadConfig - 重新加载配置测试")
void testReloadConfig() throws IllegalAccessException, NoSuchFieldException {
// Mock the autoLoadConfigFromResource method
// 模拟autoLoadConfigFromResource方法
try (MockedStatic<McpClientManager> managerMock = Mockito.mockStatic(McpClientManager.class,
Mockito.CALLS_REAL_METHODS)) {
// Add a descriptor first
// 首先添加一个描述符
java.lang.reflect.Field registryField = McpClientManager.class.getDeclaredField("descriptorRegistry");
registryField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.Map<String, McpClientDescriptor> registry =
(java.util.Map<String, McpClientDescriptor>) registryField.get(mcpClientManager);
registry.put("test-server", mockDescriptor);
// Mock close behavior
// 模拟关闭行为
doNothing().when(mockDescriptor).close();
assertDoesNotThrow(() -> {
McpClientManager.reloadConfig();
}, "Reload config should not throw exception");
// Verify that existing descriptors were closed
// 验证现有描述符被关闭
verify(mockDescriptor, times(1)).close();
}
}
@Test
@DisplayName("Test close functionality - 关闭功能测试")
void testClose() {
assertDoesNotThrow(() -> {
mcpClientManager.close();
}, "Close should not throw exception");
// Verify that the manager can be closed multiple times safely
// 验证管理器可以安全地多次关闭
assertDoesNotThrow(() -> {
mcpClientManager.close();
}, "Second close should not throw exception");
}
@Test
@DisplayName("Test environment variable resolution - 环境变量解析测试")
void testEnvironmentVariableResolution() {
// Test the registerFromConfig method with environment variable resolution
// 测试带有环境变量解析的registerFromConfig方法
String jsonWithInputVar = """
{
"mcpServers": {
"test-server": {
"transport": "stdio",
"env": {
"INPUT_VAR": "${input:test_input}",
"NORMAL_VAR": "normal_value"
}
}
}
}
""";
// Set system property for input resolution
// 设置输入解析的系统属性
System.setProperty("mcp.input.test_input", "resolved_value");
try {
mcpClientManager.registerFromJson(jsonWithInputVar);
} finally {
// Clean up system property
// 清理系统属性
System.clearProperty("mcp.input.test_input");
}
// The registration should succeed without throwing exception
// 注册应该成功而不抛出异常
assertDoesNotThrow(() -> {
mcpClientManager.isClientOnline("test-server");
});
}
@Test
@DisplayName("Test JSON parsing error - JSON解析错误测试")
void testJsonParsingError() {
String invalidJson = "{ invalid json }";
assertThrows(Exception.class, () -> {
mcpClientManager.registerFromJson(invalidJson);
}, "Should throw exception for invalid JSON");
}
@Test
@DisplayName("Test duplicate registration - 重复注册测试")
void testDuplicateRegistration() {
// Register the same server twice
// 重复注册同一个服务器
try {
java.lang.reflect.Field registryField = McpClientManager.class.getDeclaredField("descriptorRegistry");
registryField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.Map<String, McpClientDescriptor> registry =
(java.util.Map<String, McpClientDescriptor>) registryField.get(mcpClientManager);
registry.put("duplicate-server", mockDescriptor);
when(mockDescriptor.getName()).thenReturn("duplicate-server");
} catch (Exception e) {
throw new RuntimeException(e);
}
// The second registration should be skipped (no exception thrown)
// 第二次注册应该被跳过(不抛出异常)
assertDoesNotThrow(() -> {
mcpClientManager.registerFromJson(TEST_JSON_CONFIG);
});
}
@Test
@DisplayName("Test call tool - 测试工具调用")
void testCallTool() {
// The second registration should be skipped (no exception thrown)
// 第二次注册应该被跳过(不抛出异常)
assertDoesNotThrow(() -> {
mcpClientManager.registerFromResource("mcp-servers.json");
});
Tool mcpTool = mcpClientManager.getMcpTool("everything", "add");
if (mcpTool == null) {
return;
}
System.out.println(mcpTool);
ToolExecutor toolExecutor = new ToolExecutor(mcpTool
, new ToolCall("add", "add", "{\"a\":1,\"b\":2}"));
Object result = toolExecutor.execute();
assertEquals("The sum of 1 and 2 is 3.", result);
System.out.println(result);
}
}