feat: 增加工作流合法性校验功能

This commit is contained in:
2026-03-04 19:56:42 +08:00
parent a79718b03b
commit ae9bb2c53f
12 changed files with 1755 additions and 38 deletions

View File

@@ -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;
}
}