Add some unit test for ai module. (#13873)

Change-Id: I466335abb694620be66b650666b15f96d090f524
This commit is contained in:
杨翊 SionYang 2025-09-30 17:00:03 +08:00 committed by GitHub
parent 08da133e55
commit 03e98934d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 2027 additions and 98 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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");

View File

@ -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.
*
* <p>
* TODO This Memory cache might include some design issues:
* <ul>
* <li>
* 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.
* </li>
* <li>
* 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.
* </li>
* <li>
* 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.
* </li>
* </ul>
* </p>
*
* @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;

View File

@ -70,6 +70,7 @@ public class AgentEndpointRequestHandler extends RequestHandler<AgentEndpointReq
@Secured(action = ActionTypes.WRITE, signType = SignType.AI)
public AgentEndpointResponse handle(AgentEndpointRequest request, RequestMeta meta) throws NacosException {
AgentEndpointResponse response = new AgentEndpointResponse();
response.setType(request.getType());
AgentRequestUtil.fillNamespaceId(request);
try {
validateRequest(request);

View File

@ -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.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.mcp.McpEndpointSpec;
import com.alibaba.nacos.api.ai.model.mcp.McpServerBasicInfo;

View File

@ -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.ai.index.McpServerIndex;
import com.alibaba.nacos.ai.model.mcp.McpServerIndexData;
import com.alibaba.nacos.api.ai.constant.AiConstants;

View File

@ -0,0 +1,41 @@
/*
* 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.config;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class McpCacheIndexPropertiesTest {
@Test
public void testToString() {
McpCacheIndexProperties properties = new McpCacheIndexProperties();
// Test default value.
assertEquals(
"McpCacheIndexProperties{enabled=true, maxSize=10000, expireTimeSeconds=3600, cleanupIntervalSeconds=300, syncIntervalSeconds=300}",
properties.toString());
properties.setEnabled(false);
properties.setCleanupIntervalSeconds(10);
properties.setSyncIntervalSeconds(10);
properties.setExpireTimeSeconds(10);
properties.setMaxSize(100);
assertEquals(
"McpCacheIndexProperties{enabled=false, maxSize=100, expireTimeSeconds=10, cleanupIntervalSeconds=10, syncIntervalSeconds=10}",
properties.toString());
}
}

View File

@ -0,0 +1,116 @@
/*
* 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.form.a2a.admin;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class AgentCardFormTest {
@Test
void testValidateSuccess() throws NacosApiException {
AgentCardForm agentCardForm = new AgentCardForm();
agentCardForm.setAgentName("test-agent");
agentCardForm.setAgentCard("{\"name\":\"test-agent\"}");
agentCardForm.validate();
// Should not throw exception
}
@Test
void testValidateWithEmptyAgentCardShouldThrowException() {
AgentCardForm agentCardForm = new AgentCardForm();
agentCardForm.setAgentName("test-agent");
agentCardForm.setAgentCard("");
assertThrows(NacosApiException.class, agentCardForm::validate);
}
@Test
void testValidateWithNullAgentCardShouldThrowException() {
AgentCardForm agentCardForm = new AgentCardForm();
agentCardForm.setAgentName("test-agent");
agentCardForm.setAgentCard(null);
assertThrows(NacosApiException.class, agentCardForm::validate);
}
@Test
void testValidateShouldFillDefaultNamespaceIdAndRegistrationType() throws NacosApiException {
AgentCardForm agentCardForm = new AgentCardForm();
agentCardForm.setAgentName("test-agent");
agentCardForm.setAgentCard("{\"name\":\"test-agent\"}");
agentCardForm.validate();
assertEquals("public", agentCardForm.getNamespaceId());
assertEquals("URL", agentCardForm.getRegistrationType());
}
@Test
void testValidateWithValidRegistrationType() throws NacosApiException {
AgentCardForm agentCardForm = new AgentCardForm();
agentCardForm.setAgentName("test-agent");
agentCardForm.setAgentCard("{\"name\":\"test-agent\"}");
agentCardForm.setRegistrationType("URL");
agentCardForm.validate();
// Should not throw exception
agentCardForm.setRegistrationType("SERVICE");
agentCardForm.validate();
// Should not throw exception
}
@Test
void testValidateWithInvalidRegistrationTypeShouldThrowException() {
AgentCardForm agentCardForm = new AgentCardForm();
agentCardForm.setAgentName("test-agent");
agentCardForm.setAgentCard("{\"name\":\"test-agent\"}");
agentCardForm.setRegistrationType("INVALID");
assertThrows(NacosApiException.class, agentCardForm::validate);
}
@Test
void testFillDefaultRegistrationType() {
AgentCardForm agentCardForm = new AgentCardForm();
agentCardForm.fillDefaultRegistrationType();
assertEquals("URL", agentCardForm.getRegistrationType());
}
@Test
void testFillDefaultRegistrationTypeWithExistingValue() {
AgentCardForm agentCardForm = new AgentCardForm();
agentCardForm.setRegistrationType("SERVICE");
agentCardForm.fillDefaultRegistrationType();
assertEquals("SERVICE", agentCardForm.getRegistrationType());
}
@Test
void testGetterAndSetter() {
AgentCardForm agentCardForm = new AgentCardForm();
agentCardForm.setNamespaceId("test-namespace");
agentCardForm.setAgentName("test-agent");
agentCardForm.setVersion("1.0.0");
agentCardForm.setRegistrationType("SERVICE");
agentCardForm.setAgentCard("{\"name\":\"test-agent\"}");
assertEquals("test-namespace", agentCardForm.getNamespaceId());
assertEquals("test-agent", agentCardForm.getAgentName());
assertEquals("1.0.0", agentCardForm.getVersion());
assertEquals("SERVICE", agentCardForm.getRegistrationType());
assertEquals("{\"name\":\"test-agent\"}", agentCardForm.getAgentCard());
}
}

View File

@ -0,0 +1,85 @@
/*
* 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.form.a2a.admin;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class AgentFormTest {
@Test
void testValidateSuccess() throws NacosApiException {
AgentForm agentForm = new AgentForm();
agentForm.setAgentName("test-agent");
agentForm.validate();
// Should not throw exception
}
@Test
void testValidateWithEmptyNameShouldThrowException() {
AgentForm agentForm = new AgentForm();
assertThrows(NacosApiException.class, agentForm::validate);
}
@Test
void testValidateWithNullNameShouldThrowException() {
AgentForm agentForm = new AgentForm();
agentForm.setAgentName(null);
assertThrows(NacosApiException.class, agentForm::validate);
}
@Test
void testFillDefaultNamespaceId() {
AgentForm agentForm = new AgentForm();
agentForm.fillDefaultNamespaceId();
assertEquals("public", agentForm.getNamespaceId());
}
@Test
void testFillDefaultNamespaceIdWithExistingValue() {
AgentForm agentForm = new AgentForm();
agentForm.setNamespaceId("test-namespace");
agentForm.fillDefaultNamespaceId();
assertEquals("test-namespace", agentForm.getNamespaceId());
}
@Test
void testValidateShouldFillDefaultNamespaceId() throws NacosApiException {
AgentForm agentForm = new AgentForm();
agentForm.setAgentName("test-agent");
agentForm.validate();
assertEquals("public", agentForm.getNamespaceId());
}
@Test
void testGetterAndSetter() {
AgentForm agentForm = new AgentForm();
agentForm.setNamespaceId("test-namespace");
agentForm.setAgentName("test-agent");
agentForm.setVersion("1.0.0");
agentForm.setRegistrationType("URL");
assertEquals("test-namespace", agentForm.getNamespaceId());
assertEquals("test-agent", agentForm.getAgentName());
assertEquals("1.0.0", agentForm.getVersion());
assertEquals("URL", agentForm.getRegistrationType());
}
}

View File

@ -0,0 +1,99 @@
/*
* 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.form.a2a.admin;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class AgentListFormTest {
@Test
void testValidateWithAccurateSearch() throws NacosApiException {
AgentListForm agentListForm = new AgentListForm();
agentListForm.setAgentName("test-agent");
agentListForm.setSearch("accurate");
agentListForm.validate();
// Should not throw exception
}
@Test
void testValidateWithBlurSearch() throws NacosApiException {
AgentListForm agentListForm = new AgentListForm();
agentListForm.setAgentName("test-agent");
agentListForm.setSearch("blur");
agentListForm.validate();
// Should not throw exception
}
@Test
void testValidateWithUpperCaseSearch() throws NacosApiException {
AgentListForm agentListForm = new AgentListForm();
agentListForm.setAgentName("test-agent");
agentListForm.setSearch("ACCURATE");
agentListForm.validate();
// Should not throw exception
agentListForm.setSearch("BLUR");
agentListForm.validate();
// Should not throw exception
}
@Test
void testValidateWithInvalidSearchShouldThrowException() {
AgentListForm agentListForm = new AgentListForm();
agentListForm.setAgentName("test-agent");
agentListForm.setSearch("invalid");
assertThrows(NacosApiException.class, agentListForm::validate);
}
@Test
void testValidateWithNullSearchShouldThrowException() {
AgentListForm agentListForm = new AgentListForm();
agentListForm.setAgentName("test-agent");
agentListForm.setSearch(null);
assertThrows(NacosApiException.class, agentListForm::validate);
}
@Test
void testValidateShouldFillDefaultNamespaceId() throws NacosApiException {
AgentListForm agentListForm = new AgentListForm();
agentListForm.setAgentName("test-agent");
agentListForm.setSearch("accurate");
agentListForm.validate();
assertEquals("public", agentListForm.getNamespaceId());
}
@Test
void testGetterAndSetter() {
AgentListForm agentListForm = new AgentListForm();
agentListForm.setNamespaceId("test-namespace");
agentListForm.setAgentName("test-agent");
agentListForm.setVersion("1.0.0");
agentListForm.setRegistrationType("URL");
agentListForm.setSearch("accurate");
assertEquals("test-namespace", agentListForm.getNamespaceId());
assertEquals("test-agent", agentListForm.getAgentName());
assertEquals("1.0.0", agentListForm.getVersion());
assertEquals("URL", agentListForm.getRegistrationType());
assertEquals("accurate", agentListForm.getSearch());
}
}

View File

@ -0,0 +1,147 @@
/*
* 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.form.mcp.admin;
import com.alibaba.nacos.api.exception.api.NacosApiException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class McpImportFormTest {
@Test
void testValidateSuccessWithJsonType() throws NacosApiException {
McpImportForm form = new McpImportForm();
form.setImportType("json");
form.setData("{\"test\": \"data\"}");
form.validate();
// Should not throw exception
}
@Test
void testValidateSuccessWithUrlType() throws NacosApiException {
McpImportForm form = new McpImportForm();
form.setImportType("url");
form.setData("http://example.com/registry.json");
form.validate();
// Should not throw exception
}
@Test
void testValidateSuccessWithFileType() throws NacosApiException {
McpImportForm form = new McpImportForm();
form.setImportType("file");
form.setData("/path/to/registry.json");
form.validate();
// Should not throw exception
}
@Test
void testValidateWithEmptyImportTypeShouldThrowException() {
McpImportForm form = new McpImportForm();
form.setImportType("");
form.setData("{\"test\": \"data\"}");
assertThrows(NacosApiException.class, form::validate);
}
@Test
void testValidateWithNullImportTypeShouldThrowException() {
McpImportForm form = new McpImportForm();
form.setImportType(null);
form.setData("{\"test\": \"data\"}");
assertThrows(NacosApiException.class, form::validate);
}
@Test
void testValidateWithEmptyDataShouldThrowException() {
McpImportForm form = new McpImportForm();
form.setImportType("json");
form.setData("");
assertThrows(NacosApiException.class, form::validate);
}
@Test
void testValidateWithNullDataShouldThrowException() {
McpImportForm form = new McpImportForm();
form.setImportType("json");
form.setData(null);
assertThrows(NacosApiException.class, form::validate);
}
@Test
void testValidateWithInvalidImportTypeShouldThrowException() {
McpImportForm form = new McpImportForm();
form.setImportType("invalid");
form.setData("{\"test\": \"data\"}");
assertThrows(NacosApiException.class, form::validate);
}
@Test
void testValidateShouldFillDefaultValue() throws NacosApiException {
McpImportForm form = new McpImportForm();
form.setImportType("json");
form.setData("{\"test\": \"data\"}");
form.validate();
assertEquals("public", form.getNamespaceId());
}
@Test
void testGetterAndSetter() {
McpImportForm form = new McpImportForm();
// Test basic fields
form.setImportType("json");
form.setData("{\"test\": \"data\"}");
form.setOverrideExisting(true);
form.setValidateOnly(true);
form.setSkipInvalid(true);
form.setCursor("cursor123");
form.setLimit(10);
form.setSearch("test");
String[] selectedServers = {"server1", "server2"};
form.setSelectedServers(selectedServers);
assertEquals("json", form.getImportType());
assertEquals("{\"test\": \"data\"}", form.getData());
assertTrue(form.isOverrideExisting());
assertTrue(form.isValidateOnly());
assertTrue(form.isSkipInvalid());
assertArrayEquals(selectedServers, form.getSelectedServers());
assertEquals("cursor123", form.getCursor());
assertEquals(10, form.getLimit());
assertEquals("test", form.getSearch());
}
@Test
void testDefaultValueOfBooleanFields() {
McpImportForm form = new McpImportForm();
assertFalse(form.isOverrideExisting());
assertFalse(form.isValidateOnly());
assertFalse(form.isSkipInvalid());
assertNull(form.getSelectedServers());
assertNull(form.getCursor());
assertNull(form.getLimit());
assertNull(form.getSearch());
}
}

View File

@ -32,18 +32,24 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.mockito.stubbing.Answer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@ -436,4 +442,574 @@ class CachedMcpServerIndexTest {
// 空字符串应该仍然调用缓存删除方法
verify(cacheIndex).removeIndex("");
}
}
// 补充的测试用例
@Test
void testGetMcpServerByIdWithCacheDisabledAndNotFound() {
// 创建禁用缓存的实例
final CachedMcpServerIndex disabledIndex = new CachedMcpServerIndex(configDetailService,
namespaceOperationService, configQueryChainService, cacheIndex, scheduledExecutor, false, 0);
final String mcpId = "test-id-123";
// 模拟数据库查询结果为null
ConfigQueryChainResponse mockResponse = mock(ConfigQueryChainResponse.class);
when(mockResponse.getStatus()).thenReturn(ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_NOT_FOUND);
when(configQueryChainService.handle(any(ConfigQueryChainRequest.class))).thenReturn(mockResponse);
// 模拟命名空间列表
List<com.alibaba.nacos.api.model.response.Namespace> 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<com.alibaba.nacos.api.model.response.Namespace> 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<ConfigInfo> 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<ConfigInfo> 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<ConfigInfo> mockPage = new Page<>();
List<ConfigInfo> 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<McpServerIndexData> 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<ConfigInfo> mockPage = new Page<>();
List<ConfigInfo> 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<McpServerIndexData> 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<ConfigInfo> mockPage = new Page<>();
List<ConfigInfo> 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<McpServerIndexData> 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<ConfigInfo> mockPage = new Page<>();
List<ConfigInfo> 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<McpServerIndexData> 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<com.alibaba.nacos.api.model.response.Namespace> 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<ConfigInfo> mockPage = new Page<>();
List<ConfigInfo> 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<ScheduledFuture>) 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<ScheduledFuture>) 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<com.alibaba.nacos.api.model.response.Namespace> 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<ConfigInfo> mockPage1 = new Page<>();
List<ConfigInfo> 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<ConfigInfo> mockPage2 = new Page<>();
List<ConfigInfo> 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<com.alibaba.nacos.api.model.response.Namespace> 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<com.alibaba.nacos.api.model.response.Namespace> 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<ConfigInfo> 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<com.alibaba.nacos.api.model.response.Namespace> 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();
}
}

View File

@ -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());
}
}
// 补充测试用例
@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();
}
}
}

View File

@ -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) {

View File

@ -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<ParamInfo> 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<ParamInfo> 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<ParamInfo> 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<ParamInfo> 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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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;