feat: 增加工作流合法性校验功能
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
package tech.easyflow.ai.easyagentsflow.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.node.WorkflowNodeParser;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class WorkflowCheckServiceTest {
|
||||
|
||||
@Test
|
||||
public void testSaveShouldPassForValidDraft() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
String content = workflowJson(
|
||||
array(
|
||||
node("start-1", "startNode", null, data("开始")),
|
||||
node("code-1", "codeNode", null, data("处理中"))
|
||||
),
|
||||
new JSONArray()
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
|
||||
Assert.assertTrue(result.isPassed());
|
||||
Assert.assertEquals(0, result.getIssueCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveShouldBlockInvalidJson() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
WorkflowCheckResult result = service.checkContent("{invalid-json", WorkflowCheckStage.SAVE, null);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "INVALID_JSON");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveShouldBlockUnknownNodeType() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
String content = workflowJson(
|
||||
array(node("n1", "unknownNodeType", null, data("未知节点"))),
|
||||
new JSONArray()
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "NODE_TYPE_UNKNOWN");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveShouldBlockEdgeWithMissingNode() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
String content = workflowJson(
|
||||
array(node("n1", "startNode", null, data("开始"))),
|
||||
array(edge("e1", "n1", "n2"))
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "EDGE_TARGET_NOT_FOUND");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldBlockMissingStartOrEnd() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
String content = workflowJson(
|
||||
array(
|
||||
node("c1", "codeNode", null, data("处理")),
|
||||
node("c2", "codeNode", null, data("处理2"))
|
||||
),
|
||||
array(edge("e1", "c1", "c2"))
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "START_NODE_MISSING");
|
||||
assertHasCode(result, "END_NODE_MISSING");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldBlockRootEntryNotStart() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
String content = workflowJson(
|
||||
array(
|
||||
node("s1", "startNode", null, data("开始")),
|
||||
node("x1", "codeNode", null, data("孤立入口")),
|
||||
node("e1", "endNode", null, data("结束"))
|
||||
),
|
||||
array(edge("e-1", "s1", "e1"))
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "ROOT_ENTRY_NOT_START");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldBlockGraphCycle() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
String content = workflowJson(
|
||||
array(
|
||||
node("s1", "startNode", null, data("开始")),
|
||||
node("c1", "codeNode", null, data("处理")),
|
||||
node("e1", "endNode", null, data("结束"))
|
||||
),
|
||||
array(
|
||||
edge("e1", "s1", "c1"),
|
||||
edge("e2", "c1", "s1"),
|
||||
edge("e3", "c1", "e1")
|
||||
)
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "GRAPH_CYCLE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldBlockDeadEndAndUnreachableEnd() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
String content = workflowJson(
|
||||
array(
|
||||
node("s1", "startNode", null, data("开始")),
|
||||
node("c1", "codeNode", null, data("死路节点")),
|
||||
node("e1", "endNode", null, data("结束"))
|
||||
),
|
||||
array(
|
||||
edge("e1", "s1", "c1"),
|
||||
edge("e2", "s1", "e1")
|
||||
)
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "DEAD_END_NODE");
|
||||
assertHasCode(result, "END_UNREACHABLE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldBlockWorkflowRecursiveReference() throws Exception {
|
||||
Map<String, String> workflowStore = new HashMap<>();
|
||||
workflowStore.put("2", workflowJson(
|
||||
array(
|
||||
node("s2", "startNode", null, data("开始2")),
|
||||
workflowNode("w2", null, "1"),
|
||||
node("e2", "endNode", null, data("结束2"))
|
||||
),
|
||||
array(
|
||||
edge("e2-1", "s2", "w2"),
|
||||
edge("e2-2", "w2", "e2")
|
||||
)
|
||||
));
|
||||
WorkflowCheckService service = newService(workflowStore);
|
||||
String rootContent = workflowJson(
|
||||
array(
|
||||
node("s1", "startNode", null, data("开始1")),
|
||||
workflowNode("w1", null, "2"),
|
||||
node("e1", "endNode", null, data("结束1"))
|
||||
),
|
||||
array(
|
||||
edge("e1-1", "s1", "w1"),
|
||||
edge("e1-2", "w1", "e1")
|
||||
)
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(rootContent, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "WORKFLOW_REF_CYCLE");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldPassForExecutableWorkflow() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
String content = workflowJson(
|
||||
array(
|
||||
node("s1", "startNode", null, data("开始")),
|
||||
node("c1", "codeNode", null, data("处理")),
|
||||
node("e1", "endNode", null, data("结束"))
|
||||
),
|
||||
array(
|
||||
edge("e1", "s1", "c1"),
|
||||
edge("e2", "c1", "e1")
|
||||
)
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
|
||||
Assert.assertTrue(result.isPassed());
|
||||
Assert.assertEquals(0, result.getIssueCount());
|
||||
}
|
||||
|
||||
private static WorkflowCheckService newService(Map<String, String> workflowStore) throws Exception {
|
||||
WorkflowCheckService service = new WorkflowCheckService();
|
||||
ChainParser parser = ChainParser.builder()
|
||||
.withDefaultParsers(true)
|
||||
.build();
|
||||
parser.addNodeParser("workflow-node", new WorkflowNodeParser());
|
||||
setField(service, "chainParser", parser);
|
||||
setField(service, "workflowService", mockWorkflowService(workflowStore));
|
||||
return service;
|
||||
}
|
||||
|
||||
private static WorkflowService mockWorkflowService(Map<String, String> workflowStore) {
|
||||
return (WorkflowService) Proxy.newProxyInstance(
|
||||
WorkflowService.class.getClassLoader(),
|
||||
new Class[]{WorkflowService.class},
|
||||
(proxy, method, args) -> {
|
||||
String methodName = method.getName();
|
||||
if ("getById".equals(methodName)) {
|
||||
if (args == null || args.length == 0 || args[0] == null) {
|
||||
return null;
|
||||
}
|
||||
String id = String.valueOf(args[0]);
|
||||
if (!workflowStore.containsKey(id)) {
|
||||
return null;
|
||||
}
|
||||
Workflow workflow = new Workflow();
|
||||
try {
|
||||
workflow.setId(new BigInteger(id));
|
||||
} catch (Exception ignored) {
|
||||
workflow.setId(null);
|
||||
}
|
||||
workflow.setContent(workflowStore.get(id));
|
||||
workflow.setTitle("workflow-" + id);
|
||||
return workflow;
|
||||
}
|
||||
if ("equals".equals(methodName)) {
|
||||
return proxy == args[0];
|
||||
}
|
||||
if ("hashCode".equals(methodName)) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
if (method.getReturnType() == boolean.class) {
|
||||
return false;
|
||||
}
|
||||
if (method.getReturnType() == int.class) {
|
||||
return 0;
|
||||
}
|
||||
if (method.getReturnType() == long.class) {
|
||||
return 0L;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value) throws Exception {
|
||||
Field field = WorkflowCheckService.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private static void assertHasCode(WorkflowCheckResult result, String code) {
|
||||
boolean exists = result.getIssues().stream().anyMatch(issue -> code.equals(issue.getCode()));
|
||||
Assert.assertTrue("missing issue code: " + code, exists);
|
||||
}
|
||||
|
||||
private static String workflowJson(JSONArray nodes, JSONArray edges) {
|
||||
JSONObject root = new JSONObject();
|
||||
root.put("nodes", nodes);
|
||||
root.put("edges", edges);
|
||||
return root.toJSONString();
|
||||
}
|
||||
|
||||
private static JSONArray array(JSONObject... objects) {
|
||||
JSONArray array = new JSONArray();
|
||||
for (JSONObject object : objects) {
|
||||
array.add(object);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private static JSONObject node(String id, String type, String parentId, JSONObject data) {
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("id", id);
|
||||
node.put("type", type);
|
||||
if (parentId != null) {
|
||||
node.put("parentId", parentId);
|
||||
}
|
||||
node.put("data", data);
|
||||
return node;
|
||||
}
|
||||
|
||||
private static JSONObject workflowNode(String id, String parentId, String workflowId) {
|
||||
JSONObject data = data("子流程");
|
||||
data.put("workflowId", workflowId);
|
||||
return node(id, "workflow-node", parentId, data);
|
||||
}
|
||||
|
||||
private static JSONObject data(String title) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("title", title);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static JSONObject edge(String id, String source, String target) {
|
||||
JSONObject edge = new JSONObject();
|
||||
edge.put("id", id);
|
||||
edge.put("source", source);
|
||||
edge.put("target", target);
|
||||
return edge;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user