fix: 完成系统向智能体数据链路切换
- 切换工作台、聊天历史、资源候选与公共调用到 Agent - 加固资源绑定、删除保护及发布运行并发控制 - 隔离旧 Bot 专属服务和组件并保留兼容入口
This commit is contained in:
@@ -6,8 +6,9 @@ import org.testng.Assert;
|
||||
import org.testng.annotations.BeforeMethod;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.admin.controller.ai.support.BotResourceCreatorNameSupport;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||
import tech.easyflow.ai.service.BotApprovalStateService;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.BotMessageService;
|
||||
import tech.easyflow.ai.service.BotService;
|
||||
@@ -38,7 +39,7 @@ public class BotControllerTest {
|
||||
private BotDocumentCollectionService botDocumentCollectionService;
|
||||
private BotMessageService botMessageService;
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
private BotApprovalStateService botApprovalStateService;
|
||||
private SysAccountService sysAccountService;
|
||||
|
||||
/**
|
||||
@@ -52,7 +53,7 @@ public class BotControllerTest {
|
||||
botDocumentCollectionService = mock(BotDocumentCollectionService.class);
|
||||
botMessageService = mock(BotMessageService.class);
|
||||
categoryPermissionService = mock(CategoryPermissionService.class);
|
||||
aiResourceApprovalStateService = mock(AiResourceApprovalStateService.class);
|
||||
botApprovalStateService = mock(BotApprovalStateService.class);
|
||||
sysAccountService = mock(SysAccountService.class);
|
||||
}
|
||||
|
||||
@@ -69,10 +70,12 @@ public class BotControllerTest {
|
||||
botMessageService
|
||||
);
|
||||
AiResourceCreatorNameSupport creatorNameSupport = new AiResourceCreatorNameSupport();
|
||||
BotResourceCreatorNameSupport botCreatorNameSupport =
|
||||
new BotResourceCreatorNameSupport(creatorNameSupport);
|
||||
setField(creatorNameSupport, "sysAccountService", sysAccountService);
|
||||
setField(controller, "categoryPermissionService", categoryPermissionService);
|
||||
setField(controller, "aiResourceApprovalStateService", aiResourceApprovalStateService);
|
||||
setField(controller, "aiResourceCreatorNameSupport", creatorNameSupport);
|
||||
setField(controller, "botApprovalStateService", botApprovalStateService);
|
||||
setField(controller, "botResourceCreatorNameSupport", botCreatorNameSupport);
|
||||
|
||||
Bot bot = new Bot();
|
||||
bot.setId(BigInteger.valueOf(101));
|
||||
@@ -84,7 +87,7 @@ public class BotControllerTest {
|
||||
when(botService.page(any(Page.class), any(QueryWrapper.class))).thenReturn(page);
|
||||
when(sysAccountService.resolveDisplayNameMap(Collections.singleton(BigInteger.valueOf(7))))
|
||||
.thenReturn(Map.of(BigInteger.valueOf(7), "管理员"));
|
||||
doNothing().when(aiResourceApprovalStateService).fillBotApprovalState(page.getRecords());
|
||||
doNothing().when(botApprovalStateService).fillApprovalState(page.getRecords());
|
||||
|
||||
Page<Bot> result = controller.invokeQueryPage(new Page<>(1, 10), QueryWrapper.create());
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.service.AgentResourceReferenceService;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link McpController} 删除锁测试。
|
||||
*/
|
||||
public class McpControllerTest {
|
||||
|
||||
/**
|
||||
* 验证 MCP 删除先锁定资源行,再执行删除。
|
||||
*/
|
||||
@Test
|
||||
public void removeShouldLockMcpBeforeRemoval() {
|
||||
McpService mcpService = mock(McpService.class);
|
||||
AgentResourceReferenceService referenceService = mock(AgentResourceReferenceService.class);
|
||||
when(mcpService.getOne(any(QueryWrapper.class))).thenReturn(new Mcp());
|
||||
McpController controller = new McpController(mcpService);
|
||||
setField(controller, "agentResourceReferenceService", referenceService);
|
||||
LoginAccount loginAccount = new LoginAccount();
|
||||
loginAccount.setTenantId(BigInteger.ONE);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> login = mockStatic(SaTokenUtil.class)) {
|
||||
login.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
|
||||
controller.remove(BigInteger.TEN);
|
||||
}
|
||||
|
||||
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
verify(mcpService).getOne(queryCaptor.capture());
|
||||
Assert.assertTrue(
|
||||
queryCaptor.getValue().toSQL().toUpperCase(Locale.ROOT).contains("FOR UPDATE")
|
||||
);
|
||||
verify(referenceService).assertMcpUnused(BigInteger.TEN);
|
||||
verify(mcpService).removeMcp(BigInteger.TEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射设置字段值。
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param fieldName 字段名
|
||||
* @param value 字段值
|
||||
*/
|
||||
private static void setField(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException("设置测试字段失败: " + fieldName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link PluginController} 删除接口测试。
|
||||
*/
|
||||
public class PluginControllerTest {
|
||||
|
||||
/**
|
||||
* 插件删除接口必须委托事务服务执行完整引用校验和删除。
|
||||
*/
|
||||
@Test
|
||||
public void removeShouldDelegateToTransactionalService() {
|
||||
PluginService pluginService = mock(PluginService.class);
|
||||
when(pluginService.removePlugin("10")).thenReturn(true);
|
||||
PluginController controller = new PluginController(pluginService);
|
||||
|
||||
boolean removed = controller.removePlugin("10").getData();
|
||||
|
||||
Assert.assertTrue(removed);
|
||||
verify(pluginService).removePlugin("10");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.service.AgentResourceReferenceService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link PluginItemController} 删除锁测试。
|
||||
*/
|
||||
public class PluginItemControllerTest {
|
||||
|
||||
/**
|
||||
* 验证插件工具删除按稳定顺序锁定资源行。
|
||||
*/
|
||||
@Test
|
||||
public void removeCheckShouldLockPluginItemsInStableOrder() {
|
||||
PluginItemService pluginItemService = mock(PluginItemService.class);
|
||||
AgentResourceReferenceService referenceService = mock(AgentResourceReferenceService.class);
|
||||
PluginService pluginService = mock(PluginService.class);
|
||||
PluginVisibilityService visibilityService = mock(PluginVisibilityService.class);
|
||||
PluginItem first = pluginItem(BigInteger.ONE, BigInteger.TEN);
|
||||
PluginItem second = pluginItem(BigInteger.TWO, BigInteger.TEN);
|
||||
Plugin plugin = new Plugin();
|
||||
plugin.setId(BigInteger.TEN);
|
||||
plugin.setTenantId(1L);
|
||||
plugin.setCreatedBy(1L);
|
||||
when(pluginItemService.list(any(QueryWrapper.class))).thenReturn(List.of(first, second));
|
||||
when(pluginService.getById(BigInteger.TEN)).thenReturn(plugin);
|
||||
PluginItemController controller = new PluginItemController(pluginItemService);
|
||||
setField(controller, "pluginItemService", pluginItemService);
|
||||
setField(controller, "agentResourceReferenceService", referenceService);
|
||||
setField(controller, "pluginService", pluginService);
|
||||
setField(controller, "pluginVisibilityService", visibilityService);
|
||||
LoginAccount loginAccount = new LoginAccount();
|
||||
loginAccount.setTenantId(BigInteger.ONE);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> login = mockStatic(SaTokenUtil.class)) {
|
||||
login.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
|
||||
controller.onRemoveBefore(List.of(BigInteger.TWO, BigInteger.ONE));
|
||||
}
|
||||
|
||||
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
verify(pluginItemService).list(queryCaptor.capture());
|
||||
String sql = queryCaptor.getValue().toSQL().toUpperCase(Locale.ROOT);
|
||||
Assert.assertTrue(sql.contains("ORDER BY"));
|
||||
Assert.assertTrue(sql.contains("FOR UPDATE"));
|
||||
verify(referenceService).assertPluginItemsUnused(List.of(BigInteger.TWO, BigInteger.ONE));
|
||||
verify(visibilityService).assertPluginVisible(1L, BigInteger.TEN, "无权限删除该插件工具");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建插件工具。
|
||||
*
|
||||
* @param id 工具 ID
|
||||
* @param pluginId 插件 ID
|
||||
* @return 插件工具
|
||||
*/
|
||||
private static PluginItem pluginItem(BigInteger id, BigInteger pluginId) {
|
||||
PluginItem pluginItem = new PluginItem();
|
||||
pluginItem.setId(id);
|
||||
pluginItem.setPluginId(pluginId);
|
||||
return pluginItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射设置字段值。
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param fieldName 字段名
|
||||
* @param value 字段值
|
||||
*/
|
||||
private static void setField(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException("设置测试字段失败: " + fieldName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.service.BotService;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||
@@ -22,6 +22,7 @@ import tech.easyflow.chatlog.service.ChatDashboardQueryService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysAccountRoleService;
|
||||
@@ -339,29 +340,34 @@ public class DashboardServiceImplTest {
|
||||
public void shouldQueryUserRanksWithAssistantFilter() throws Exception {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
BotService botService = mock(BotService.class);
|
||||
AgentService agentService = mock(AgentService.class);
|
||||
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||
SysAccountRoleService sysAccountRoleService = mock(SysAccountRoleService.class);
|
||||
SysRoleService sysRoleService = mock(SysRoleService.class);
|
||||
|
||||
Bot bot = new Bot();
|
||||
bot.setId(BigInteger.TEN);
|
||||
bot.setStatus(1);
|
||||
bot.setCreatedBy(BigInteger.ONE);
|
||||
bot.setCategoryId(BigInteger.valueOf(8));
|
||||
Agent agent = new Agent();
|
||||
agent.setId(BigInteger.TEN);
|
||||
agent.setStatus(1);
|
||||
agent.setCreatedBy(BigInteger.ONE);
|
||||
agent.setCategoryId(BigInteger.valueOf(8));
|
||||
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5)))
|
||||
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(2), "demo-user", 2L, 4L, 1L)));
|
||||
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
|
||||
when(agentService.getById(BigInteger.TEN)).thenReturn(agent);
|
||||
when(categoryPermissionService.canAccessCategory(
|
||||
any(LoginAccount.class),
|
||||
eq(CategoryResourceType.AGENT.getCode()),
|
||||
eq(BigInteger.ONE),
|
||||
eq(BigInteger.valueOf(8))
|
||||
))
|
||||
.thenReturn(true);
|
||||
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(2L, "demo-user", "演示用户")));
|
||||
when(sysAccountRoleService.list(any(QueryWrapper.class))).thenReturn(Collections.emptyList());
|
||||
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setField(service, "botService", botService);
|
||||
setField(service, "agentService", agentService);
|
||||
setField(service, "categoryPermissionService", categoryPermissionService);
|
||||
setField(service, "sysAccountService", sysAccountService);
|
||||
setField(service, "sysAccountRoleService", sysAccountRoleService);
|
||||
@@ -382,26 +388,59 @@ public class DashboardServiceImplTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证未启用智能体会被拒绝。
|
||||
* 验证停用智能体仍可用于筛选历史统计。
|
||||
*/
|
||||
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
|
||||
public void shouldRejectDisabledAssistantFilter() {
|
||||
@Test
|
||||
public void shouldAllowDisabledAgentFilterForHistoricalStatistics() {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
BotService botService = mock(BotService.class);
|
||||
AgentService agentService = mock(AgentService.class);
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||
|
||||
Bot bot = new Bot();
|
||||
bot.setId(BigInteger.TEN);
|
||||
bot.setStatus(0);
|
||||
Agent agent = new Agent();
|
||||
agent.setId(BigInteger.TEN);
|
||||
agent.setStatus(0);
|
||||
agent.setCreatedBy(BigInteger.ONE);
|
||||
agent.setCategoryId(BigInteger.valueOf(8));
|
||||
|
||||
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||
when(agentService.getById(BigInteger.TEN)).thenReturn(agent);
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(categoryPermissionService.canAccessCategory(
|
||||
any(LoginAccount.class),
|
||||
eq(CategoryResourceType.AGENT.getCode()),
|
||||
eq(BigInteger.ONE),
|
||||
eq(BigInteger.valueOf(8))
|
||||
)).thenReturn(true);
|
||||
|
||||
setFieldSilently(service, "botService", botService);
|
||||
setFieldSilently(service, "agentService", agentService);
|
||||
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setFieldSilently(service, "categoryPermissionService", mock(CategoryPermissionService.class));
|
||||
setFieldSilently(service, "categoryPermissionService", categoryPermissionService);
|
||||
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));
|
||||
|
||||
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||
query.setRange("7d");
|
||||
query.setAssistantId(BigInteger.TEN);
|
||||
List<DashboardUserRankItemVo> userRanks = service.getUserRanks(new LoginAccount(), query);
|
||||
|
||||
Assert.assertTrue(userRanks.isEmpty());
|
||||
verify(chatDashboardQueryService).queryActiveUserRanks(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
eq(BigInteger.TEN),
|
||||
eq(5)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证不存在的智能体筛选会被拒绝。
|
||||
*/
|
||||
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "智能体不存在或不可见")
|
||||
public void shouldRejectMissingAgentFilter() {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
AgentService agentService = mock(AgentService.class);
|
||||
setFieldSilently(service, "agentService", agentService);
|
||||
|
||||
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||
query.setRange("7d");
|
||||
query.setAssistantId(BigInteger.TEN);
|
||||
@@ -411,25 +450,30 @@ public class DashboardServiceImplTest {
|
||||
/**
|
||||
* 验证当前作用域不可见的智能体会被拒绝。
|
||||
*/
|
||||
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
|
||||
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "智能体不存在或不可见")
|
||||
public void shouldRejectInvisibleAssistantFilter() {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
BotService botService = mock(BotService.class);
|
||||
AgentService agentService = mock(AgentService.class);
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||
|
||||
Bot bot = new Bot();
|
||||
bot.setId(BigInteger.TEN);
|
||||
bot.setStatus(1);
|
||||
bot.setCreatedBy(BigInteger.ONE);
|
||||
bot.setCategoryId(BigInteger.valueOf(8));
|
||||
Agent agent = new Agent();
|
||||
agent.setId(BigInteger.TEN);
|
||||
agent.setStatus(1);
|
||||
agent.setCreatedBy(BigInteger.ONE);
|
||||
agent.setCategoryId(BigInteger.valueOf(8));
|
||||
|
||||
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||
when(agentService.getById(BigInteger.TEN)).thenReturn(agent);
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
|
||||
when(categoryPermissionService.canAccessCategory(
|
||||
any(LoginAccount.class),
|
||||
eq(CategoryResourceType.AGENT.getCode()),
|
||||
eq(BigInteger.ONE),
|
||||
eq(BigInteger.valueOf(8))
|
||||
))
|
||||
.thenReturn(false);
|
||||
|
||||
setFieldSilently(service, "botService", botService);
|
||||
setFieldSilently(service, "agentService", agentService);
|
||||
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setFieldSilently(service, "categoryPermissionService", categoryPermissionService);
|
||||
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));
|
||||
|
||||
Reference in New Issue
Block a user