feat: 完善 Agent 标准交互与安全运行时
- 接入 AG-UI 运行投影、Turn 时间线和审批隔离 - 增加 Agent Skill 冻结绑定与运行时消费闭环 - 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
@@ -69,8 +69,10 @@ public class SkillApprovalSubjectHandlerContentReferenceTest {
|
||||
saToken = mockStatic(SaTokenUtil.class);
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
when(skillMapper.updateApprovalState(any(), any(), any(), any())).thenReturn(1);
|
||||
when(skillMapper.publish(any(), any(), any(), any(), any(), any())).thenReturn(1);
|
||||
when(skillMapper.publishApproved(any(), any(), any(), any(), any(), any(), any())).thenReturn(1);
|
||||
when(skillMapper.publish(any(), any(), any(), any(), any(), any(), any())).thenReturn(1);
|
||||
when(skillMapper.publishApproved(any(), any(), any(), any(), any(), any(), any(), any())).thenReturn(1);
|
||||
when(skillService.extractContentSnapshot(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(skillService.extractToolBindingsSnapshot(any())).thenReturn(Map.of());
|
||||
when(skillMapper.markOfflineApproved(any(), any(), any())).thenReturn(1);
|
||||
when(skillMapper.restoreApprovalState(any(), any(), any(), any())).thenReturn(1);
|
||||
handler = new SkillApprovalSubjectHandler(
|
||||
@@ -124,7 +126,7 @@ public class SkillApprovalSubjectHandlerContentReferenceTest {
|
||||
ApprovalActionType.PUBLISH.getCode(), SKILL_ID, candidate, OPERATOR_ID);
|
||||
|
||||
verify(skillMapper).publish(
|
||||
eq(SKILL_ID), eq(BigInteger.ONE), same(candidate), any(Date.class),
|
||||
eq(SKILL_ID), eq(BigInteger.ONE), same(candidate), eq(Map.of()), any(Date.class),
|
||||
eq(OPERATOR_ID), isNull());
|
||||
verify(skillService).releaseSnapshotContents(previous);
|
||||
verify(skillService, never()).releaseSnapshotContents(candidate);
|
||||
@@ -228,6 +230,7 @@ public class SkillApprovalSubjectHandlerContentReferenceTest {
|
||||
eq(BigInteger.ONE),
|
||||
eq(instanceId),
|
||||
same(candidate),
|
||||
eq(Map.of()),
|
||||
any(Date.class),
|
||||
eq(OPERATOR_ID),
|
||||
eq("candidate-hash"));
|
||||
@@ -253,7 +256,7 @@ public class SkillApprovalSubjectHandlerContentReferenceTest {
|
||||
BigInteger.valueOf(99)));
|
||||
|
||||
assertEquals(409, exception.getHttpStatus());
|
||||
verify(skillMapper, never()).publishApproved(any(), any(), any(), any(), any(), any(), any());
|
||||
verify(skillMapper, never()).publishApproved(any(), any(), any(), any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,7 +276,7 @@ public class SkillApprovalSubjectHandlerContentReferenceTest {
|
||||
handler.applyApprovedAction(
|
||||
ApprovalActionType.PUBLISH.getCode(), SKILL_ID, candidate, OPERATOR_ID, instanceId);
|
||||
|
||||
verify(skillMapper, never()).publishApproved(any(), any(), any(), any(), any(), any(), any());
|
||||
verify(skillMapper, never()).publishApproved(any(), any(), any(), any(), any(), any(), any(), any());
|
||||
verify(skillService, never()).releaseSnapshotContents(any());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.mapper.PluginMapper;
|
||||
import tech.easyflow.ai.permission.McpAccessPermissionChecker;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.vo.SkillToolOptionPage;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Skill Tool 候选权限边界测试。
|
||||
*/
|
||||
public class SkillToolOptionQueryServiceTest {
|
||||
|
||||
/**
|
||||
* 验证聚合查询在缺少 MCP 权限时仍返回其他已授权候选。
|
||||
*/
|
||||
@Test
|
||||
public void allShouldOmitMcpWithoutBlockingOtherCandidates() {
|
||||
Dependencies dependencies = new Dependencies();
|
||||
LoginAccount account = account();
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.setId(BigInteger.TEN);
|
||||
workflow.setTenantId(account.getTenantId());
|
||||
workflow.setTitle("合同审批");
|
||||
workflow.setDescription("审批合同");
|
||||
workflow.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
// tb_workflow.status 是历史字段,线上可用性以发布状态为准。
|
||||
workflow.setStatus(0);
|
||||
when(dependencies.workflowService.list(any(QueryWrapper.class))).thenReturn(List.of(workflow));
|
||||
when(dependencies.resourceAccessService.canAccess(any(), any(), any())).thenReturn(true);
|
||||
when(dependencies.pluginMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of());
|
||||
when(dependencies.mcpAccessPermissionChecker.canUseMcp()).thenReturn(false);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> login = mockStatic(SaTokenUtil.class)) {
|
||||
login.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
|
||||
SkillToolOptionPage result = dependencies.service().page(null, "ALL", 1, 20);
|
||||
|
||||
Assert.assertEquals(1L, result.total());
|
||||
Assert.assertEquals("WORKFLOW", result.records().get(0).toolType());
|
||||
verify(dependencies.mcpService, never()).list(any(QueryWrapper.class));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证显式查询 MCP 时仍严格要求 MCP 权限。
|
||||
*/
|
||||
@Test
|
||||
public void explicitMcpShouldRejectMissingPermission() {
|
||||
Dependencies dependencies = new Dependencies();
|
||||
doThrow(new BusinessException(403, 403, "无权限查询或使用 MCP"))
|
||||
.when(dependencies.mcpAccessPermissionChecker).assertCanUseMcp();
|
||||
|
||||
try (MockedStatic<SaTokenUtil> login = mockStatic(SaTokenUtil.class)) {
|
||||
login.when(SaTokenUtil::getLoginAccount).thenReturn(account());
|
||||
try {
|
||||
dependencies.service().page(null, "MCP", 1, 20);
|
||||
Assert.fail("显式 MCP 查询必须校验权限");
|
||||
} catch (BusinessException exception) {
|
||||
Assert.assertEquals(403, exception.getHttpStatus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LoginAccount account() {
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(BigInteger.ONE);
|
||||
account.setTenantId(BigInteger.valueOf(42));
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询服务依赖夹具。
|
||||
*/
|
||||
private static final class Dependencies {
|
||||
|
||||
private final WorkflowService workflowService = mock(WorkflowService.class);
|
||||
private final PluginItemService pluginItemService = mock(PluginItemService.class);
|
||||
private final PluginMapper pluginMapper = mock(PluginMapper.class);
|
||||
private final PluginVisibilityService pluginVisibilityService = mock(PluginVisibilityService.class);
|
||||
private final McpService mcpService = mock(McpService.class);
|
||||
private final McpAccessPermissionChecker mcpAccessPermissionChecker =
|
||||
mock(McpAccessPermissionChecker.class);
|
||||
private final SkillToolResourceService resourceService = mock(SkillToolResourceService.class);
|
||||
private final ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
|
||||
/**
|
||||
* 创建待测服务。
|
||||
*
|
||||
* @return 待测服务
|
||||
*/
|
||||
private SkillToolOptionQueryService service() {
|
||||
return new SkillToolOptionQueryService(
|
||||
workflowService,
|
||||
pluginItemService,
|
||||
pluginMapper,
|
||||
pluginVisibilityService,
|
||||
mcpService,
|
||||
mcpAccessPermissionChecker,
|
||||
resourceService,
|
||||
resourceAccessService
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.easyagents.agent.runtime.mcp.McpToolManifestEntry;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillCategoryService;
|
||||
import tech.easyflow.skill.service.SkillReferenceProvider;
|
||||
import tech.easyflow.skill.service.SkillResourceService;
|
||||
import tech.easyflow.skill.service.SkillToolBindingService;
|
||||
import tech.easyflow.skill.store.DBSkillContentStore;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Skill 组合发布快照 hash 测试。
|
||||
*/
|
||||
public class SkillServiceImplSnapshotHashTest {
|
||||
|
||||
/**
|
||||
* 含 MCP Manifest POJO 的组合快照经过 JSON 持久化后仍应通过校验。
|
||||
*
|
||||
* @throws Exception JSON 或反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldVerifyAggregateSnapshotAfterJsonRoundTrip() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
SkillToolBindingService toolBindingService = mock(SkillToolBindingService.class);
|
||||
SkillServiceImpl service = service(objectMapper, toolBindingService);
|
||||
|
||||
Map<String, Object> contentSnapshot = new LinkedHashMap<>();
|
||||
contentSnapshot.put("schemaVersion", 2);
|
||||
contentSnapshot.put("name", "l21-mcp-docs");
|
||||
contentSnapshot.put("snapshotHash", hash(service, contentSnapshot));
|
||||
|
||||
Map<String, Object> binding = new LinkedHashMap<>();
|
||||
binding.put("toolType", "MCP");
|
||||
McpToolManifestEntry manifestEntry = new McpToolManifestEntry();
|
||||
manifestEntry.setName("query-docs");
|
||||
manifestEntry.setDescription("查询文档");
|
||||
manifestEntry.setInputSchema(Map.of("type", "object"));
|
||||
manifestEntry.setOutputSchema(Map.of());
|
||||
binding.put("mcpToolManifest", List.of(manifestEntry));
|
||||
Map<String, Object> toolSnapshot = new LinkedHashMap<>();
|
||||
toolSnapshot.put("schemaVersion", 1);
|
||||
toolSnapshot.put("bindings", List.of(binding));
|
||||
toolSnapshot.put("snapshotHash", "verified-by-tool-service");
|
||||
|
||||
Map<String, Object> aggregate = new LinkedHashMap<>(contentSnapshot);
|
||||
Object contentHash = aggregate.remove("snapshotHash");
|
||||
aggregate.put("contentSnapshotHash", contentHash);
|
||||
aggregate.put("platformToolBindings", toolSnapshot);
|
||||
aggregate.put("toolBindingsHash", toolSnapshot.get("snapshotHash"));
|
||||
|
||||
Skill persistedSkill = new Skill();
|
||||
persistedSkill.setPublishedSnapshotJson(objectMapper.readValue(
|
||||
objectMapper.writeValueAsBytes(contentSnapshot), Map.class));
|
||||
persistedSkill.setPublishedToolBindingsJson(objectMapper.readValue(
|
||||
objectMapper.writeValueAsBytes(toolSnapshot), Map.class));
|
||||
persistedSkill.setSnapshotHash(hash(service, aggregate));
|
||||
|
||||
service.assertPublishedAggregateHash(persistedSkill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建仅用于快照校验的服务。
|
||||
*
|
||||
* @param objectMapper JSON 映射器
|
||||
* @param toolBindingService Tool 快照服务
|
||||
* @return Skill 服务
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private SkillServiceImpl service(ObjectMapper objectMapper,
|
||||
SkillToolBindingService toolBindingService) {
|
||||
return new SkillServiceImpl(
|
||||
mock(SkillCategoryService.class),
|
||||
mock(SkillResourceService.class),
|
||||
toolBindingService,
|
||||
mock(DBSkillContentStore.class),
|
||||
mock(ResourceAccessService.class),
|
||||
mock(CategoryPermissionService.class),
|
||||
objectMapper,
|
||||
mock(ObjectProvider.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用生产代码的统一快照 hash 算法。
|
||||
*
|
||||
* @param service Skill 服务
|
||||
* @param value 待计算结构
|
||||
* @return SHA-256 hash
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
private String hash(SkillServiceImpl service, Object value) throws Exception {
|
||||
Method method = SkillServiceImpl.class.getDeclaredMethod("hashJson", Object.class);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(service, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.easyagents.agent.runtime.mcp.McpToolManifestEntry;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillToolBinding;
|
||||
import tech.easyflow.skill.mapper.SkillMapper;
|
||||
import tech.easyflow.skill.service.SkillToolResourceService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Skill Tool 绑定发布快照测试。
|
||||
*/
|
||||
public class SkillToolBindingServiceImplTest {
|
||||
|
||||
/**
|
||||
* 发布快照应包含资源冻结数据,并拒绝任何后续篡改。
|
||||
*/
|
||||
@Test
|
||||
public void shouldBuildAndVerifyFrozenToolSnapshot() {
|
||||
SkillToolResourceService resources = mock(SkillToolResourceService.class);
|
||||
SkillToolBinding workflowBinding = binding("WORKFLOW", 100, null);
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.setId(BigInteger.valueOf(100));
|
||||
workflow.setTitle("合同审查流程");
|
||||
when(resources.requireWorkflow(org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.eq(workflowBinding))).thenReturn(workflow);
|
||||
when(resources.snapshotWorkflow(workflow)).thenReturn(Map.of(
|
||||
"id", BigInteger.valueOf(100),
|
||||
"content", "{\"nodes\":[]}"));
|
||||
SkillToolBindingServiceImpl service = service(resources, List.of(workflowBinding));
|
||||
|
||||
Map<String, Object> snapshot = service.buildPublishSnapshot(skill());
|
||||
|
||||
service.assertPublishedSnapshotHash(snapshot);
|
||||
verify(resources).requireWorkflow(org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.eq(workflowBinding));
|
||||
Map<String, Object> tampered = new LinkedHashMap<>(snapshot);
|
||||
tampered.put("schemaVersion", 2);
|
||||
Assert.assertThrows(BusinessException.class,
|
||||
() -> service.assertPublishedSnapshotHash(tampered));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布快照经过数据库 JSON 持久化后仍应保持同一 hash。
|
||||
*
|
||||
* @throws Exception JSON 往返失败时抛出
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldVerifyMcpSnapshotAfterJsonRoundTrip() throws Exception {
|
||||
SkillToolResourceService resources = mock(SkillToolResourceService.class);
|
||||
SkillToolBinding mcpBinding = binding("MCP", 200, "manifest-hash");
|
||||
Mcp mcp = mcp();
|
||||
when(resources.requireMcp(org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.eq(mcpBinding))).thenReturn(new SkillToolResourceService.McpResource(
|
||||
mcp, List.of(manifest("search")), "manifest-hash"));
|
||||
when(resources.snapshotMcpConnection(mcp)).thenReturn(Map.of(
|
||||
"id", mcp.getId(),
|
||||
"configJson", "{\"mcpServers\":{}}"));
|
||||
SkillToolBindingServiceImpl service = service(resources, List.of(mcpBinding));
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
Map<String, Object> snapshot = service.buildPublishSnapshot(skill());
|
||||
Map<String, Object> persisted = mapper.readValue(
|
||||
mapper.writeValueAsBytes(snapshot), Map.class);
|
||||
|
||||
service.assertPublishedSnapshotHash(persisted);
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 清单变化后发布必须失败,要求用户重新保存并确认绑定。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectChangedMcpManifestAtPublish() {
|
||||
SkillToolResourceService resources = mock(SkillToolResourceService.class);
|
||||
SkillToolBinding mcpBinding = binding("MCP", 200, "old-hash");
|
||||
when(resources.requireMcp(org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.eq(mcpBinding))).thenReturn(new SkillToolResourceService.McpResource(
|
||||
mcp(), List.of(manifest("search")), "new-hash"));
|
||||
SkillToolBindingServiceImpl service = service(resources, List.of(mcpBinding));
|
||||
|
||||
BusinessException exception = Assert.assertThrows(BusinessException.class,
|
||||
() -> service.buildPublishSnapshot(skill()));
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("清单已变化"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个 Skill 展开后的实际 MCP Tool 数量不得超过二十个。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectMoreThanTwentyExpandedTools() {
|
||||
SkillToolResourceService resources = mock(SkillToolResourceService.class);
|
||||
SkillToolBinding mcpBinding = binding("MCP", 200, "manifest-hash");
|
||||
List<McpToolManifestEntry> manifest = new ArrayList<>();
|
||||
for (int index = 1; index <= 21; index++) {
|
||||
manifest.add(manifest("tool-" + index));
|
||||
}
|
||||
when(resources.requireMcp(org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.eq(mcpBinding))).thenReturn(new SkillToolResourceService.McpResource(
|
||||
mcp(), manifest, "manifest-hash"));
|
||||
SkillToolBindingServiceImpl service = service(resources, List.of(mcpBinding));
|
||||
|
||||
BusinessException exception = Assert.assertThrows(BusinessException.class,
|
||||
() -> service.buildPublishSnapshot(skill()));
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("20"));
|
||||
verify(resources).requireMcp(org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.eq(mcpBinding));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建可注入固定绑定列表的服务。
|
||||
*
|
||||
* @param resources Tool 资源服务
|
||||
* @param bindings 固定绑定
|
||||
* @return 测试服务
|
||||
*/
|
||||
private SkillToolBindingServiceImpl service(SkillToolResourceService resources,
|
||||
List<SkillToolBinding> bindings) {
|
||||
return new SkillToolBindingServiceImpl(
|
||||
mock(SkillMapper.class), resources, mock(ResourceAccessService.class), new ObjectMapper()) {
|
||||
@Override
|
||||
public List<SkillToolBinding> listBindings(BigInteger skillId) {
|
||||
return bindings;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试 Skill。
|
||||
*
|
||||
* @return Skill
|
||||
*/
|
||||
private Skill skill() {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(BigInteger.ONE);
|
||||
skill.setTenantId(BigInteger.ONE);
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Tool 绑定。
|
||||
*
|
||||
* @param type 类型
|
||||
* @param targetId 目标 ID
|
||||
* @param manifestHash MCP 清单 hash
|
||||
* @return 绑定
|
||||
*/
|
||||
private SkillToolBinding binding(String type, long targetId, String manifestHash) {
|
||||
SkillToolBinding binding = new SkillToolBinding();
|
||||
binding.setToolType(type);
|
||||
binding.setTargetId(BigInteger.valueOf(targetId));
|
||||
binding.setMcpToolManifestHash(manifestHash);
|
||||
binding.setSortNo(0);
|
||||
return binding;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建测试 MCP。
|
||||
*
|
||||
* @return MCP
|
||||
*/
|
||||
private Mcp mcp() {
|
||||
Mcp mcp = new Mcp();
|
||||
mcp.setId(BigInteger.valueOf(200));
|
||||
mcp.setTitle("测试 MCP");
|
||||
return mcp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建最小 MCP Tool 清单项。
|
||||
*
|
||||
* @param name Tool 名称
|
||||
* @return 清单项
|
||||
*/
|
||||
private McpToolManifestEntry manifest(String name) {
|
||||
McpToolManifestEntry entry = new McpToolManifestEntry();
|
||||
entry.setName(name);
|
||||
entry.setDescription("测试工具");
|
||||
entry.setInputSchema(Map.of("type", "object"));
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mockito;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillToolBinding;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.service.SkillToolBindingService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 对平台 Tool 的生命周期引用查询测试。
|
||||
*/
|
||||
public class SkillToolReferenceProviderImplTest {
|
||||
|
||||
/**
|
||||
* 草稿绑定和有效发布快照都应参与 Workflow 下线与删除影响检查。
|
||||
*/
|
||||
@Test
|
||||
public void shouldIncludeDraftAndPublishedWorkflowReferences() {
|
||||
SkillService skillService = Mockito.mock(SkillService.class);
|
||||
SkillToolBindingService bindingService = Mockito.mock(SkillToolBindingService.class);
|
||||
SkillToolBinding draftBinding = new SkillToolBinding();
|
||||
draftBinding.setSkillId(BigInteger.ONE);
|
||||
draftBinding.setToolType("WORKFLOW");
|
||||
draftBinding.setTargetId(BigInteger.TEN);
|
||||
Skill publishedProjection = skill(BigInteger.TWO, "线上 Skill");
|
||||
publishedProjection.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
publishedProjection.setPublishedToolBindingsJson(Map.of(
|
||||
"bindings", List.of(Map.of(
|
||||
"toolType", "WORKFLOW",
|
||||
"targetId", BigInteger.TEN))));
|
||||
Mockito.when(bindingService.list(Mockito.any(QueryWrapper.class)))
|
||||
.thenReturn(List.of(draftBinding));
|
||||
Mockito.when(skillService.list(Mockito.any(QueryWrapper.class)))
|
||||
.thenReturn(List.of(publishedProjection));
|
||||
Mockito.when(skillService.listByIds(Mockito.anyCollection()))
|
||||
.thenReturn(List.of(
|
||||
skill(BigInteger.ONE, "草稿 Skill"),
|
||||
skill(BigInteger.TWO, "线上 Skill")));
|
||||
SkillToolReferenceProviderImpl provider = new SkillToolReferenceProviderImpl(
|
||||
skillService, bindingService);
|
||||
|
||||
var references = provider.listSkillsByWorkflowId(BigInteger.TEN);
|
||||
|
||||
Assert.assertEquals(2, references.size());
|
||||
Assert.assertEquals("Skill“草稿 Skill”", references.get(0).getTitle());
|
||||
Assert.assertEquals("Skill“线上 Skill”", references.get(1).getTitle());
|
||||
}
|
||||
|
||||
/**
|
||||
* 已下线发布快照不应继续阻止 Tool 生命周期操作。
|
||||
*/
|
||||
@Test
|
||||
public void shouldIgnoreOfflinePublishedSnapshot() {
|
||||
SkillService skillService = Mockito.mock(SkillService.class);
|
||||
SkillToolBindingService bindingService = Mockito.mock(SkillToolBindingService.class);
|
||||
Skill offline = skill(BigInteger.ONE, "已下线 Skill");
|
||||
offline.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
offline.setPublishedToolBindingsJson(Map.of(
|
||||
"bindings", List.of(Map.of(
|
||||
"toolType", "MCP",
|
||||
"targetId", BigInteger.TEN))));
|
||||
Mockito.when(bindingService.list(Mockito.any(QueryWrapper.class))).thenReturn(List.of());
|
||||
Mockito.when(skillService.list(Mockito.any(QueryWrapper.class))).thenReturn(List.of(offline));
|
||||
SkillToolReferenceProviderImpl provider = new SkillToolReferenceProviderImpl(
|
||||
skillService, bindingService);
|
||||
|
||||
Assert.assertTrue(provider.listSkillsByMcpId(BigInteger.TEN).isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Skill 摘要。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @param displayName 展示名
|
||||
* @return Skill
|
||||
*/
|
||||
private Skill skill(BigInteger id, String displayName) {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(id);
|
||||
skill.setName("skill-" + id);
|
||||
skill.setDisplayName(displayName);
|
||||
return skill;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user