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:
+ *
+ * -
+ * 1. The read method in cache include LRU operation(write), which means read lock can't intercept write operation
+ * in multiple threads reading and cause thread-safe problem.
+ *
+ * -
+ * 2. For solve problem 1. Use {@code synchronized} wrapper {@link #removeFromLru} and {@link #moveToHead} method,
+ * which may cause the read operation performance will be affected in high qps.
+ *
+ * -
+ * 3. The next consider it whether keep the LRU behavior in next versions when qps improved. If keep it, the LRU cache should
+ * be re-designed or use stabled high performance LRU cache such as guava.
+ *
+ *
+ *
+ *
* @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;