first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,108 @@
import pytest_asyncio
from src.message_types.broker import Items
from src.message_serde import NODE_MODE_MAP
from tests.fixtures.local_task_broker import LocalTaskBroker
from tests.fixtures.task_runner_manager import TaskRunnerManager
from tests.fixtures.test_constants import (
TASK_RESPONSE_WAIT,
)
NODE_MODE_TO_BROKER_STYLE = {v: k for k, v in NODE_MODE_MAP.items()}
@pytest_asyncio.fixture
async def broker():
broker = LocalTaskBroker()
await broker.start()
yield broker
await broker.stop()
@pytest_asyncio.fixture
async def manager(broker):
manager = TaskRunnerManager(task_broker_url=broker.get_url())
await manager.start()
yield manager
await manager.stop()
@pytest_asyncio.fixture
async def manager_with_stdlib_wildcard(broker):
manager = TaskRunnerManager(
task_broker_url=broker.get_url(),
custom_env={
"N8N_RUNNERS_STDLIB_ALLOW": "*",
},
)
await manager.start()
yield manager
await manager.stop()
@pytest_asyncio.fixture
async def manager_with_env_access_blocked(broker):
manager = TaskRunnerManager(
task_broker_url=broker.get_url(),
custom_env={
"N8N_RUNNERS_STDLIB_ALLOW": "os",
"N8N_BLOCK_RUNNER_ENV_ACCESS": "true",
},
)
await manager.start()
yield manager
await manager.stop()
@pytest_asyncio.fixture
async def manager_with_env_access_allowed(broker):
manager = TaskRunnerManager(
task_broker_url=broker.get_url(),
custom_env={
"N8N_RUNNERS_STDLIB_ALLOW": "os",
"N8N_BLOCK_RUNNER_ENV_ACCESS": "false",
},
)
await manager.start()
yield manager
await manager.stop()
def create_task_settings(
code: str,
node_mode: str,
items: Items | None = None,
continue_on_fail: bool = False,
):
return {
"code": code,
"nodeMode": NODE_MODE_TO_BROKER_STYLE[node_mode],
"items": items if items is not None else [],
"continueOnFail": continue_on_fail,
}
async def wait_for_task_done(broker, task_id: str, timeout: float = TASK_RESPONSE_WAIT):
return await broker.wait_for_msg(
"runner:taskdone",
timeout=timeout,
predicate=lambda msg: msg.get("taskId") == task_id,
)
async def wait_for_task_error(
broker, task_id: str, timeout: float = TASK_RESPONSE_WAIT
):
return await broker.wait_for_msg(
"runner:taskerror",
timeout=timeout,
predicate=lambda msg: msg.get("taskId") == task_id,
)
def get_browser_console_msgs(broker: LocalTaskBroker, task_id: str) -> list[list[str]]:
console_msgs = []
for msg in broker.get_task_rpc_messages(task_id):
if msg.get("method") == "logNodeOutput":
console_msgs.append(msg.get("params", []))
return console_msgs
@@ -0,0 +1,458 @@
import asyncio
import textwrap
import pytest
from src.nanoid import nanoid
from tests.integration.conftest import (
create_task_settings,
wait_for_task_done,
wait_for_task_error,
)
from tests.fixtures.test_constants import TASK_TIMEOUT
# ========== all_items mode ==========
@pytest.mark.asyncio
async def test_all_items_with_success(broker, manager):
task_id = nanoid()
items = [
{"json": {"name": "Alice", "age": 30}},
{"json": {"name": "Bob", "age": 16}},
{"json": {"name": "Charlie", "age": 35}},
]
code = textwrap.dedent("""
result = []
for item in _items:
person = item['json']
result.append({
'name': person['name'],
'age': person['age'],
'adult': person['age'] >= 18
})
return result
""")
task_settings = create_task_settings(code=code, node_mode="all_items", items=items)
await broker.send_task(task_id=task_id, task_settings=task_settings)
result = await wait_for_task_done(broker, task_id)
assert result["data"]["result"] == [
{"name": "Alice", "age": 30, "adult": True},
{"name": "Bob", "age": 16, "adult": False},
{"name": "Charlie", "age": 35, "adult": True},
]
@pytest.mark.asyncio
async def test_all_items_with_error(broker, manager):
task_id = nanoid()
code = "raise ValueError('Intentional error')"
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
error_msg = await wait_for_task_error(broker, task_id)
assert error_msg["taskId"] == task_id
assert "Intentional error" in str(error_msg["error"]["message"])
@pytest.mark.asyncio
async def test_all_items_with_continue_on_fail(broker, manager):
task_id = nanoid()
code = "raise ValueError('Intentional error')"
task_settings = create_task_settings(
code=code, node_mode="all_items", continue_on_fail=True
)
await broker.send_task(task_id=task_id, task_settings=task_settings)
done_msg = await wait_for_task_done(broker, task_id)
assert len(done_msg["data"]["result"]) == 1
assert "error" in done_msg["data"]["result"][0]["json"]
assert "Intentional error" in str(done_msg["data"]["result"][0]["json"]["error"])
# ========== per_item mode ==========
@pytest.mark.asyncio
async def test_per_item_with_success(broker, manager):
task_id = nanoid()
items = [
{"json": {"value": 10}},
{"json": {"value": 20}},
{"json": {"value": 30}},
]
code = "return {'doubled': _item['json']['value'] * 2}"
task_settings = create_task_settings(code=code, node_mode="per_item", items=items)
await broker.send_task(task_id=task_id, task_settings=task_settings)
done_msg = await wait_for_task_done(broker, task_id)
assert done_msg["taskId"] == task_id
assert done_msg["data"]["result"] == [
{"json": {"doubled": 20}, "pairedItem": {"item": 0}},
{"json": {"doubled": 40}, "pairedItem": {"item": 1}},
{"json": {"doubled": 60}, "pairedItem": {"item": 2}},
]
@pytest.mark.asyncio
async def test_per_item_with_explicit_json_and_binary(broker, manager):
task_id = nanoid()
items = [{"json": {"value": 10}}]
code = "return {'json': {'custom': 'data'}, 'binary': {'file': 'data'}}"
task_settings = create_task_settings(code=code, node_mode="per_item", items=items)
await broker.send_task(task_id=task_id, task_settings=task_settings)
result = await wait_for_task_done(broker, task_id)
assert result["data"]["result"] == [
{
"json": {"custom": "data"},
"binary": {"file": "data"},
"pairedItem": {"item": 0},
}
]
@pytest.mark.asyncio
async def test_per_item_with_binary_only(broker, manager):
task_id = nanoid()
items = [{"json": {"value": 10}}]
code = "return {'binary': {'file': 'data'}}"
task_settings = create_task_settings(code=code, node_mode="per_item", items=items)
await broker.send_task(task_id=task_id, task_settings=task_settings)
result = await wait_for_task_done(broker, task_id)
assert result["data"]["result"] == [
{"json": {}, "binary": {"file": "data"}, "pairedItem": {"item": 0}}
]
@pytest.mark.asyncio
async def test_per_item_with_filtering(broker, manager):
task_id = nanoid()
items = [
{"json": {"value": 5}},
{"json": {"value": 15}},
{"json": {"value": 25}},
{"json": {"value": 8}},
]
code = textwrap.dedent("""
value = _item['json']['value']
if value > 10:
return {'value': value, 'passed': True}
else:
return None # Filter out this item
""")
task_settings = create_task_settings(code=code, node_mode="per_item", items=items)
await broker.send_task(task_id=task_id, task_settings=task_settings)
result = await wait_for_task_done(broker, task_id)
assert result["data"]["result"] == [
{"json": {"value": 15, "passed": True}, "pairedItem": {"item": 1}},
{"json": {"value": 25, "passed": True}, "pairedItem": {"item": 2}},
]
@pytest.mark.asyncio
async def test_per_item_with_continue_on_fail(broker, manager):
task_id = nanoid()
items = [
{"json": {"value": 10}},
{"json": {"value": 0}}, # Will cause division by zero
{"json": {"value": 20}},
]
code = "return {'result': 100 / _item['json']['value']}"
task_settings = create_task_settings(
code=code,
node_mode="per_item",
items=items,
continue_on_fail=True,
)
await broker.send_task(task_id=task_id, task_settings=task_settings)
done_msg = await wait_for_task_done(broker, task_id)
assert len(done_msg["data"]["result"]) == 1
assert "error" in done_msg["data"]["result"][0]["json"]
assert "division by zero" in done_msg["data"]["result"][0]["json"]["error"]
# ========== Security ===========
@pytest.mark.asyncio
async def test_cannot_access_builtins_via_globals(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
b = globals()['__builtins__']
imp = b['__import__']
os = imp('os')
return [{"json": {"pid": os.getpid()}}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
error_msg = await wait_for_task_error(broker, task_id)
assert error_msg["taskId"] == task_id
assert "globals" in str(error_msg["error"]["message"]).lower()
@pytest.mark.asyncio
async def test_cannot_access_builtins_via_locals(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
b = locals()['__builtins__']
imp = b['__import__']
os = imp('os')
return [{"json": {"pid": os.getpid()}}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
error_msg = await wait_for_task_error(broker, task_id)
assert error_msg["taskId"] == task_id
assert "locals" in str(error_msg["error"]["message"]).lower()
# ========== edge cases ===========
@pytest.mark.asyncio
async def test_cancel_during_execution(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
import time
for i in range(20):
time.sleep(0.05)
if i == 10:
# Should be cancelled around here
pass
return [{"completed": "should not reach here"}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
await asyncio.sleep(0.3)
await broker.cancel_task(task_id, reason="Cancelled during execution")
error_msg = await wait_for_task_error(broker, task_id)
assert error_msg["taskId"] == task_id
assert "error" in error_msg
@pytest.mark.asyncio
async def test_timeout_during_execution(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
while True:
pass
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
error_msg = await wait_for_task_error(broker, task_id, timeout=TASK_TIMEOUT + 1.5)
assert error_msg["taskId"] == task_id
assert "timed out" in error_msg["error"]["message"].lower()
@pytest.mark.asyncio
async def test_stdlib_submodules_with_wildcard(broker, manager_with_stdlib_wildcard):
task_id = nanoid()
code = textwrap.dedent("""
from collections.abc import Iterable
result = isinstance([1, 2, 3], Iterable)
return [{"json": {"is_iterable": result}}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
result = await wait_for_task_done(broker, task_id)
assert result["data"]["result"] == [{"json": {"is_iterable": True}}]
@pytest.mark.asyncio
async def test_cannot_bypass_import_restrictions_via_builtins_dict(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
os = __builtins__['__import__']('os')
print(os.getpid())
return []
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
error_msg = await wait_for_task_error(broker, task_id)
assert error_msg["taskId"] == task_id
assert "error" in error_msg
assert "__import__" in str(error_msg["error"]["description"]).lower()
@pytest.mark.asyncio
async def test_cannot_bypass_import_restrictions_via_builtins_spec_loader(
broker, manager
):
task_id = nanoid()
code = textwrap.dedent("""
sys = __builtins__['__spec__'].loader.load_module('sys')
os = sys.meta_path[-1].find_spec("os").loader.load_module('os')
return [{"json": {"pid": os.getpid()}}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
error_msg = await wait_for_task_error(broker, task_id)
assert error_msg["taskId"] == task_id
assert "error" in error_msg
@pytest.mark.asyncio
async def test_cannot_bypass_import_restrictions_via_sys_builtins_spec_leader(
broker, manager_with_stdlib_wildcard
):
task_id = nanoid()
code = textwrap.dedent("""
import sys
os = sys.__builtins__['__spec__'].loader.load_module('os')
return [{"json": {"pid": os.getpid()}}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
error_msg = await wait_for_task_error(broker, task_id)
assert error_msg["taskId"] == task_id
assert "error" in error_msg
@pytest.mark.asyncio
async def test_cannot_bypass_import_restrictions_via_format_string(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
ex = None
try:
"{.__builtins__[__import__].__call__.a}".format(print)
except Exception as e:
ex = e
return [{"json": {"error": str(ex)}}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
error_msg = await wait_for_task_error(broker, task_id)
assert error_msg["taskId"] == task_id
assert "error" in error_msg
assert "__builtins__" in str(error_msg["error"]["description"]).lower()
@pytest.mark.asyncio
async def test_env_blocked_by_default_all_items(
broker, manager_with_env_access_blocked
):
task_id = nanoid()
code = textwrap.dedent("""
import os
path = os.environ.get('PATH', 'NOT_FOUND')
home = os.environ.get('HOME', 'NOT_FOUND')
env_dict = dict(os.environ)
return [{"json": {
"path": path,
"home": home,
"env_count": len(env_dict)
}}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
result = await wait_for_task_done(broker, task_id)
assert result["data"]["result"][0]["json"]["path"] == "NOT_FOUND"
assert result["data"]["result"][0]["json"]["home"] == "NOT_FOUND"
assert result["data"]["result"][0]["json"]["env_count"] == 0
@pytest.mark.asyncio
async def test_env_blocked_by_default_per_item(broker, manager_with_env_access_blocked):
task_id = nanoid()
items = [
{"json": {"index": 0}},
{"json": {"index": 1}},
]
code = textwrap.dedent("""
import os
path = os.environ.get('PATH', 'NOT_FOUND')
return {"path": path, "env_count": len(dict(os.environ))}
""")
task_settings = create_task_settings(code=code, node_mode="per_item", items=items)
await broker.send_task(task_id=task_id, task_settings=task_settings)
result = await wait_for_task_done(broker, task_id)
assert len(result["data"]["result"]) == 2
for item in result["data"]["result"]:
assert item["json"]["path"] == "NOT_FOUND"
assert item["json"]["env_count"] == 0
@pytest.mark.asyncio
async def test_env_accessible_when_allowed_all_items(
broker, manager_with_env_access_allowed
):
task_id = nanoid()
code = textwrap.dedent("""
import os
path = os.environ.get('PATH', 'NOT_FOUND')
env_dict = dict(os.environ)
return [{"json": {
"has_path": path != 'NOT_FOUND',
"env_count": len(env_dict)
}}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
result = await wait_for_task_done(broker, task_id)
assert result["data"]["result"][0]["json"]["has_path"] is True
assert result["data"]["result"][0]["json"]["env_count"] > 0
@pytest.mark.asyncio
async def test_env_accessible_when_allowed_per_item(
broker, manager_with_env_access_allowed
):
task_id = nanoid()
items = [
{"json": {"index": 0}},
{"json": {"index": 1}},
]
code = textwrap.dedent("""
import os
path = os.environ.get('PATH', 'NOT_FOUND')
return {
"has_path": path != 'NOT_FOUND',
"env_count": len(dict(os.environ))
}
""")
task_settings = create_task_settings(code=code, node_mode="per_item", items=items)
await broker.send_task(task_id=task_id, task_settings=task_settings)
result = await wait_for_task_done(broker, task_id)
assert len(result["data"]["result"]) == 2
for item in result["data"]["result"]:
assert item["json"]["has_path"] is True
assert item["json"]["env_count"] > 0
@@ -0,0 +1,40 @@
import asyncio
import textwrap
import aiohttp
import pytest
from src.nanoid import nanoid
from tests.integration.conftest import create_task_settings
@pytest.mark.asyncio
async def test_health_check_server_responds(broker, manager):
health_check_url = manager.get_health_check_url()
async with aiohttp.ClientSession() as session:
for _ in range(10):
try:
response = await session.get(health_check_url)
if response.status == 200:
assert await response.text() == "OK"
return
except aiohttp.ClientConnectionError:
await asyncio.sleep(0.1)
@pytest.mark.asyncio
async def test_health_check_server_ressponds_mid_execution(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
for _ in range(10_000_000):
pass
return [{"result": "completed"}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
await asyncio.sleep(0.3)
async with aiohttp.ClientSession() as session:
response = await session.get(manager.get_health_check_url())
assert response.status == 200
assert await response.text() == "OK"
@@ -0,0 +1,116 @@
import textwrap
import pytest
from src.nanoid import nanoid
from tests.integration.conftest import (
create_task_settings,
get_browser_console_msgs,
wait_for_task_done,
)
@pytest.mark.asyncio
async def test_print_basic_types(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
print("Hello, World!")
print(42)
print(3.14)
print(True)
print(None)
print("Multiple", "args", 123, False)
return [{"printed": "ok"}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
done_msg = await wait_for_task_done(broker, task_id, timeout=5.0)
assert done_msg["taskId"] == task_id
assert done_msg["data"]["result"] == [{"printed": "ok"}]
msgs = get_browser_console_msgs(broker, task_id)
assert len(msgs) > 0, "Should have captured console messages"
all_args = []
for msg in msgs:
all_args.extend(msg)
expected = [
"'Hello, World!'",
"42",
"3.14",
"True",
"None",
"'Multiple'",
"'args'",
"123",
"False",
]
for item in expected:
assert item in all_args, f"Expected '{item}' not found in console output"
@pytest.mark.asyncio
async def test_print_complex_types(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
print({"name": "John", "age": 30, "active": True})
print([1, 2, "three", {"four": 4}])
print({"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]})
return [{"result": "success"}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
result_msg = await wait_for_task_done(broker, task_id, timeout=5.0)
assert result_msg["data"]["result"] == [{"result": "success"}]
msgs = get_browser_console_msgs(broker, task_id)
assert len(msgs) > 0, "Should have captured console messages"
all_output = " ".join(["".join(msg) for msg in msgs]).replace(" ", "")
expected = [
'{"name":"John","age":30,"active":true}',
'[1,2,"three",{"four":4}]',
]
for item in expected:
assert item in all_output, f"Expected '{item}' not found in console output"
@pytest.mark.asyncio
async def test_print_edge_cases(broker, manager):
task_id = nanoid()
code = textwrap.dedent("""
print("Hello 世界 🌍")
print({"emoji": "🚀", "chinese": "你好", "arabic": "مرحبا"})
print("Line\\nbreak")
print("Tab\\tseparated")
print('Quote "test" here')
print()
print("")
print(" ")
print([])
print({})
print(())
print("x" * 1_000)
return [{"test": "complete"}]
""")
task_settings = create_task_settings(code=code, node_mode="all_items")
await broker.send_task(task_id=task_id, task_settings=task_settings)
done_msg = await wait_for_task_done(broker, task_id, timeout=5.0)
assert done_msg["data"]["result"] == [{"test": "complete"}]
msgs = get_browser_console_msgs(broker, task_id)
assert len(msgs) > 0, "Should have captured console messages"
all_output = " ".join(["".join(msg) for msg in msgs])
expected = ["世界", "🌍", "🚀", "你好", "[]", "{}"]
for item in expected:
assert item in all_output, f"Expected '{item}' not found in console output"