feat(XL13): 归档工作流对话运行界面
- 接入发布快照优先与未发布草稿受控运行 - 支持文本和思考流式输出、循环多输出及实时运行详情 - 完成聊天分享、图片输入、中止与清空重来 - 补充后端与前端定向回归测试
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.admin.service.ai.WorkflowChatEventStream;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.WorkflowExecResultService;
|
||||
import tech.easyflow.ai.service.WorkflowExecStepService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowShareService;
|
||||
import tech.easyflow.ai.share.WorkflowSharePolicy;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
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;
|
||||
|
||||
/**
|
||||
* {@link WorkflowChatController} 运行来源与分享边界测试。
|
||||
*/
|
||||
public class WorkflowChatControllerTest {
|
||||
|
||||
/**
|
||||
* 验证登录用户可以进入并运行未发布工作流,同时描述信息禁止分享。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRunDraftForAuthenticatedRequestWithoutSharing() {
|
||||
ControllerFixture fixture = fixture(workflow(PublishStatus.DRAFT, "draft-content", false));
|
||||
when(fixture.parameterResolver.buildRunningParametersView(fixture.current))
|
||||
.thenReturn(new LinkedHashMap<>());
|
||||
when(fixture.parameterResolver.normalizeRuntimeVariables(
|
||||
eq("draft-content"),
|
||||
anyMap()
|
||||
)).thenReturn(new LinkedHashMap<>());
|
||||
when(fixture.eventStream.start(eq("1"), anyMap())).thenReturn(new SseEmitter());
|
||||
|
||||
try (MockedStatic<SaTokenUtil> login = login(fixture.account)) {
|
||||
Result<Map<String, Object>> descriptor = fixture.controller.descriptor(
|
||||
BigInteger.ONE,
|
||||
request(Map.of())
|
||||
);
|
||||
fixture.controller.run(BigInteger.ONE, Map.of(), request(Map.of()));
|
||||
|
||||
Assert.assertEquals(descriptor.getData().get("shareable"), false);
|
||||
Assert.assertEquals(
|
||||
descriptor.getData().get("publishStatus"),
|
||||
PublishStatus.DRAFT.getCode()
|
||||
);
|
||||
}
|
||||
|
||||
verify(fixture.parameterResolver).buildRunningParametersView(fixture.current);
|
||||
verify(fixture.eventStream).start(eq("1"), anyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证已发布工作流的管理端运行继续读取发布快照。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRunPublishedSnapshotForAuthenticatedRequest() {
|
||||
Workflow current = workflow(PublishStatus.PUBLISHED, "draft-content", true);
|
||||
Workflow published = workflow(PublishStatus.PUBLISHED, "published-content", true);
|
||||
ControllerFixture fixture = fixture(current, published);
|
||||
when(fixture.parameterResolver.buildRunningParametersView(published))
|
||||
.thenReturn(new LinkedHashMap<>());
|
||||
when(fixture.parameterResolver.normalizeRuntimeVariables(
|
||||
eq("published-content"),
|
||||
anyMap()
|
||||
)).thenReturn(new LinkedHashMap<>());
|
||||
when(fixture.eventStream.start(
|
||||
eq(PublishedWorkflowDefinitionIds.published("1")),
|
||||
anyMap()
|
||||
)).thenReturn(new SseEmitter());
|
||||
|
||||
try (MockedStatic<SaTokenUtil> login = login(fixture.account)) {
|
||||
Result<Map<String, Object>> descriptor = fixture.controller.descriptor(
|
||||
BigInteger.ONE,
|
||||
request(Map.of())
|
||||
);
|
||||
fixture.controller.run(BigInteger.ONE, Map.of(), request(Map.of()));
|
||||
|
||||
Assert.assertEquals(descriptor.getData().get("shareable"), true);
|
||||
}
|
||||
|
||||
verify(fixture.parameterResolver).buildRunningParametersView(published);
|
||||
verify(fixture.eventStream).start(
|
||||
eq(PublishedWorkflowDefinitionIds.published("1")),
|
||||
anyMap()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分享访问仍拒绝未发布工作流。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectDraftWorkflowFromShareRequest() {
|
||||
ControllerFixture fixture = fixture(workflow(PublishStatus.DRAFT, "draft-content", false));
|
||||
HttpServletRequest request = request(Map.of(
|
||||
WorkflowSharePolicy.CHAT_SHARE_KEY_HEADER.toLowerCase(Locale.ROOT),
|
||||
"share-key"
|
||||
));
|
||||
|
||||
try (MockedStatic<SaTokenUtil> login = login(fixture.account)) {
|
||||
Assert.expectThrows(
|
||||
BusinessException.class,
|
||||
() -> fixture.controller.descriptor(BigInteger.ONE, request)
|
||||
);
|
||||
}
|
||||
|
||||
verify(fixture.workflowShareService).assertChatShareAccess(
|
||||
"share-key",
|
||||
BigInteger.ONE,
|
||||
BigInteger.ONE
|
||||
);
|
||||
verify(fixture.resourceAccessService, never()).assertAccess(
|
||||
org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.any(),
|
||||
org.mockito.ArgumentMatchers.anyString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建使用同一当前视图和发布视图的测试夹具。
|
||||
*
|
||||
* @param current 当前工作流
|
||||
* @return 控制器测试夹具
|
||||
*/
|
||||
private ControllerFixture fixture(Workflow current) {
|
||||
return fixture(current, current);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建控制器测试夹具。
|
||||
*
|
||||
* @param current 当前工作流
|
||||
* @param published 发布工作流视图
|
||||
* @return 控制器测试夹具
|
||||
*/
|
||||
private ControllerFixture fixture(Workflow current, Workflow published) {
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
WorkflowShareService workflowShareService = mock(WorkflowShareService.class);
|
||||
WorkflowCheckService workflowCheckService = mock(WorkflowCheckService.class);
|
||||
WorkflowRunningParameterResolver parameterResolver =
|
||||
mock(WorkflowRunningParameterResolver.class);
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
WorkflowChatEventStream eventStream = mock(WorkflowChatEventStream.class);
|
||||
WorkflowChatController controller = new WorkflowChatController();
|
||||
setField(controller, "workflowService", workflowService);
|
||||
setField(controller, "workflowShareService", workflowShareService);
|
||||
setField(controller, "workflowCheckService", workflowCheckService);
|
||||
setField(controller, "parameterResolver", parameterResolver);
|
||||
setField(controller, "resourceAccessService", resourceAccessService);
|
||||
setField(controller, "eventStream", eventStream);
|
||||
setField(controller, "chainExecutor", mock(ChainExecutor.class));
|
||||
setField(controller, "execResultService", mock(WorkflowExecResultService.class));
|
||||
setField(controller, "execStepService", mock(WorkflowExecStepService.class));
|
||||
when(workflowService.getById(BigInteger.ONE)).thenReturn(current);
|
||||
when(workflowService.getPublishedById(BigInteger.ONE)).thenReturn(published);
|
||||
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(BigInteger.ONE);
|
||||
account.setTenantId(BigInteger.ONE);
|
||||
return new ControllerFixture(
|
||||
controller,
|
||||
current,
|
||||
workflowService,
|
||||
workflowShareService,
|
||||
parameterResolver,
|
||||
resourceAccessService,
|
||||
eventStream,
|
||||
account
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工作流测试视图。
|
||||
*
|
||||
* @param publishStatus 发布状态
|
||||
* @param content 工作流内容
|
||||
* @param withSnapshot 是否包含发布快照
|
||||
* @return 工作流测试视图
|
||||
*/
|
||||
private Workflow workflow(
|
||||
PublishStatus publishStatus,
|
||||
String content,
|
||||
boolean withSnapshot
|
||||
) {
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.setId(BigInteger.ONE);
|
||||
workflow.setContent(content);
|
||||
workflow.setPublishStatus(publishStatus.getCode());
|
||||
if (withSnapshot) {
|
||||
workflow.setPublishedSnapshotJson(Map.of("content", content));
|
||||
}
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建登录账号静态模拟。
|
||||
*
|
||||
* @param account 登录账号
|
||||
* @return 静态模拟句柄
|
||||
*/
|
||||
private MockedStatic<SaTokenUtil> login(LoginAccount account) {
|
||||
MockedStatic<SaTokenUtil> login = mockStatic(SaTokenUtil.class);
|
||||
login.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
return login;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建仅提供请求头能力的轻量 Servlet 请求代理。
|
||||
*
|
||||
* @param headers 小写请求头映射
|
||||
* @return HTTP 请求代理
|
||||
*/
|
||||
private HttpServletRequest request(Map<String, String> headers) {
|
||||
return (HttpServletRequest) java.lang.reflect.Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{HttpServletRequest.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("getHeader".equals(method.getName())) {
|
||||
String name = String.valueOf(args[0]).toLowerCase(Locale.ROOT);
|
||||
return headers.get(name);
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射设置控制器依赖。
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param fieldName 字段名
|
||||
* @param value 字段值
|
||||
*/
|
||||
private void setField(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
throw new IllegalStateException("设置测试字段失败: " + fieldName, exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回代理方法所需的基础类型默认值。
|
||||
*
|
||||
* @param returnType 返回类型
|
||||
* @return 默认值
|
||||
*/
|
||||
private Object defaultValue(Class<?> returnType) {
|
||||
if (!returnType.isPrimitive()) {
|
||||
return null;
|
||||
}
|
||||
if (boolean.class == returnType) {
|
||||
return false;
|
||||
}
|
||||
if (char.class == returnType) {
|
||||
return '\0';
|
||||
}
|
||||
if (byte.class == returnType) {
|
||||
return (byte) 0;
|
||||
}
|
||||
if (short.class == returnType) {
|
||||
return (short) 0;
|
||||
}
|
||||
if (int.class == returnType) {
|
||||
return 0;
|
||||
}
|
||||
if (long.class == returnType) {
|
||||
return 0L;
|
||||
}
|
||||
if (float.class == returnType) {
|
||||
return 0F;
|
||||
}
|
||||
return 0D;
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制器及其测试依赖夹具。
|
||||
*
|
||||
* @param controller 控制器
|
||||
* @param current 当前工作流
|
||||
* @param workflowService 工作流服务
|
||||
* @param workflowShareService 工作流分享服务
|
||||
* @param parameterResolver 参数解析器
|
||||
* @param resourceAccessService 资源权限服务
|
||||
* @param eventStream 事件流服务
|
||||
* @param account 登录账号
|
||||
*/
|
||||
private record ControllerFixture(
|
||||
WorkflowChatController controller,
|
||||
Workflow current,
|
||||
WorkflowService workflowService,
|
||||
WorkflowShareService workflowShareService,
|
||||
WorkflowRunningParameterResolver parameterResolver,
|
||||
ResourceAccessService resourceAccessService,
|
||||
WorkflowChatEventStream eventStream,
|
||||
LoginAccount account
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user