test: split CodeNode validation from sandbox coverage

This commit is contained in:
Asuka Minato 2026-08-13 13:49:45 +09:00 committed by GitHub
parent 7737e5474f
commit a458f77e58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 121 additions and 311 deletions

View File

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

View File

@ -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)