mirror of https://github.com/alibaba/nacos.git
Add some unit test for ai module. (#13873)
Change-Id: I466335abb694620be66b650666b15f96d090f524
This commit is contained in:
parent
08da133e55
commit
03e98934d4
|
@ -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.
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue