From 03e98934d48b4f3cf8c35e2f52291b8e06aad177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E7=BF=8A=20SionYang?= Date: Tue, 30 Sep 2025 17:00:03 +0800 Subject: [PATCH] Add some unit test for ai module. (#13873) Change-Id: I466335abb694620be66b650666b15f96d090f524 --- .../McpServerValidationConstants.java | 4 +- .../ai/form/a2a/admin/AgentCardForm.java | 5 +- .../nacos/ai/form/a2a/admin/AgentForm.java | 17 - .../ai/form/a2a/admin/AgentListForm.java | 18 - .../nacos/ai/index/CachedMcpServerIndex.java | 21 +- .../nacos/ai/index/MemoryMcpCacheIndex.java | 112 ++-- .../a2a/AgentEndpointRequestHandler.java | 1 + .../ai/service/McpServerImportService.java | 2 +- .../service/McpServerValidationService.java | 2 +- .../config/McpCacheIndexPropertiesTest.java | 41 ++ .../ai/form/a2a/admin/AgentCardFormTest.java | 116 ++++ .../ai/form/a2a/admin/AgentFormTest.java | 85 +++ .../ai/form/a2a/admin/AgentListFormTest.java | 99 +++ .../ai/form/mcp/admin/McpImportFormTest.java | 147 +++++ .../ai/index/CachedMcpServerIndexTest.java | 578 +++++++++++++++++- .../ai/index/MemoryMcpCacheIndexTest.java | 334 +++++++++- .../ai/index/PlainMcpServerIndexTest.java | 11 + .../ai/param/AgentHttpParamExtractorTest.java | 113 ++++ .../a2a/AgentEndpointRequestHandlerTest.java | 160 +++++ .../a2a/QueryAgentCardRequestHandlerTest.java | 93 +++ .../ReleaseAgentCardRequestHandlerTest.java | 164 +++++ .../service/McpServerImportServiceTest.java | 2 +- 22 files changed, 2027 insertions(+), 98 deletions(-) rename ai/src/main/java/com/alibaba/nacos/ai/{constants => constant}/McpServerValidationConstants.java (92%) create mode 100644 ai/src/test/java/com/alibaba/nacos/ai/config/McpCacheIndexPropertiesTest.java create mode 100644 ai/src/test/java/com/alibaba/nacos/ai/form/a2a/admin/AgentCardFormTest.java create mode 100644 ai/src/test/java/com/alibaba/nacos/ai/form/a2a/admin/AgentFormTest.java create mode 100644 ai/src/test/java/com/alibaba/nacos/ai/form/a2a/admin/AgentListFormTest.java create mode 100644 ai/src/test/java/com/alibaba/nacos/ai/form/mcp/admin/McpImportFormTest.java create mode 100644 ai/src/test/java/com/alibaba/nacos/ai/param/AgentHttpParamExtractorTest.java create mode 100644 ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/AgentEndpointRequestHandlerTest.java create mode 100644 ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/QueryAgentCardRequestHandlerTest.java create mode 100644 ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/ReleaseAgentCardRequestHandlerTest.java diff --git a/ai/src/main/java/com/alibaba/nacos/ai/constants/McpServerValidationConstants.java b/ai/src/main/java/com/alibaba/nacos/ai/constant/McpServerValidationConstants.java similarity index 92% rename from ai/src/main/java/com/alibaba/nacos/ai/constants/McpServerValidationConstants.java rename to ai/src/main/java/com/alibaba/nacos/ai/constant/McpServerValidationConstants.java index f620c59067..6ebaab247e 100644 --- a/ai/src/main/java/com/alibaba/nacos/ai/constants/McpServerValidationConstants.java +++ b/ai/src/main/java/com/alibaba/nacos/ai/constant/McpServerValidationConstants.java @@ -1,5 +1,5 @@ /* - * Copyright 1999-2018 Alibaba Group Holding Ltd. + * Copyright 1999-2025 Alibaba Group Holding Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.nacos.ai.constants; +package com.alibaba.nacos.ai.constant; /** * Constants for MCP server validation. diff --git a/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentCardForm.java b/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentCardForm.java index 25851beb37..374533543b 100644 --- a/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentCardForm.java +++ b/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentCardForm.java @@ -46,9 +46,8 @@ public class AgentCardForm extends AgentForm { throw new NacosApiException(NacosException.INVALID_PARAM, ErrorCode.PARAMETER_MISSING, "Request parameter `agentCard` should not be `null` or empty."); } - if (StringUtils.isNotEmpty(getRegistrationType())) { - validateRegistrationType(); - } + validateRegistrationType(); + } protected void validateRegistrationType() throws NacosApiException { diff --git a/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentForm.java b/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentForm.java index ccd12af8fa..668f187dc2 100644 --- a/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentForm.java +++ b/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentForm.java @@ -24,7 +24,6 @@ import com.alibaba.nacos.api.model.v2.ErrorCode; import com.alibaba.nacos.common.utils.StringUtils; import java.io.Serial; -import java.util.Objects; import static com.alibaba.nacos.api.ai.constant.AiConstants.A2a.A2A_DEFAULT_NAMESPACE; @@ -92,20 +91,4 @@ public class AgentForm implements NacosForm { public void setRegistrationType(String registrationType) { this.registrationType = registrationType; } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - AgentForm agentForm = (AgentForm) o; - return Objects.equals(namespaceId, agentForm.namespaceId) && Objects.equals(agentName, agentForm.agentName) - && Objects.equals(version, agentForm.version) && Objects.equals(registrationType, - agentForm.registrationType); - } - - @Override - public int hashCode() { - return Objects.hash(namespaceId, agentName, version, registrationType); - } } diff --git a/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentListForm.java b/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentListForm.java index 09bddc0150..4a85fb6196 100644 --- a/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentListForm.java +++ b/ai/src/main/java/com/alibaba/nacos/ai/form/a2a/admin/AgentListForm.java @@ -22,7 +22,6 @@ import com.alibaba.nacos.api.exception.api.NacosApiException; import com.alibaba.nacos.api.model.v2.ErrorCode; import java.io.Serial; -import java.util.Objects; /** * Agent list form. @@ -53,21 +52,4 @@ public class AgentListForm extends AgentForm { public void setSearch(String search) { this.search = search; } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) { - return false; - } - if (!super.equals(o)) { - return false; - } - AgentListForm that = (AgentListForm) o; - return Objects.equals(search, that.search); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), search); - } } diff --git a/ai/src/main/java/com/alibaba/nacos/ai/index/CachedMcpServerIndex.java b/ai/src/main/java/com/alibaba/nacos/ai/index/CachedMcpServerIndex.java index 66f8cf42d8..81afdd65a2 100644 --- a/ai/src/main/java/com/alibaba/nacos/ai/index/CachedMcpServerIndex.java +++ b/ai/src/main/java/com/alibaba/nacos/ai/index/CachedMcpServerIndex.java @@ -16,15 +16,6 @@ package com.alibaba.nacos.ai.index; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import javax.annotation.PreDestroy; - import com.alibaba.nacos.ai.constant.Constants; import com.alibaba.nacos.ai.model.mcp.McpServerIndexData; import com.alibaba.nacos.ai.utils.McpConfigUtils; @@ -39,6 +30,15 @@ import com.alibaba.nacos.core.service.NamespaceOperationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.PreDestroy; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + /** * Enhanced MCP cache index implementation combining memory cache and database queries. * @@ -261,9 +261,6 @@ public class CachedMcpServerIndex implements McpServerIndex { * Start scheduled sync task. */ private void startSyncTask() { - if (syncTask != null && !syncTask.isCancelled()) { - syncTask.cancel(false); - } syncTask = scheduledExecutor.scheduleWithFixedDelay(() -> { try { LOGGER.debug("Starting cache sync task"); diff --git a/ai/src/main/java/com/alibaba/nacos/ai/index/MemoryMcpCacheIndex.java b/ai/src/main/java/com/alibaba/nacos/ai/index/MemoryMcpCacheIndex.java index c11029dd37..404d2f4a96 100644 --- a/ai/src/main/java/com/alibaba/nacos/ai/index/MemoryMcpCacheIndex.java +++ b/ai/src/main/java/com/alibaba/nacos/ai/index/MemoryMcpCacheIndex.java @@ -16,6 +16,12 @@ package com.alibaba.nacos.ai.index; +import com.alibaba.nacos.ai.config.McpCacheIndexProperties; +import com.alibaba.nacos.ai.model.mcp.McpServerIndexData; +import com.alibaba.nacos.common.utils.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -26,17 +32,33 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReentrantReadWriteLock; -import com.alibaba.nacos.ai.config.McpCacheIndexProperties; -import com.alibaba.nacos.ai.model.mcp.McpServerIndexData; -import com.alibaba.nacos.common.utils.StringUtils; - /** * Memory-based MCP cache index implementation with optimized locking. * + *

+ * TODO This Memory cache might include some design issues: + *

+ *

+ * * @author misselvexu */ public class MemoryMcpCacheIndex implements McpCacheIndex { + private static final Logger LOGGER = LoggerFactory.getLogger(MemoryMcpCacheIndex.class); + private static final int DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 5; private final McpCacheIndexProperties properties; @@ -107,28 +129,33 @@ public class MemoryMcpCacheIndex implements McpCacheIndex { } String key = buildNameKey(namespaceId, mcpName); - String id = nameKeyToId.get(key); - if (id == null) { - missCount.incrementAndGet(); - return null; - } - - CacheNode node = idToEntry.get(id); - if (node == null || node.isExpired(properties.getExpireTimeSeconds())) { - // Clean up invalid mapping - nameKeyToId.remove(key, id); - if (node != null) { - removeFromLru(node); - idToEntry.remove(id, node); + readLock.lock(); + try { + String id = nameKeyToId.get(key); + if (id == null) { + missCount.incrementAndGet(); + return null; } - missCount.incrementAndGet(); - return null; + + CacheNode node = idToEntry.get(id); + if (node == null || node.isExpired(properties.getExpireTimeSeconds())) { + // Clean up invalid mapping + nameKeyToId.remove(key, id); + if (node != null) { + removeFromLru(node); + idToEntry.remove(id, node); + } + missCount.incrementAndGet(); + return null; + } + + // Update LRU position + moveToHead(node); + hitCount.incrementAndGet(); + return id; + } finally { + readLock.unlock(); } - - // Update LRU position - moveToHead(node); - hitCount.incrementAndGet(); - return id; } @Override @@ -146,21 +173,26 @@ public class MemoryMcpCacheIndex implements McpCacheIndex { return null; } - CacheNode node = idToEntry.get(mcpId); - if (node == null || node.isExpired(properties.getExpireTimeSeconds())) { - if (node != null) { - removeFromLru(node); - idToEntry.remove(mcpId, node); - cleanupInvalidMappings(mcpId); + readLock.lock(); + try { + CacheNode node = idToEntry.get(mcpId); + if (node == null || node.isExpired(properties.getExpireTimeSeconds())) { + if (node != null) { + removeFromLru(node); + idToEntry.remove(mcpId, node); + cleanupInvalidMappings(mcpId); + } + missCount.incrementAndGet(); + return null; } - missCount.incrementAndGet(); - return null; + + // Update LRU position + moveToHead(node); + hitCount.incrementAndGet(); + return node.data; + } finally { + readLock.unlock(); } - - // Update LRU position - moveToHead(node); - hitCount.incrementAndGet(); - return node.data; } @Override @@ -299,7 +331,7 @@ public class MemoryMcpCacheIndex implements McpCacheIndex { } } } catch (Exception e) { - // Log error but don't throw + LOGGER.error("Clean up expired mcp id and name cache failed.", e); } } @@ -322,14 +354,14 @@ public class MemoryMcpCacheIndex implements McpCacheIndex { head.next = node; } - private void removeFromLru(CacheNode node) { + private synchronized void removeFromLru(CacheNode node) { if (node.prev != null && node.next != null) { node.prev.next = node.next; node.next.prev = node.prev; } } - private void moveToHead(CacheNode node) { + private synchronized void moveToHead(CacheNode node) { // Remove from current position if (node.prev != null && node.next != null) { node.prev.next = node.next; diff --git a/ai/src/main/java/com/alibaba/nacos/ai/remote/handler/a2a/AgentEndpointRequestHandler.java b/ai/src/main/java/com/alibaba/nacos/ai/remote/handler/a2a/AgentEndpointRequestHandler.java index 72d5273f55..b22479e032 100644 --- a/ai/src/main/java/com/alibaba/nacos/ai/remote/handler/a2a/AgentEndpointRequestHandler.java +++ b/ai/src/main/java/com/alibaba/nacos/ai/remote/handler/a2a/AgentEndpointRequestHandler.java @@ -70,6 +70,7 @@ public class AgentEndpointRequestHandler extends RequestHandler namespaceList = new ArrayList<>(); + com.alibaba.nacos.api.model.response.Namespace namespace = new com.alibaba.nacos.api.model.response.Namespace(); + namespace.setNamespace("test-namespace"); + namespaceList.add(namespace); + when(namespaceOperationService.getNamespaceList()).thenReturn(namespaceList); + + // 执行查询 + McpServerIndexData result = disabledIndex.getMcpServerById(mcpId); + + // 验证结果为null + assertNull(result); + + // 验证缓存没有被调用 + verify(cacheIndex, never()).getMcpServerById(anyString()); + verify(cacheIndex, never()).updateIndex(anyString(), anyString(), anyString()); + + // 验证数据库查询被调用 + verify(configQueryChainService).handle(any(ConfigQueryChainRequest.class)); + } + + @Test + void testGetMcpServerByIdWithCacheMissAndNotFound() { + final String mcpId = "test-id-123"; + + // 模拟缓存未命中 + when(cacheIndex.getMcpServerById(mcpId)).thenReturn(null); + + // 模拟数据库查询结果为null(未找到) + ConfigQueryChainResponse mockResponse = mock(ConfigQueryChainResponse.class); + when(mockResponse.getStatus()).thenReturn(ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_NOT_FOUND); + when(configQueryChainService.handle(any(ConfigQueryChainRequest.class))).thenReturn(mockResponse); + + // 模拟命名空间列表 + List namespaceList = new ArrayList<>(); + com.alibaba.nacos.api.model.response.Namespace namespace = new com.alibaba.nacos.api.model.response.Namespace(); + namespace.setNamespace("test-namespace"); + namespaceList.add(namespace); + when(namespaceOperationService.getNamespaceList()).thenReturn(namespaceList); + + // 执行查询 + McpServerIndexData result = cachedIndex.getMcpServerById(mcpId); + + // 验证结果为null + assertNull(result); + + // 验证缓存被调用,数据库查询也被调用 + verify(cacheIndex).getMcpServerById(mcpId); + verify(configQueryChainService).handle(any(ConfigQueryChainRequest.class)); + + // 验证缓存未被更新(因为未找到) + verify(cacheIndex, never()).updateIndex(anyString(), anyString(), anyString()); + } + + @Test + void testGetMcpServerByNameWithInvalidParameters() { + // 测试null参数 + McpServerIndexData result1 = cachedIndex.getMcpServerByName(null, "test-name"); + assertNull(result1); + + McpServerIndexData result2 = cachedIndex.getMcpServerByName("test-namespace", null); + assertNull(result2); + + McpServerIndexData result3 = cachedIndex.getMcpServerByName(null, null); + assertNull(result3); + + // 测试空字符串参数 + McpServerIndexData result4 = cachedIndex.getMcpServerByName("", "test-name"); + assertNull(result4); + + McpServerIndexData result5 = cachedIndex.getMcpServerByName("test-namespace", ""); + assertNull(result5); + + McpServerIndexData result6 = cachedIndex.getMcpServerByName("", ""); + assertNull(result6); + + // 验证缓存未被调用 + verify(cacheIndex, never()).getMcpServerByName(anyString(), anyString()); + } + + @Test + void testGetMcpServerByNameWithCacheDisabledAndNotFound() { + // 创建禁用缓存的实例 + final CachedMcpServerIndex disabledIndex = new CachedMcpServerIndex(configDetailService, + namespaceOperationService, configQueryChainService, cacheIndex, scheduledExecutor, false, 0); + + final String namespaceId = "test-namespace"; + final String mcpName = "test-mcp"; + + // 模拟数据库查询结果为null + final Page mockPage = new Page<>(); + mockPage.setPageItems(new ArrayList<>()); + mockPage.setTotalCount(0); + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_ACCURATE), eq(1), eq(1), isNull(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any())).thenReturn(mockPage); + + // 执行查询 + McpServerIndexData result = disabledIndex.getMcpServerByName(namespaceId, mcpName); + + // 验证结果为null + assertNull(result); + + // 验证缓存没有被调用 + verify(cacheIndex, never()).getMcpServerByName(anyString(), anyString()); + verify(cacheIndex, never()).updateIndex(anyString(), anyString(), anyString()); + + // 验证数据库查询被调用 + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_ACCURATE), eq(1), eq(1), isNull(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any()); + } + + @Test + void testGetMcpServerByNameWithCacheMissAndNotFound() { + final String namespaceId = "test-namespace"; + final String mcpName = "test-mcp"; + + // 模拟缓存未命中 + when(cacheIndex.getMcpServerByName(namespaceId, mcpName)).thenReturn(null); + + // 模拟数据库查询结果为null + final Page mockPage = new Page<>(); + mockPage.setPageItems(new ArrayList<>()); + mockPage.setTotalCount(0); + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_ACCURATE), eq(1), eq(1), isNull(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any())).thenReturn(mockPage); + + // 执行查询 + McpServerIndexData result = cachedIndex.getMcpServerByName(namespaceId, mcpName); + + // 验证结果为null + assertNull(result); + + // 验证缓存被调用,数据库查询也被调用 + verify(cacheIndex).getMcpServerByName(namespaceId, mcpName); + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_ACCURATE), eq(1), eq(1), isNull(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any()); + + // 验证缓存未被更新(因为未找到) + verify(cacheIndex, never()).updateIndex(anyString(), anyString(), anyString()); + } + + @Test + void testSearchMcpServerByNameWithNullName() { + final String namespaceId = "test-namespace"; + final String mcpId = "test-id-123"; + + // 模拟数据库查询结果 + final Page mockPage = new Page<>(); + List configList = new ArrayList<>(); + ConfigInfo configInfo = new ConfigInfo(); + configInfo.setDataId(mcpId + Constants.MCP_SERVER_VERSION_DATA_ID_SUFFIX); + configInfo.setTenant(namespaceId); + configList.add(configInfo); + mockPage.setPageItems(configList); + mockPage.setTotalCount(1); + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(10), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any())).thenReturn(mockPage); + + // 执行搜索,name为null + Page result = cachedIndex.searchMcpServerByName(namespaceId, null, + Constants.MCP_LIST_SEARCH_BLUR, 0, 10); + + // 验证结果 + assertNotNull(result); + assertEquals(1, result.getTotalCount()); + assertEquals(1, result.getPageItems().size()); + + McpServerIndexData indexData = result.getPageItems().get(0); + assertEquals(mcpId, indexData.getId()); + assertEquals(namespaceId, indexData.getNamespaceId()); + + // 验证数据库查询被调用 + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(10), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any()); + } + + @Test + void testSearchMcpServerByNameWithEmptyName() { + final String namespaceId = "test-namespace"; + final String mcpId = "test-id-123"; + + // 模拟数据库查询结果 + final Page mockPage = new Page<>(); + List configList = new ArrayList<>(); + ConfigInfo configInfo = new ConfigInfo(); + configInfo.setDataId(mcpId + Constants.MCP_SERVER_VERSION_DATA_ID_SUFFIX); + configInfo.setTenant(namespaceId); + configList.add(configInfo); + mockPage.setPageItems(configList); + mockPage.setTotalCount(1); + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(10), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any())).thenReturn(mockPage); + + // 执行搜索,name为空字符串 + Page result = cachedIndex.searchMcpServerByName(namespaceId, "", + Constants.MCP_LIST_SEARCH_ACCURATE, 0, 10); + + // 验证结果 + assertNotNull(result); + assertEquals(1, result.getTotalCount()); + assertEquals(1, result.getPageItems().size()); + + McpServerIndexData indexData = result.getPageItems().get(0); + assertEquals(mcpId, indexData.getId()); + assertEquals(namespaceId, indexData.getNamespaceId()); + + // 验证数据库查询被调用 + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(10), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any()); + } + + @Test + void testSearchMcpServerByNameWithBlurSearch() { + final String namespaceId = "test-namespace"; + final String mcpName = "test-mcp"; + final String mcpId = "test-id-123"; + + // 模拟数据库查询结果 + final Page mockPage = new Page<>(); + List configList = new ArrayList<>(); + ConfigInfo configInfo = new ConfigInfo(); + configInfo.setDataId(mcpId + Constants.MCP_SERVER_VERSION_DATA_ID_SUFFIX); + configInfo.setTenant(namespaceId); + configList.add(configInfo); + mockPage.setPageItems(configList); + mockPage.setTotalCount(1); + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(10), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any())).thenReturn(mockPage); + + // 执行搜索 + Page result = cachedIndex.searchMcpServerByName(namespaceId, mcpName, + Constants.MCP_LIST_SEARCH_BLUR, 0, 10); + + // 验证结果 + assertNotNull(result); + assertEquals(1, result.getTotalCount()); + assertEquals(1, result.getPageItems().size()); + + McpServerIndexData indexData = result.getPageItems().get(0); + assertEquals(mcpId, indexData.getId()); + assertEquals(namespaceId, indexData.getNamespaceId()); + + // 验证数据库查询被调用 + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(10), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any()); + } + + @Test + void testSearchMcpServerByNameWithPagination() { + final String namespaceId = "test-namespace"; + final String mcpName = "test-mcp"; + final String mcpId = "test-id-123"; + + // 模拟数据库查询结果 + final Page mockPage = new Page<>(); + List configList = new ArrayList<>(); + ConfigInfo configInfo = new ConfigInfo(); + configInfo.setDataId(mcpId + Constants.MCP_SERVER_VERSION_DATA_ID_SUFFIX); + configInfo.setTenant(namespaceId); + configList.add(configInfo); + mockPage.setPageItems(configList); + mockPage.setTotalCount(15); // 总数15,测试分页 + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_ACCURATE), eq(3), eq(5), isNull(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any())).thenReturn(mockPage); + + // 执行搜索,offset=10, limit=5,应该查询第3页 + Page result = cachedIndex.searchMcpServerByName(namespaceId, mcpName, + Constants.MCP_LIST_SEARCH_ACCURATE, 10, 5); + + // 验证结果 + assertNotNull(result); + assertEquals(15, result.getTotalCount()); + assertEquals(1, result.getPageItems().size()); + assertEquals(3, result.getPageNumber()); + assertEquals(3, result.getPagesAvailable()); // ceil(15/5) = 3 + + McpServerIndexData indexData = result.getPageItems().get(0); + assertEquals(mcpId, indexData.getId()); + assertEquals(namespaceId, indexData.getNamespaceId()); + + // 验证数据库查询被调用 + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_ACCURATE), eq(3), eq(5), isNull(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any()); + } + + @Test + void testFetchOrderedNamespaceList() { + // 模拟命名空间列表(无序) + final List namespaceList = new ArrayList<>(); + com.alibaba.nacos.api.model.response.Namespace namespace1 = new com.alibaba.nacos.api.model.response.Namespace(); + namespace1.setNamespace("b-namespace"); + com.alibaba.nacos.api.model.response.Namespace namespace2 = new com.alibaba.nacos.api.model.response.Namespace(); + namespace2.setNamespace("a-namespace"); + com.alibaba.nacos.api.model.response.Namespace namespace3 = new com.alibaba.nacos.api.model.response.Namespace(); + namespace3.setNamespace("c-namespace"); + namespaceList.add(namespace1); + namespaceList.add(namespace2); + namespaceList.add(namespace3); + when(namespaceOperationService.getNamespaceList()).thenReturn(namespaceList); + + // 通过调用依赖该方法的函数来间接测试 + final String mcpId = "test-id-123"; + + // 模拟缓存未命中 + when(cacheIndex.getMcpServerById(mcpId)).thenReturn(null); + + // 模拟数据库查询结果 + ConfigQueryChainResponse mockResponse = mock(ConfigQueryChainResponse.class); + when(mockResponse.getStatus()).thenReturn(ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_FOUND_FORMAL); + when(configQueryChainService.handle(any(ConfigQueryChainRequest.class))).thenReturn(mockResponse); + + // 执行查询 + cachedIndex.getMcpServerById(mcpId); + + // 验证命名空间服务被调用 + verify(namespaceOperationService).getNamespaceList(); + } + + @Test + void testMapMcpServerVersionConfigToIndexData() { + // 通过调用依赖该方法的函数来间接测试 + final String namespaceId = "test-namespace"; + final String mcpName = "test-mcp"; + final String mcpId = "test-id-123"; + + // 模拟缓存未命中 + when(cacheIndex.getMcpServerByName(namespaceId, mcpName)).thenReturn(null); + + // 模拟数据库查询结果 + final Page mockPage = new Page<>(); + List configList = new ArrayList<>(); + ConfigInfo configInfo = new ConfigInfo(); + configInfo.setDataId(mcpId + Constants.MCP_SERVER_VERSION_DATA_ID_SUFFIX); + configInfo.setTenant(namespaceId); + configList.add(configInfo); + mockPage.setPageItems(configList); + mockPage.setTotalCount(1); + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_ACCURATE), eq(1), eq(1), isNull(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq(namespaceId), any())).thenReturn(mockPage); + + // 执行查询 + McpServerIndexData result = cachedIndex.getMcpServerByName(namespaceId, mcpName); + + // 验证结果,确保mapMcpServerVersionConfigToIndexData方法正确执行 + assertNotNull(result); + assertEquals(mcpId, result.getId()); + assertEquals(namespaceId, result.getNamespaceId()); + } + + @Test + void testTriggerCacheSyncWhenCacheDisabled() { + // 创建禁用缓存的实例 + final CachedMcpServerIndex disabledIndex = new CachedMcpServerIndex(configDetailService, + namespaceOperationService, configQueryChainService, cacheIndex, scheduledExecutor, false, 0); + + // 执行手动同步 + disabledIndex.triggerCacheSync(); + + // 验证数据库查询没有被调用 + verify(configDetailService, never()).findConfigInfoPage(anyString(), anyInt(), anyInt(), anyString(), + anyString(), anyString(), any()); + } + + @Test + void testStartSyncTask() { + when(scheduledExecutor.scheduleWithFixedDelay(any(Runnable.class), eq(10L), eq(10L), any(TimeUnit.class))).then( + (Answer) invocation -> { + invocation.getArgument(0, Runnable.class).run(); + return null; + }); + // 创建一个新的实例来测试startSyncTask方法 + CachedMcpServerIndex newIndex = new CachedMcpServerIndex(configDetailService, namespaceOperationService, + configQueryChainService, cacheIndex, scheduledExecutor, true, 10); + + // 验证调度任务已启动 + verify(scheduledExecutor).scheduleWithFixedDelay(any(Runnable.class), eq(10L), eq(10L), any(TimeUnit.class)); + verify(namespaceOperationService).getNamespaceList(); + } + + @Test + void testStartSyncTaskWithException() { + when(scheduledExecutor.scheduleWithFixedDelay(any(Runnable.class), eq(10L), eq(10L), any(TimeUnit.class))).then( + (Answer) invocation -> { + invocation.getArgument(0, Runnable.class).run(); + return null; + }); + when(namespaceOperationService.getNamespaceList()).thenThrow(new RuntimeException("test")); + // 创建一个新的实例来测试startSyncTask方法 + CachedMcpServerIndex newIndex = new CachedMcpServerIndex(configDetailService, namespaceOperationService, + configQueryChainService, cacheIndex, scheduledExecutor, true, 10); + + // 验证调度任务已启动 + verify(scheduledExecutor).scheduleWithFixedDelay(any(Runnable.class), eq(10L), eq(10L), any(TimeUnit.class)); + } + + @Test + void testDestroy() { + // 模拟一个已经存在的任务 + ScheduledFuture mockTask = mock(ScheduledFuture.class); + when(scheduledExecutor.scheduleWithFixedDelay(any(Runnable.class), anyLong(), anyLong(), + any(TimeUnit.class))).thenReturn(mockTask); + + // 创建一个新的实例来测试destroy方法 + CachedMcpServerIndex indexToDestroy = new CachedMcpServerIndex(configDetailService, namespaceOperationService, + configQueryChainService, cacheIndex, scheduledExecutor, true, 300); + + // 调用destroy方法 + indexToDestroy.destroy(); + + // 验证任务被取消并且线程池被关闭 + verify(mockTask).cancel(true); + verify(scheduledExecutor).shutdown(); + } + + @Test + void testDestroyWithExceptionHandling() { + // 模拟scheduledExecutor.shutdown()抛出异常 + doThrow(new RuntimeException("Shutdown failed")).when(scheduledExecutor).shutdown(); + + // 创建一个新的实例来测试destroy方法 + CachedMcpServerIndex indexToDestroy = new CachedMcpServerIndex(configDetailService, namespaceOperationService, + configQueryChainService, cacheIndex, scheduledExecutor, true, 300); + + // 调用destroy方法不应该抛出异常 + indexToDestroy.destroy(); + } + + @Test + void testSyncCacheFromDatabase() { + // 模拟命名空间列表 + List namespaceList = new ArrayList<>(); + com.alibaba.nacos.api.model.response.Namespace namespace1 = new com.alibaba.nacos.api.model.response.Namespace(); + namespace1.setNamespace("namespace-1"); + com.alibaba.nacos.api.model.response.Namespace namespace2 = new com.alibaba.nacos.api.model.response.Namespace(); + namespace2.setNamespace("namespace-2"); + namespaceList.add(namespace1); + namespaceList.add(namespace2); + when(namespaceOperationService.getNamespaceList()).thenReturn(namespaceList); + + // 模拟每个命名空间的搜索结果 + final Page mockPage1 = new Page<>(); + List configList1 = new ArrayList<>(); + ConfigInfo configInfo1 = new ConfigInfo(); + configInfo1.setDataId("server1" + Constants.MCP_SERVER_VERSION_DATA_ID_SUFFIX); + configInfo1.setTenant("namespace-1"); + configList1.add(configInfo1); + mockPage1.setPageItems(configList1); + mockPage1.setTotalCount(1); + + final Page mockPage2 = new Page<>(); + List configList2 = new ArrayList<>(); + ConfigInfo configInfo2 = new ConfigInfo(); + configInfo2.setDataId("server2" + Constants.MCP_SERVER_VERSION_DATA_ID_SUFFIX); + configInfo2.setTenant("namespace-2"); + configList2.add(configInfo2); + mockPage2.setPageItems(configList2); + mockPage2.setTotalCount(1); + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(1000), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq("namespace-1"), any())).thenReturn(mockPage1); + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(1000), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq("namespace-2"), any())).thenReturn(mockPage2); + + // 调用syncCacheFromDatabase方法(通过triggerCacheSync) + cachedIndex.triggerCacheSync(); + + // 验证为每个命名空间调用了搜索 + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(1000), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq("namespace-1"), any()); + + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(1000), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq("namespace-2"), any()); + + // 验证缓存被更新 + verify(cacheIndex).updateIndex(eq("namespace-1"), eq("server1"), eq("server1")); + verify(cacheIndex).updateIndex(eq("namespace-2"), eq("server2"), eq("server2")); + } + + @Test + void testSyncCacheFromDatabaseWithSearchException() { + // 模拟命名空间列表 + List namespaceList = new ArrayList<>(); + com.alibaba.nacos.api.model.response.Namespace namespace = new com.alibaba.nacos.api.model.response.Namespace(); + namespace.setNamespace("namespace-1"); + namespaceList.add(namespace); + when(namespaceOperationService.getNamespaceList()).thenReturn(namespaceList); + + // 模拟搜索时抛出异常 + when(configDetailService.findConfigInfoPage(anyString(), anyInt(), anyInt(), anyString(), anyString(), + anyString(), any())).thenThrow(new RuntimeException("Database error")); + + // 调用syncCacheFromDatabase方法(通过triggerCacheSync) + cachedIndex.triggerCacheSync(); + + // 即使出现异常也应该继续执行而不会中断 + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(1000), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq("namespace-1"), any()); + } + + @Test + void testSyncCacheFromDatabaseWithEmptyResult() { + // 模拟命名空间列表 + List namespaceList = new ArrayList<>(); + com.alibaba.nacos.api.model.response.Namespace namespace = new com.alibaba.nacos.api.model.response.Namespace(); + namespace.setNamespace("namespace-1"); + namespaceList.add(namespace); + when(namespaceOperationService.getNamespaceList()).thenReturn(namespaceList); + + // 模拟空的搜索结果 + Page mockPage = new Page<>(); + mockPage.setPageItems(new ArrayList<>()); + mockPage.setTotalCount(0); + + when(configDetailService.findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(1000), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq("namespace-1"), any())).thenReturn(mockPage); + + // 调用syncCacheFromDatabase方法(通过triggerCacheSync) + cachedIndex.triggerCacheSync(); + + // 验证搜索被调用但缓存未更新 + verify(configDetailService).findConfigInfoPage(eq(Constants.MCP_LIST_SEARCH_BLUR), eq(1), eq(1000), anyString(), + eq(Constants.MCP_SERVER_VERSIONS_GROUP), eq("namespace-1"), any()); + + // 没有数据所以不需要更新缓存 + verify(cacheIndex, never()).updateIndex(anyString(), anyString(), anyString()); + } + + @Test + void testSyncCacheFromDatabaseWithException() { + // 模拟命名空间列表 + List namespaceList = new ArrayList<>(); + com.alibaba.nacos.api.model.response.Namespace namespace = new com.alibaba.nacos.api.model.response.Namespace(); + namespace.setNamespace("test-namespace"); + namespaceList.add(namespace); + when(namespaceOperationService.getNamespaceList()).thenReturn(namespaceList); + + // 模拟搜索时抛出异常 + when(configDetailService.findConfigInfoPage(anyString(), anyInt(), anyInt(), anyString(), anyString(), + anyString(), any())).thenThrow(new RuntimeException("Test exception")); + + // 通过调用triggerCacheSync来触发syncCacheFromDatabase + cachedIndex.triggerCacheSync(); + + // 验证异常被处理,不会导致程序崩溃 + verify(namespaceOperationService).getNamespaceList(); + } +} \ No newline at end of file diff --git a/ai/src/test/java/com/alibaba/nacos/ai/index/MemoryMcpCacheIndexTest.java b/ai/src/test/java/com/alibaba/nacos/ai/index/MemoryMcpCacheIndexTest.java index 697dbc6cfb..f9b97f1aaf 100644 --- a/ai/src/test/java/com/alibaba/nacos/ai/index/MemoryMcpCacheIndexTest.java +++ b/ai/src/test/java/com/alibaba/nacos/ai/index/MemoryMcpCacheIndexTest.java @@ -20,16 +20,24 @@ import com.alibaba.nacos.ai.config.McpCacheIndexProperties; import com.alibaba.nacos.ai.model.mcp.McpServerIndexData; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class MemoryMcpCacheIndexTest { @@ -148,7 +156,7 @@ class MemoryMcpCacheIndexTest { } // 增加等待时间 - boolean completed = latch.await(60, TimeUnit.SECONDS); + boolean completed = latch.await(10, TimeUnit.SECONDS); assertTrue(completed, "All threads should complete within timeout"); // 关闭线程池并等待所有任务完成 @@ -413,4 +421,326 @@ class MemoryMcpCacheIndexTest { assertEquals("test-id", cache.getMcpId("test", "test")); assertEquals(1, cache.getSize()); } -} \ No newline at end of file + + // 补充测试用例 + + @Test + void testGetMcpIdWithInvalidParameters() { + // 测试null参数 + assertNull(cache.getMcpId(null, "name")); + assertNull(cache.getMcpId("namespace", null)); + assertNull(cache.getMcpId(null, null)); + + // 测试空字符串参数 + assertNull(cache.getMcpId("", "name")); + assertNull(cache.getMcpId("namespace", "")); + assertNull(cache.getMcpId("", "")); + } + + @Test + void testGetMcpIdWithNonExistentEntry() { + // 测试不存在的条目 + assertNull(cache.getMcpId("non-existent-namespace", "non-existent-name")); + } + + @Test + void testGetMcpIdWithExpiredEntry() throws InterruptedException { + // 添加一个条目 + cache.updateIndex("ns", "name", "id1"); + assertEquals("id1", cache.getMcpId("ns", "name")); + + // 等待过期 + Thread.sleep(2100); + + // 再次获取应该返回null + assertNull(cache.getMcpId("ns", "name")); + } + + @Test + void testGetMcpServerByIdWithInvalidParameters() { + // 测试null参数 + assertNull(cache.getMcpServerById(null)); + + // 测试空字符串参数 + assertNull(cache.getMcpServerById("")); + } + + @Test + void testGetMcpServerByIdWithNonExistentEntry() { + // 测试不存在的条目 + assertNull(cache.getMcpServerById("non-existent-id")); + } + + @Test + void testGetMcpServerByIdWithExpiredEntry() throws InterruptedException { + // 添加一个条目 + cache.updateIndex("ns", "name", "id1"); + assertNotNull(cache.getMcpServerById("id1")); + + // 等待过期 + Thread.sleep(2100); + + // 再次获取应该返回null + assertNull(cache.getMcpServerById("id1")); + } + + @Test + void testGetMcpServerByIdUpdatesLru() { + // 填满缓存 + cache.updateIndex("ns", "name1", "id1"); + cache.updateIndex("ns", "name2", "id2"); + cache.updateIndex("ns", "name3", "id3"); + + // 访问id1,使其成为最近使用的 + assertNotNull(cache.getMcpServerById("id1")); + + // 添加新元素,应该淘汰id2而不是id1 + cache.updateIndex("ns", "name4", "id4"); + + // 验证id1仍然存在,id2被淘汰 + assertNotNull(cache.getMcpServerById("id1")); + assertNull(cache.getMcpServerById("id2")); + assertNotNull(cache.getMcpServerById("id3")); + assertNotNull(cache.getMcpServerById("id4")); + } + + @Test + void testShutdown() { + // 添加一些数据 + cache.updateIndex("ns", "name", "id1"); + assertEquals(1, cache.getSize()); + + // 调用shutdown + cache.shutdown(); + + // 验证缓存被清空 + assertEquals(0, cache.getSize()); + assertNull(cache.getMcpId("ns", "name")); + assertNull(cache.getMcpServerById("id1")); + } + + @Test + void testShutdownTimeout() throws InterruptedException { + ScheduledExecutorService executorService = (ScheduledExecutorService) ReflectionTestUtils.getField(cache, + "cleanupScheduler"); + executorService.shutdownNow(); + ScheduledExecutorService mockExecutorService = Mockito.mock(ScheduledExecutorService.class); + ReflectionTestUtils.setField(cache, "cleanupScheduler", mockExecutorService); + cache.shutdown(); + verify(mockExecutorService).shutdownNow(); + } + + @Test + void testShutdownWithInterruptedException() throws InterruptedException { + ScheduledExecutorService executorService = (ScheduledExecutorService) ReflectionTestUtils.getField(cache, + "cleanupScheduler"); + executorService.shutdownNow(); + ScheduledExecutorService mockExecutorService = Mockito.mock(ScheduledExecutorService.class); + when(mockExecutorService.awaitTermination(anyLong(), any())).thenThrow(new InterruptedException()); + ReflectionTestUtils.setField(cache, "cleanupScheduler", mockExecutorService); + cache.shutdown(); + verify(mockExecutorService).shutdownNow(); + } + + @Test + void testDuplicateShutdown() throws InterruptedException { + cache.shutdown(); + ScheduledExecutorService mockExecutorService = Mockito.mock(ScheduledExecutorService.class); + ReflectionTestUtils.setField(cache, "cleanupScheduler", mockExecutorService); + cache.shutdown(); + verify(mockExecutorService, never()).awaitTermination(anyLong(), any()); + } + + @Test + void testNoExpiredEntries() throws InterruptedException { + McpCacheIndexProperties shortExpireProps = new McpCacheIndexProperties(); + shortExpireProps.setMaxSize(100); + shortExpireProps.setExpireTimeSeconds(-1); // 1秒过期 + shortExpireProps.setCleanupIntervalSeconds(1); + MemoryMcpCacheIndex noExpireCache = new MemoryMcpCacheIndex(shortExpireProps); + try { + noExpireCache.updateIndex("ns", "name", "id1"); + assertEquals("id1", noExpireCache.getMcpId("ns", "name")); + + Thread.sleep(1500); + assertEquals("id1", noExpireCache.getMcpId("ns", "name")); + } finally { + noExpireCache.shutdown(); + } + } + + @Test + void testCleanupExpiredEntries() throws InterruptedException { + // 创建一个具有短过期时间的缓存实例 + McpCacheIndexProperties shortExpireProps = new McpCacheIndexProperties(); + shortExpireProps.setMaxSize(100); + shortExpireProps.setExpireTimeSeconds(1); // 1秒过期 + shortExpireProps.setCleanupIntervalSeconds(1); + MemoryMcpCacheIndex shortExpireCache = new MemoryMcpCacheIndex(shortExpireProps); + + try { + // 添加一些条目 + shortExpireCache.updateIndex("ns1", "name1", "id1"); + shortExpireCache.updateIndex("ns2", "name2", "id2"); + shortExpireCache.updateIndex("ns3", "name3", "id3"); + + // 验证条目存在 + assertEquals("id1", shortExpireCache.getMcpId("ns1", "name1")); + assertEquals("id2", shortExpireCache.getMcpId("ns2", "name2")); + assertEquals("id3", shortExpireCache.getMcpId("ns3", "name3")); + + // 等待过期和清理 + Thread.sleep(1500); + + // 触发清理(通过获取来间接触发) + shortExpireCache.getMcpId("ns1", "name1"); + + // 验证过期条目被清理 + assertNull(shortExpireCache.getMcpId("ns1", "name1")); + assertNull(shortExpireCache.getMcpId("ns2", "name2")); + assertNull(shortExpireCache.getMcpId("ns3", "name3")); + + // 验证统计信息 + McpCacheIndex.CacheStats stats = shortExpireCache.getStats(); + assertEquals(0, stats.getSize()); + } finally { + shortExpireCache.shutdown(); + } + } + + @Test + void testCleanupExpiredEntriesDoesNotAffectValidEntries() throws InterruptedException { + // 创建一个具有不同过期时间的缓存实例 + McpCacheIndexProperties mixedProps = new McpCacheIndexProperties(); + mixedProps.setMaxSize(100); + mixedProps.setExpireTimeSeconds(2); // 2秒过期 + mixedProps.setCleanupIntervalSeconds(1); + MemoryMcpCacheIndex mixedCache = new MemoryMcpCacheIndex(mixedProps); + + try { + // 添加一些条目 + mixedCache.updateIndex("ns1", "name1", "id1"); // 这个会过期 + Thread.sleep(1100); // 等待1.1秒 + mixedCache.updateIndex("ns2", "name2", "id2"); // 这个不会过期 + + // 验证两个条目都存在 + assertEquals("id1", mixedCache.getMcpId("ns1", "name1")); + assertEquals("id2", mixedCache.getMcpId("ns2", "name2")); + + // 再等待1.1秒,使第一个条目过期但第二个不过期 + Thread.sleep(1100); + + // 触发清理(通过获取来间接触发) + mixedCache.getMcpId("ns1", "name1"); + + // 验证只有过期的条目被清理 + assertNull(mixedCache.getMcpId("ns1", "name1")); + assertEquals("id2", mixedCache.getMcpId("ns2", "name2")); + + // 验证统计数据 + McpCacheIndex.CacheStats stats = mixedCache.getStats(); + assertEquals(1, stats.getSize()); + } finally { + mixedCache.shutdown(); + } + } + + @Test + void testConcurrentAccessDuringCleanup() throws InterruptedException { + // 创建一个具有短过期时间的缓存实例 + McpCacheIndexProperties concurrentProps = new McpCacheIndexProperties(); + concurrentProps.setMaxSize(100); + concurrentProps.setExpireTimeSeconds(1); // 1秒过期 + concurrentProps.setCleanupIntervalSeconds(1); + MemoryMcpCacheIndex concurrentCache = new MemoryMcpCacheIndex(concurrentProps); + + try { + int threadCount = 5; + int opCount = 20; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + // 添加初始数据 + for (int i = 0; i < opCount; i++) { + concurrentCache.updateIndex("ns", "name" + i, "id" + i); + } + + // 并发执行读写和清理操作 + for (int i = 0; i < threadCount; i++) { + final int threadIndex = i; + executor.submit(() -> { + try { + for (int j = 0; j < opCount; j++) { + int index = threadIndex * opCount + j; + + // 读取操作 + concurrentCache.getMcpId("ns", "name" + (index % opCount)); + concurrentCache.getMcpServerById("id" + (index % opCount)); + + // 写入操作 + concurrentCache.updateIndex("ns", "newname" + index, "newid" + index); + + // 删除操作 + if (index % 2 == 0) { + concurrentCache.removeIndex("ns", "newname" + index); + } else { + concurrentCache.removeIndex("newid" + index); + } + } + } catch (Exception e) { + // 忽略并发访问中可能发生的异常 + } finally { + latch.countDown(); + } + }); + } + + // 等待所有线程完成 + boolean completed = latch.await(30, TimeUnit.SECONDS); + assertTrue(completed, "All threads should complete within timeout"); + + executor.shutdown(); + boolean terminated = executor.awaitTermination(10, TimeUnit.SECONDS); + assertTrue(terminated, "Executor should terminate within timeout"); + + // 验证缓存仍然可以正常工作 + concurrentCache.updateIndex("final", "final", "final-id"); + assertEquals("final-id", concurrentCache.getMcpId("final", "final")); + } finally { + concurrentCache.shutdown(); + } + } + + @Test + void testCleanupExpiredEntriesWithException() throws InterruptedException { + McpCacheIndexProperties concurrentProps = new McpCacheIndexProperties(); + concurrentProps.setMaxSize(100); + concurrentProps.setExpireTimeSeconds(1); // 1秒过期 + concurrentProps.setCleanupIntervalSeconds(1); + MemoryMcpCacheIndex testCache = new MemoryMcpCacheIndex(concurrentProps); + try { + testCache.updateIndex("ns", "name", "id"); + ReflectionTestUtils.setField(testCache, "properties", null); + TimeUnit.MILLISECONDS.sleep(1100); + } finally { + testCache.shutdown(); + } + } + + @Test + void testCleanupExpiredEntriesAfterShutdown() throws InterruptedException { + McpCacheIndexProperties concurrentProps = new McpCacheIndexProperties(); + concurrentProps.setMaxSize(100); + concurrentProps.setExpireTimeSeconds(1); // 1秒过期 + concurrentProps.setCleanupIntervalSeconds(1); + MemoryMcpCacheIndex testCache = new MemoryMcpCacheIndex(concurrentProps); + try { + ReflectionTestUtils.setField(testCache, "shutdown", true); + TimeUnit.MILLISECONDS.sleep(1100); + } finally { + ScheduledExecutorService cleanupScheduler = (ScheduledExecutorService) ReflectionTestUtils.getField(testCache, + "cleanupScheduler"); + cleanupScheduler.shutdownNow(); + } + } +} \ No newline at end of file diff --git a/ai/src/test/java/com/alibaba/nacos/ai/index/PlainMcpServerIndexTest.java b/ai/src/test/java/com/alibaba/nacos/ai/index/PlainMcpServerIndexTest.java index 1dd01810a5..0a8da4c358 100644 --- a/ai/src/test/java/com/alibaba/nacos/ai/index/PlainMcpServerIndexTest.java +++ b/ai/src/test/java/com/alibaba/nacos/ai/index/PlainMcpServerIndexTest.java @@ -214,6 +214,17 @@ class PlainMcpServerIndexTest { assertDoesNotThrow(() -> UUID.fromString(result.getId())); } + @Test + void removeMcpServerByName() { + assertDoesNotThrow( + () -> plainMcpServerIndex.removeMcpServerByName(AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, "mcpName")); + } + + @Test + void removeMcpServerById() { + assertDoesNotThrow(() -> plainMcpServerIndex.removeMcpServerById(UUID.randomUUID().toString())); + } + private ConfigQueryChainResponse mockConfigQueryChainResponse(Object obj) { ConfigQueryChainResponse mockResponse = new ConfigQueryChainResponse(); if (null != obj) { diff --git a/ai/src/test/java/com/alibaba/nacos/ai/param/AgentHttpParamExtractorTest.java b/ai/src/test/java/com/alibaba/nacos/ai/param/AgentHttpParamExtractorTest.java new file mode 100644 index 0000000000..d923cb5d36 --- /dev/null +++ b/ai/src/test/java/com/alibaba/nacos/ai/param/AgentHttpParamExtractorTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 1999-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.ai.param; + +import com.alibaba.nacos.api.ai.model.a2a.AgentCapabilities; +import com.alibaba.nacos.api.ai.model.a2a.AgentCard; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.common.paramcheck.ParamInfo; +import com.alibaba.nacos.common.utils.JacksonUtils; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AgentHttpParamExtractorTest { + + @Mock + HttpServletRequest request; + + AgentHttpParamExtractor httpParamExtractor; + + @BeforeEach + void setUp() { + httpParamExtractor = new AgentHttpParamExtractor(); + } + + @Test + void extractParamWithNamespaceIdAndAgentName() throws NacosException { + String agentName = "testAgent"; + when(request.getParameter("namespaceId")).thenReturn("testNs"); + when(request.getParameter("agentName")).thenReturn(agentName); + when(request.getParameterMap()).thenReturn( + Map.of("namespaceId", new String[] {"testNs"}, "agentName", new String[] {agentName})); + + List actual = httpParamExtractor.extractParam(request); + assertEquals(1, actual.size()); + assertEquals("testNs", actual.get(0).getNamespaceId()); + assertEquals(agentName, actual.get(0).getAgentName()); + } + + @Test + void extractParamWithAgentCard() throws NacosException { + AgentCard agentCard = new AgentCard(); + agentCard.setName("testAgentFromCard"); + agentCard.setDescription("Test agent card"); + + AgentCapabilities capabilities = new AgentCapabilities(); + agentCard.setCapabilities(capabilities); + + String agentCardJson = JacksonUtils.toJson(agentCard); + + when(request.getParameter("namespaceId")).thenReturn("testNs"); + when(request.getParameter("agentName")).thenReturn("shouldBeOverridden"); + when(request.getParameter("agentCard")).thenReturn(agentCardJson); + when(request.getParameterMap()).thenReturn( + Map.of("namespaceId", new String[] {"testNs"}, "agentName", new String[] {"shouldBeOverridden"}, + "agentCard", new String[] {agentCardJson})); + + List actual = httpParamExtractor.extractParam(request); + assertEquals(1, actual.size()); + assertEquals("testNs", actual.get(0).getNamespaceId()); + assertEquals("testAgentFromCard", actual.get(0).getAgentName()); + } + + @Test + void extractParamWithInvalidAgentCardJson() throws NacosException { + when(request.getParameter("namespaceId")).thenReturn("testNs"); + when(request.getParameter("agentName")).thenReturn("testAgent"); + when(request.getParameter("agentCard")).thenReturn("{invalidJson"); + when(request.getParameterMap()).thenReturn( + Map.of("namespaceId", new String[] {"testNs"}, "agentName", new String[] {"testAgent"}, "agentCard", + new String[] {"{invalidJson"})); + + List actual = httpParamExtractor.extractParam(request); + assertEquals(1, actual.size()); + assertEquals("testNs", actual.get(0).getNamespaceId()); + assertEquals("", actual.get(0).getAgentName()); + } + + @Test + void extractParamWithEmptyParameters() throws NacosException { + when(request.getParameterMap()).thenReturn(new java.util.HashMap<>()); + + List actual = httpParamExtractor.extractParam(request); + assertEquals(1, actual.size()); + assertTrue(actual.get(0).getNamespaceId() == null || actual.get(0).getNamespaceId().isEmpty()); + assertTrue(actual.get(0).getAgentName() == null || actual.get(0).getAgentName().isEmpty()); + } +} \ No newline at end of file diff --git a/ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/AgentEndpointRequestHandlerTest.java b/ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/AgentEndpointRequestHandlerTest.java new file mode 100644 index 0000000000..e6e8afcbe0 --- /dev/null +++ b/ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/AgentEndpointRequestHandlerTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 1999-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.ai.remote.handler.a2a; + +import com.alibaba.nacos.ai.service.a2a.identity.AgentIdCodecHolder; +import com.alibaba.nacos.api.ai.model.a2a.AgentEndpoint; +import com.alibaba.nacos.api.ai.remote.AiRemoteConstants; +import com.alibaba.nacos.api.ai.remote.request.AgentEndpointRequest; +import com.alibaba.nacos.api.ai.remote.response.AgentEndpointResponse; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.naming.pojo.Instance; +import com.alibaba.nacos.api.remote.request.RequestMeta; +import com.alibaba.nacos.api.remote.response.ResponseCode; +import com.alibaba.nacos.naming.core.v2.pojo.Service; +import com.alibaba.nacos.naming.core.v2.service.impl.EphemeralClientOperationServiceImpl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AgentEndpointRequestHandlerTest { + + @Mock + private EphemeralClientOperationServiceImpl clientOperationService; + + @Mock + private AgentIdCodecHolder agentIdCodecHolder; + + @Mock + private RequestMeta meta; + + private AgentEndpointRequestHandler requestHandler; + + @BeforeEach + void setUp() { + requestHandler = new AgentEndpointRequestHandler(clientOperationService, agentIdCodecHolder); + } + + @AfterEach + void tearDown() { + } + + @Test + void handleWithInvalidAgentName() throws NacosException { + AgentEndpointRequest request = new AgentEndpointRequest(); + AgentEndpointResponse response = requestHandler.handle(request, meta); + assertErrorResponse(response, NacosException.INVALID_PARAM, + "Required parameter `agentName` can't be empty or null"); + } + + @Test + void handleWithNullEndpoint() throws NacosException { + AgentEndpointRequest request = new AgentEndpointRequest(); + request.setAgentName("test"); + AgentEndpointResponse response = requestHandler.handle(request, meta); + assertErrorResponse(response, NacosException.INVALID_PARAM, "Required parameter `endpoint` can't be null"); + } + + @Test + void handleWithEmptyEndpointVersion() throws NacosException { + AgentEndpointRequest request = new AgentEndpointRequest(); + request.setAgentName("test"); + AgentEndpoint endpoint = new AgentEndpoint(); + endpoint.setAddress("1.1.1.1"); + endpoint.setPort(8080); + request.setEndpoint(endpoint); + AgentEndpointResponse response = requestHandler.handle(request, meta); + assertErrorResponse(response, NacosException.INVALID_PARAM, + "Required parameter `endpoint.version` can't be empty or null"); + } + + @Test + void handleWithInvalidType() throws NacosException { + AgentEndpointRequest request = new AgentEndpointRequest(); + request.setAgentName("test"); + AgentEndpoint endpoint = new AgentEndpoint(); + endpoint.setAddress("1.1.1.1"); + endpoint.setPort(8080); + endpoint.setVersion("1.0.0"); + request.setEndpoint(endpoint); + request.setType("INVALID_TYPE"); + when(agentIdCodecHolder.encode("test")).thenReturn("test"); + AgentEndpointResponse response = requestHandler.handle(request, meta); + assertErrorResponse(response, NacosException.INVALID_PARAM, + "parameter `type` should be registerEndpoint or deregisterEndpoint, but was INVALID_TYPE"); + } + + @Test + void handleForRegisterEndpoint() throws NacosException { + AgentEndpointRequest request = new AgentEndpointRequest(); + request.setAgentName("test"); + request.setNamespaceId("public"); + AgentEndpoint endpoint = new AgentEndpoint(); + endpoint.setAddress("1.1.1.1"); + endpoint.setPort(8080); + endpoint.setVersion("1.0.0"); + endpoint.setPath("/test"); + endpoint.setTransport("JSONRPC"); + endpoint.setSupportTls(false); + request.setEndpoint(endpoint); + request.setType(AiRemoteConstants.REGISTER_ENDPOINT); + when(agentIdCodecHolder.encode("test")).thenReturn("test"); + when(meta.getConnectionId()).thenReturn("TEST_CONNECTION_ID"); + AgentEndpointResponse response = requestHandler.handle(request, meta); + assertEquals(AiRemoteConstants.REGISTER_ENDPOINT, response.getType()); + verify(clientOperationService).registerInstance(any(Service.class), any(Instance.class), + eq("TEST_CONNECTION_ID")); + } + + @Test + void handleForDeregisterEndpoint() throws NacosException { + AgentEndpointRequest request = new AgentEndpointRequest(); + request.setAgentName("test"); + request.setNamespaceId("public"); + AgentEndpoint endpoint = new AgentEndpoint(); + endpoint.setAddress("1.1.1.1"); + endpoint.setPort(8080); + endpoint.setVersion("1.0.0"); + endpoint.setPath("/test"); + endpoint.setTransport("JSONRPC"); + endpoint.setSupportTls(false); + request.setEndpoint(endpoint); + request.setType(AiRemoteConstants.DE_REGISTER_ENDPOINT); + when(agentIdCodecHolder.encode("test")).thenReturn("test"); + when(meta.getConnectionId()).thenReturn("TEST_CONNECTION_ID"); + AgentEndpointResponse response = requestHandler.handle(request, meta); + assertEquals(AiRemoteConstants.DE_REGISTER_ENDPOINT, response.getType()); + verify(clientOperationService).deregisterInstance(any(Service.class), any(Instance.class), + eq("TEST_CONNECTION_ID")); + } + + private void assertErrorResponse(AgentEndpointResponse response, int code, String message) { + assertEquals(ResponseCode.FAIL.getCode(), response.getResultCode()); + assertEquals(code, response.getErrorCode()); + assertEquals(message, response.getMessage()); + } +} \ No newline at end of file diff --git a/ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/QueryAgentCardRequestHandlerTest.java b/ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/QueryAgentCardRequestHandlerTest.java new file mode 100644 index 0000000000..b928e4d219 --- /dev/null +++ b/ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/QueryAgentCardRequestHandlerTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 1999-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.ai.remote.handler.a2a; + +import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService; +import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo; +import com.alibaba.nacos.api.ai.remote.request.QueryAgentCardRequest; +import com.alibaba.nacos.api.ai.remote.response.QueryAgentCardResponse; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.exception.api.NacosApiException; +import com.alibaba.nacos.api.model.v2.ErrorCode; +import com.alibaba.nacos.api.remote.request.RequestMeta; +import com.alibaba.nacos.api.remote.response.ResponseCode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class QueryAgentCardRequestHandlerTest { + + @Mock + private A2aServerOperationService a2aServerOperationService; + + @Mock + private RequestMeta meta; + + private QueryAgentCardRequestHandler requestHandler; + + @BeforeEach + void setUp() { + requestHandler = new QueryAgentCardRequestHandler(a2aServerOperationService); + } + + @AfterEach + void tearDown() { + } + + @Test + void handleWithInvalidAgentName() throws NacosException { + QueryAgentCardRequest request = new QueryAgentCardRequest(); + QueryAgentCardResponse response = requestHandler.handle(request, meta); + assertEquals(ResponseCode.FAIL.getCode(), response.getResultCode()); + assertEquals(NacosException.INVALID_PARAM, response.getErrorCode()); + assertEquals("parameters `agentName` can't be empty or null", response.getMessage()); + } + + @Test + void handleWithValidParameters() throws NacosException { + QueryAgentCardRequest request = new QueryAgentCardRequest(); + request.setAgentName("test"); + request.setNamespaceId("public"); + AgentCardDetailInfo mockAgentCard = new AgentCardDetailInfo(); + mockAgentCard.setName("test"); + when(a2aServerOperationService.getAgentCard("public", "test", null, null)).thenReturn(mockAgentCard); + QueryAgentCardResponse response = requestHandler.handle(request, meta); + assertEquals(mockAgentCard, response.getAgentCardDetailInfo()); + assertNull(response.getMessage()); + } + + @Test + void handleWithException() throws NacosException { + QueryAgentCardRequest request = new QueryAgentCardRequest(); + request.setAgentName("test"); + request.setNamespaceId("public"); + when(a2aServerOperationService.getAgentCard("public", "test", null, null)).thenThrow( + new NacosApiException(NacosException.SERVER_ERROR, ErrorCode.SERVER_ERROR, "test error")); + QueryAgentCardResponse response = requestHandler.handle(request, meta); + assertEquals(ResponseCode.FAIL.getCode(), response.getResultCode()); + assertEquals(NacosException.SERVER_ERROR, response.getErrorCode()); + assertEquals("test error", response.getMessage()); + } +} \ No newline at end of file diff --git a/ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/ReleaseAgentCardRequestHandlerTest.java b/ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/ReleaseAgentCardRequestHandlerTest.java new file mode 100644 index 0000000000..fb9936144f --- /dev/null +++ b/ai/src/test/java/com/alibaba/nacos/ai/remote/handler/a2a/ReleaseAgentCardRequestHandlerTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 1999-2025 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.nacos.ai.remote.handler.a2a; + +import com.alibaba.nacos.ai.service.a2a.A2aServerOperationService; +import com.alibaba.nacos.api.ai.model.a2a.AgentCard; +import com.alibaba.nacos.api.ai.model.a2a.AgentCardDetailInfo; +import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest; +import com.alibaba.nacos.api.ai.remote.response.ReleaseAgentCardResponse; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.exception.api.NacosApiException; +import com.alibaba.nacos.api.model.v2.ErrorCode; +import com.alibaba.nacos.api.remote.request.RequestMeta; +import com.alibaba.nacos.api.remote.response.ResponseCode; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ReleaseAgentCardRequestHandlerTest { + + @Mock + private A2aServerOperationService a2aServerOperationService; + + @Mock + private RequestMeta meta; + + private ReleaseAgentCardRequestHandler requestHandler; + + @BeforeEach + void setUp() { + requestHandler = new ReleaseAgentCardRequestHandler(a2aServerOperationService); + } + + @AfterEach + void tearDown() { + } + + @Test + void handleWithNullAgentCard() throws NacosException { + ReleaseAgentCardRequest request = new ReleaseAgentCardRequest(); + ReleaseAgentCardResponse response = requestHandler.handle(request, meta); + assertEquals(ResponseCode.FAIL.getCode(), response.getResultCode()); + assertEquals(NacosException.INVALID_PARAM, response.getErrorCode()); + assertEquals("parameters `agentCard` can't be null", response.getMessage()); + } + + @Test + void handleWithValidNewAgentCard() throws NacosException { + final ReleaseAgentCardRequest request = new ReleaseAgentCardRequest(); + AgentCard agentCard = new AgentCard(); + agentCard.setName("test"); + agentCard.setVersion("1.0.0"); + agentCard.setProtocolVersion("0.3.0"); + agentCard.setPreferredTransport("JSONRPC"); + agentCard.setUrl("https://example.com"); + request.setAgentCard(agentCard); + request.setNamespaceId("public"); + when(meta.getConnectionId()).thenReturn("TEST_CONNECTION_ID"); + when(a2aServerOperationService.getAgentCard("public", "test", "1.0.0", "")).thenThrow( + new NacosApiException(NacosException.NOT_FOUND, ErrorCode.AGENT_NOT_FOUND, "")); + ReleaseAgentCardResponse response = requestHandler.handle(request, meta); + assertEquals(ResponseCode.SUCCESS.getCode(), response.getResultCode()); + assertNull(response.getMessage()); + verify(a2aServerOperationService).registerAgent(any(AgentCard.class), anyString(), anyString()); + } + + @Test + void handleWithValidNewVersionAgentCard() throws NacosException { + final ReleaseAgentCardRequest request = new ReleaseAgentCardRequest(); + AgentCard agentCard = new AgentCard(); + agentCard.setName("test"); + agentCard.setVersion("1.0.0"); + agentCard.setProtocolVersion("0.3.0"); + agentCard.setPreferredTransport("JSONRPC"); + agentCard.setUrl("https://example.com"); + request.setAgentCard(agentCard); + request.setNamespaceId("public"); + request.setSetAsLatest(true); + when(meta.getConnectionId()).thenReturn("TEST_CONNECTION_ID"); + AgentCardDetailInfo existAgentCard = new AgentCardDetailInfo(); + existAgentCard.setName("test"); + existAgentCard.setVersion("0.9.0"); + when(a2aServerOperationService.getAgentCard("public", "test", "1.0.0", "")).thenThrow( + new NacosApiException(NacosException.NOT_FOUND, ErrorCode.AGENT_VERSION_NOT_FOUND, "")); + ReleaseAgentCardResponse response = requestHandler.handle(request, meta); + assertEquals(ResponseCode.SUCCESS.getCode(), response.getResultCode()); + assertNull(response.getMessage()); + verify(a2aServerOperationService).updateAgentCard(any(AgentCard.class), anyString(), anyString(), eq(true)); + } + + @Test + void handleWithExistingAgentCard() throws NacosException { + final ReleaseAgentCardRequest request = new ReleaseAgentCardRequest(); + AgentCard agentCard = new AgentCard(); + agentCard.setName("test"); + agentCard.setVersion("1.0.0"); + agentCard.setProtocolVersion("0.3.0"); + agentCard.setPreferredTransport("JSONRPC"); + agentCard.setUrl("https://example.com"); + request.setAgentCard(agentCard); + request.setNamespaceId("public"); + when(meta.getConnectionId()).thenReturn("TEST_CONNECTION_ID"); + AgentCardDetailInfo existAgentCard = new AgentCardDetailInfo(); + existAgentCard.setName("test"); + existAgentCard.setVersion("1.0.0"); + when(a2aServerOperationService.getAgentCard("public", "test", "1.0.0", "")).thenReturn(existAgentCard); + ReleaseAgentCardResponse response = requestHandler.handle(request, meta); + assertEquals(ResponseCode.SUCCESS.getCode(), response.getResultCode()); + verify(a2aServerOperationService, never()).registerAgent(any(AgentCard.class), anyString(), anyString()); + verify(a2aServerOperationService, never()).updateAgentCard(any(AgentCard.class), anyString(), anyString(), + anyBoolean()); + } + + @Test + void handleWithOtherException() throws NacosException { + final ReleaseAgentCardRequest request = new ReleaseAgentCardRequest(); + AgentCard agentCard = new AgentCard(); + agentCard.setName("test"); + agentCard.setVersion("1.0.0"); + agentCard.setProtocolVersion("0.3.0"); + agentCard.setPreferredTransport("JSONRPC"); + agentCard.setUrl("https://example.com"); + request.setAgentCard(agentCard); + request.setNamespaceId("public"); + when(meta.getConnectionId()).thenReturn("TEST_CONNECTION_ID"); + when(a2aServerOperationService.getAgentCard("public", "test", "1.0.0", "")).thenThrow( + new NacosApiException(NacosException.SERVER_ERROR, ErrorCode.SERVER_ERROR, "test error")); + ReleaseAgentCardResponse response = requestHandler.handle(request, meta); + assertEquals(ResponseCode.FAIL.getCode(), response.getResultCode()); + assertEquals(NacosException.SERVER_ERROR, response.getErrorCode()); + assertEquals("test error", response.getMessage()); + verify(a2aServerOperationService, never()).registerAgent(any(AgentCard.class), anyString(), anyString()); + verify(a2aServerOperationService, never()).updateAgentCard(any(AgentCard.class), anyString(), anyString(), + anyBoolean()); + } +} \ No newline at end of file diff --git a/ai/src/test/java/com/alibaba/nacos/ai/service/McpServerImportServiceTest.java b/ai/src/test/java/com/alibaba/nacos/ai/service/McpServerImportServiceTest.java index f5bcfbe1e5..aa4d7b9591 100644 --- a/ai/src/test/java/com/alibaba/nacos/ai/service/McpServerImportServiceTest.java +++ b/ai/src/test/java/com/alibaba/nacos/ai/service/McpServerImportServiceTest.java @@ -16,7 +16,7 @@ package com.alibaba.nacos.ai.service; -import com.alibaba.nacos.ai.constants.McpServerValidationConstants; +import com.alibaba.nacos.ai.constant.McpServerValidationConstants; import com.alibaba.nacos.api.ai.model.mcp.McpServerDetailInfo; import com.alibaba.nacos.api.ai.model.mcp.McpServerImportRequest; import com.alibaba.nacos.api.ai.model.mcp.McpServerImportResponse;