From a458f77e58173c81baad321f2f13c656cbfeed9b Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 13 Aug 2026 13:49:45 +0900 Subject: [PATCH] test: split CodeNode validation from sandbox coverage --- .../workflow/nodes/test_code.py | 345 ++---------------- .../code/test_code_node_output_validation.py | 87 +++++ 2 files changed, 121 insertions(+), 311 deletions(-) create mode 100644 api/tests/unit_tests/core/workflow/nodes/code/test_code_node_output_validation.py diff --git a/api/tests/integration_tests/workflow/nodes/test_code.py b/api/tests/integration_tests/workflow/nodes/test_code.py index 9345113aa3e..6a5ddf498f9 100644 --- a/api/tests/integration_tests/workflow/nodes/test_code.py +++ b/api/tests/integration_tests/workflow/nodes/test_code.py @@ -1,3 +1,5 @@ +"""Sandbox-backed CodeNode execution tests.""" + import time import uuid @@ -18,21 +20,12 @@ from tests.workflow_test_utils import build_test_graph_init_params pytest_plugins = ("tests.integration_tests.workflow.nodes.__mock.code_executor",) -CODE_MAX_STRING_LENGTH = dify_config.CODE_MAX_STRING_LENGTH - -def init_code_node(code_config: dict): +def _init_code_node(code_config: dict) -> CodeNode: graph_config = { - "edges": [ - { - "id": "start-source-code-target", - "source": "start", - "target": "code", - }, - ], + "edges": [{"id": "start-source-code-target", "source": "start", "target": "code"}], "nodes": [{"data": {"type": "start", "title": "Start"}, "id": "start"}, code_config], } - init_params = build_test_graph_init_params( workflow_id="1", graph_config=graph_config, @@ -43,8 +36,6 @@ def init_code_node(code_config: dict): invoke_from=InvokeFrom.DEBUGGER, call_depth=0, ) - - # construct variable pool variable_pool = VariablePool.from_bootstrap( system_variables=build_system_variables(user_id="aaa", files=[]), user_inputs={}, @@ -53,18 +44,10 @@ def init_code_node(code_config: dict): ) variable_pool.add(["code", "args1"], 1) variable_pool.add(["code", "args2"], 2) - graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter()) - - # Create node factory - node_factory = DifyNodeFactory( - graph_init_params=init_params, - graph_runtime_state=graph_runtime_state, - ) - - graph = Graph.init(graph_config=graph_config, node_factory=node_factory, root_node_id="start") - - node = CodeNode( + node_factory = DifyNodeFactory(graph_init_params=init_params, graph_runtime_state=graph_runtime_state) + Graph.init(graph_config=graph_config, node_factory=node_factory, root_node_id="start") + return CodeNode( node_id=str(uuid.uuid4()), data=CodeNodeData.model_validate(code_config["data"]), graph_init_params=init_params, @@ -82,49 +65,36 @@ def init_code_node(code_config: dict): ), ) - return node - -@pytest.mark.parametrize("setup_code_executor_mock", [["none"]], indirect=True) -def test_execute_code(setup_code_executor_mock): - code = """ - def main(args1: int, args2: int): - return { - "result": args1 + args2, - } - """ - # trim first 4 spaces at the beginning of each line - code = "\n".join([line[4:] for line in code.split("\n")]) - - code_config = { +def _code_config(code: str, output_type: str, *, variables: bool = True) -> dict: + selectors = [] + if variables: + selectors = [ + {"variable": "args1", "value_selector": ["1", "args1"]}, + {"variable": "args2", "value_selector": ["1", "args2"]}, + ] + return { "id": "code", "data": { "type": "code", - "outputs": { - "result": { - "type": "number", - }, - }, - "title": "123", - "variables": [ - { - "variable": "args1", - "value_selector": ["1", "args1"], - }, - {"variable": "args2", "value_selector": ["1", "args2"]}, - ], - "answer": "123", + "outputs": {"result": {"type": output_type}}, + "title": "Code", + "variables": selectors, "code_language": "python3", "code": code, }, } - node = init_code_node(code_config) + +@pytest.mark.parametrize("setup_code_executor_mock", [["none"]], indirect=True) +def test_execute_code(setup_code_executor_mock) -> None: + code = "def main(args1: int, args2: int):\n return {'result': args1 + args2}" + node = _init_code_node(_code_config(code, "number")) node.graph_runtime_state.variable_pool.add(["1", "args1"], 1) node.graph_runtime_state.variable_pool.add(["1", "args2"], 2) - # execute node result = node._run() + assert isinstance(result, NodeRunResult) assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert result.outputs is not None @@ -133,274 +103,27 @@ def test_execute_code(setup_code_executor_mock): @pytest.mark.parametrize("setup_code_executor_mock", [["none"]], indirect=True) -def test_execute_code_output_validator(setup_code_executor_mock): - code = """ - def main(args1: int, args2: int): - return { - "result": args1 + args2, - } - """ - # trim first 4 spaces at the beginning of each line - code = "\n".join([line[4:] for line in code.split("\n")]) - - code_config = { - "id": "code", - "data": { - "type": "code", - "outputs": { - "result": { - "type": "string", - }, - }, - "title": "123", - "variables": [ - { - "variable": "args1", - "value_selector": ["1", "args1"], - }, - {"variable": "args2", "value_selector": ["1", "args2"]}, - ], - "answer": "123", - "code_language": "python3", - "code": code, - }, - } - - node = init_code_node(code_config) +def test_execute_code_output_validator(setup_code_executor_mock) -> None: + code = "def main(args1: int, args2: int):\n return {'result': args1 + args2}" + node = _init_code_node(_code_config(code, "string")) node.graph_runtime_state.variable_pool.add(["1", "args1"], 1) node.graph_runtime_state.variable_pool.add(["1", "args2"], 2) - # execute node result = node._run() + assert isinstance(result, NodeRunResult) assert result.status == WorkflowNodeExecutionStatus.FAILED assert result.error == "Output result must be a string, got int instead." -def test_execute_code_output_validator_depth(): - code = """ - def main(args1: int, args2: int): - return { - "result": { - "result": args1 + args2, - } - } - """ - # trim first 4 spaces at the beginning of each line - code = "\n".join([line[4:] for line in code.split("\n")]) - - code_config = { - "id": "code", - "data": { - "type": "code", - "outputs": { - "string_validator": { - "type": "string", - }, - "number_validator": { - "type": "number", - }, - "number_array_validator": { - "type": "array[number]", - }, - "string_array_validator": { - "type": "array[string]", - }, - "object_validator": { - "type": "object", - "children": { - "result": { - "type": "number", - }, - "depth": { - "type": "object", - "children": { - "depth": { - "type": "object", - "children": { - "depth": { - "type": "number", - } - }, - } - }, - }, - }, - }, - }, - "title": "123", - "variables": [ - { - "variable": "args1", - "value_selector": ["1", "args1"], - }, - {"variable": "args2", "value_selector": ["1", "args2"]}, - ], - "answer": "123", - "code_language": "python3", - "code": code, - }, - } - - node = init_code_node(code_config) - - # construct result - result = { - "number_validator": 1, - "string_validator": "1", - "number_array_validator": [1, 2, 3, 3.333], - "string_array_validator": ["1", "2", "3"], - "object_validator": {"result": 1, "depth": {"depth": {"depth": 1}}}, - } - - # validate - node._transform_result(result, node._node_data.outputs) - - # construct result - result = { - "number_validator": "1", - "string_validator": 1, - "number_array_validator": ["1", "2", "3", "3.333"], - "string_array_validator": [1, 2, 3], - "object_validator": {"result": "1", "depth": {"depth": {"depth": "1"}}}, - } - - # validate - with pytest.raises(ValueError): - node._transform_result(result, node._node_data.outputs) - - # construct result - result = { - "number_validator": 1, - "string_validator": (CODE_MAX_STRING_LENGTH + 1) * "1", - "number_array_validator": [1, 2, 3, 3.333], - "string_array_validator": ["1", "2", "3"], - "object_validator": {"result": 1, "depth": {"depth": {"depth": 1}}}, - } - - # validate - with pytest.raises(ValueError): - node._transform_result(result, node._node_data.outputs) - - # construct result - result = { - "number_validator": 1, - "string_validator": "1", - "number_array_validator": [1, 2, 3, 3.333] * 2000, - "string_array_validator": ["1", "2", "3"], - "object_validator": {"result": 1, "depth": {"depth": {"depth": 1}}}, - } - - # validate - with pytest.raises(ValueError): - node._transform_result(result, node._node_data.outputs) - - -def test_execute_code_output_object_list(): - code = """ - def main(args1: int, args2: int): - return { - "result": { - "result": args1 + args2, - } - } - """ - # trim first 4 spaces at the beginning of each line - code = "\n".join([line[4:] for line in code.split("\n")]) - - code_config = { - "id": "code", - "data": { - "type": "code", - "outputs": { - "object_list": { - "type": "array[object]", - }, - }, - "title": "123", - "variables": [ - { - "variable": "args1", - "value_selector": ["1", "args1"], - }, - {"variable": "args2", "value_selector": ["1", "args2"]}, - ], - "answer": "123", - "code_language": "python3", - "code": code, - }, - } - - node = init_code_node(code_config) - - # construct result - result = { - "object_list": [ - { - "result": 1, - }, - { - "result": 2, - }, - { - "result": [1, 2, 3], - }, - ] - } - - # validate - node._transform_result(result, node._node_data.outputs) - - # construct result - result = { - "object_list": [ - { - "result": 1, - }, - { - "result": 2, - }, - { - "result": [1, 2, 3], - }, - 1, - ] - } - - # validate - with pytest.raises(ValueError): - node._transform_result(result, node._node_data.outputs) - - @pytest.mark.parametrize("setup_code_executor_mock", [["none"]], indirect=True) -def test_execute_code_scientific_notation(setup_code_executor_mock): - code = """ - def main(): - return { - "result": -8.0E-5 - } - """ - code = "\n".join([line[4:] for line in code.split("\n")]) +def test_execute_code_scientific_notation(setup_code_executor_mock) -> None: + code = "def main():\n return {'result': -8.0E-5}" + node = _init_code_node(_code_config(code, "number", variables=False)) - code_config = { - "id": "code", - "data": { - "type": "code", - "outputs": { - "result": { - "type": "number", - }, - }, - "title": "123", - "variables": [], - "answer": "123", - "code_language": "python3", - "code": code, - }, - } - - node = init_code_node(code_config) - # execute node result = node._run() + assert isinstance(result, NodeRunResult) assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert result.outputs is not None + assert result.outputs["result"] == -8e-5 diff --git a/api/tests/unit_tests/core/workflow/nodes/code/test_code_node_output_validation.py b/api/tests/unit_tests/core/workflow/nodes/code/test_code_node_output_validation.py new file mode 100644 index 00000000000..015eee83da5 --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/code/test_code_node_output_validation.py @@ -0,0 +1,87 @@ +"""Unit coverage for CodeNode output transformation and limits.""" + +import pytest + +from configs import dify_config +from graphon.nodes.code.code_node import CodeNode +from graphon.nodes.code.entities import CodeNodeData +from graphon.nodes.code.limits import CodeNodeLimits + + +def _node() -> CodeNode: + node = CodeNode.__new__(CodeNode) + node._limits = CodeNodeLimits( + max_string_length=dify_config.CODE_MAX_STRING_LENGTH, + max_number=dify_config.CODE_MAX_NUMBER, + min_number=dify_config.CODE_MIN_NUMBER, + max_precision=dify_config.CODE_MAX_PRECISION, + max_depth=dify_config.CODE_MAX_DEPTH, + max_number_array_length=dify_config.CODE_MAX_NUMBER_ARRAY_LENGTH, + max_string_array_length=dify_config.CODE_MAX_STRING_ARRAY_LENGTH, + max_object_array_length=dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH, + ) + return node + + +def _outputs() -> dict[str, CodeNodeData.Output]: + return { + "string_validator": CodeNodeData.Output(type="string"), + "number_validator": CodeNodeData.Output(type="number"), + "number_array_validator": CodeNodeData.Output(type="array[number]"), + "string_array_validator": CodeNodeData.Output(type="array[string]"), + "object_validator": CodeNodeData.Output( + type="object", + children={ + "result": CodeNodeData.Output(type="number"), + "depth": CodeNodeData.Output( + type="object", + children={ + "depth": CodeNodeData.Output( + type="object", + children={"depth": CodeNodeData.Output(type="number")}, + ) + }, + ), + }, + ), + } + + +def test_transform_result_validates_nested_values_and_limits() -> None: + node = _node() + outputs = _outputs() + valid = { + "number_validator": 1, + "string_validator": "1", + "number_array_validator": [1, 2, 3, 3.333], + "string_array_validator": ["1", "2", "3"], + "object_validator": {"result": 1, "depth": {"depth": {"depth": 1}}}, + } + node._transform_result(valid, outputs) + + wrong_types = { + "number_validator": "1", + "string_validator": 1, + "number_array_validator": ["1"], + "string_array_validator": [1], + "object_validator": {"result": "1", "depth": {"depth": {"depth": "1"}}}, + } + with pytest.raises(ValueError): + node._transform_result(wrong_types, outputs) + + overlong = valid | {"string_validator": "1" * (dify_config.CODE_MAX_STRING_LENGTH + 1)} + with pytest.raises(ValueError): + node._transform_result(overlong, outputs) + + oversized_array = valid | {"number_array_validator": [1, 2, 3, 3.333] * 2000} + with pytest.raises(ValueError): + node._transform_result(oversized_array, outputs) + + +def test_transform_result_validates_object_arrays() -> None: + node = _node() + outputs = {"object_list": CodeNodeData.Output(type="array[object]")} + node._transform_result({"object_list": [{"result": 1}, {"result": [1, 2, 3]}]}, outputs) + + with pytest.raises(ValueError): + node._transform_result({"object_list": [{"result": 1}, 1]}, outputs)