Paper2Agent_decoupleRpy / tests /test_executor_seam.py
Annie Voigt
fix(tests): stop the executor shims squatting `managers` in sys.modules
ef311cd
Raw
History Blame Contribute Delete
7.42 kB
"""
Network-free tests for the executor seam (ADR-0007 Phase 0,
`src/managers/execution/base.py`).
Covers executor SELECTION via the EXECUTOR env var (in_process | sandbox +
unknown), that the default factory returns an in-process executor conforming to
the `Executor` protocol, that it round-trips a trivial `execute(...)`, that the
`sandbox` value returns a `SandboxedExecutor` (ADR-0007 Phase 1 — was a
NotImplementedError stub in Phase 0), and that an unknown value raises ValueError.
The factory dispatch / error paths need no heavy deps. The round-trip touches
`PythonExecutor.reset_environment`, which imports pandas/numpy — guarded with
`importorskip` so the dispatch/error assertions still run where those are absent.
"""
from __future__ import annotations
import importlib.util
import sys
import types
from pathlib import Path
import pytest
# Load the execution package's `base` + `python_executor` modules DIRECTLY from
# their files, WITHOUT importing `managers.__init__` (which pulls in the full
# agent stack: langchain_core, rich, …). This keeps the seam test dependency-light
# — only the factory's own deps (none for dispatch/errors; pandas/numpy only for
# the guarded round-trip).
#
# The stand-in packages are registered under a PRIVATE root (`_seam_mgr`), never
# under the real name `managers`. That matters beyond tidiness: pytest imports
# every test module during collection, so a stand-in left at `managers` (with an
# empty __path__) shadows the real package for every module collected afterwards
# — `from managers import ConsoleDisplay` then fails with "unknown location",
# which previously made test_upload_ui_gate.py and test_tool_registry.py
# uncollectable in a full-suite run while passing standalone. A private root
# cannot collide, so no cleanup is needed and collection order stops mattering.
#
# Everything under src/managers/execution imports RELATIVELY (`from .base import
# …`, `from ..namespace_kernel import …`), and a relative import resolves through
# the importing module's own __package__. So inside this alias tree every such
# import lands back in the tree, and the classes the tests hold are the same
# objects the factory returns — the module-identity property an alias preserves
# but deleting the entries after load would destroy.
_EXEC_DIR = Path(__file__).parent.parent / "src" / "managers" / "execution"
_ALIAS = "_seam_mgr"
def _load_execution_package():
for name, path in ((_ALIAS, []), (f"{_ALIAS}.execution", [str(_EXEC_DIR)])):
if name not in sys.modules:
pkg = types.ModuleType(name)
pkg.__path__ = path
sys.modules[name] = pkg
# The alias package's __path__ points at the real directory, so ordinary
# import machinery finds the submodules (and their relative-import deps,
# e.g. python_executor -> namespace_kernel) without hand-loading each file.
return (
importlib.import_module(f"{_ALIAS}.execution.base"),
importlib.import_module(f"{_ALIAS}.execution.python_executor"),
)
_base, _py = _load_execution_package()
Executor = _base.Executor
get_executor = _base.get_executor
PythonExecutor = _py.PythonExecutor
# --------------------------------------------------------------------------- #
# Interface surface + structural conformance
# --------------------------------------------------------------------------- #
def test_executor_protocol_surface():
"""The Executor protocol captures exactly the current PythonExecutor surface."""
for method in ("reset_environment", "send_functions", "send_variables", "execute", "__call__"):
assert hasattr(Executor, method), f"Executor missing {method}"
def test_python_executor_conforms_structurally():
"""PythonExecutor conforms to the runtime_checkable Executor protocol."""
assert issubclass(PythonExecutor, Executor)
def test_seam_is_reexported_from_init_files():
"""Executor / get_executor are wired into the package __init__ exports.
Asserted by reading the __init__ source (not importing it) so the check
stays free of the heavy `managers` import chain (langchain_core, rich, …).
"""
exec_init = (_EXEC_DIR / "__init__.py").read_text()
assert "get_executor" in exec_init and "Executor" in exec_init
managers_init = (_EXEC_DIR.parent / "__init__.py").read_text()
assert "get_executor" in managers_init and "Executor" in managers_init
# --------------------------------------------------------------------------- #
# Factory dispatch (no heavy deps needed for these three)
# --------------------------------------------------------------------------- #
def test_default_is_in_process(monkeypatch):
"""No EXECUTOR set → in_process → a PythonExecutor conforming to Executor."""
monkeypatch.delenv("EXECUTOR", raising=False)
ex = get_executor()
assert isinstance(ex, PythonExecutor)
assert isinstance(ex, Executor)
def test_explicit_in_process_kind():
"""Explicit kind='in_process' returns a PythonExecutor regardless of env."""
ex = get_executor("in_process")
assert isinstance(ex, PythonExecutor)
def test_env_selects_in_process(monkeypatch):
"""EXECUTOR=in_process (case/space-insensitive) selects the in-process executor."""
monkeypatch.setenv("EXECUTOR", " In_Process ")
assert isinstance(get_executor(), PythonExecutor)
def test_sandbox_selects_sandboxed_executor(monkeypatch):
"""EXECUTOR=sandbox now returns a SandboxedExecutor (ADR-0007 Phase 1 landed).
Was a NotImplementedError stub in Phase 0. With the subprocess launcher (so no
Docker is needed) the factory constructs the real client — verified by class
name so this seam test stays free of the sandbox package's own imports. No
kernel is started here (construction is lazy; start happens on first call).
"""
monkeypatch.setenv("EXECUTOR", "sandbox")
monkeypatch.setenv("SANDBOX_LAUNCHER", "subprocess")
ex = get_executor()
assert type(ex).__name__ == "SandboxedExecutor"
assert isinstance(ex, Executor) # conforms to the same protocol
# explicit-kind path too
ex2 = get_executor("sandbox")
assert type(ex2).__name__ == "SandboxedExecutor"
def test_unknown_value_raises_value_error(monkeypatch):
"""An unknown EXECUTOR value fails loud with ValueError."""
monkeypatch.setenv("EXECUTOR", "wasm")
with pytest.raises(ValueError) as exc:
get_executor()
assert "wasm" in str(exc.value)
# --------------------------------------------------------------------------- #
# Round-trip through the default executor (needs pandas/numpy)
# --------------------------------------------------------------------------- #
def test_in_process_round_trip():
"""The default executor executes code and returns captured stdout."""
pytest.importorskip("pandas")
pytest.importorskip("numpy")
ex = get_executor("in_process")
assert ex.execute("print(1 + 1)") == "2"
# __call__ is an alias for execute
assert ex("print('hi')") == "hi"
# empty output path
assert ex.execute("x = 5") == "Code executed successfully"
def test_in_process_namespace_persists():
"""State persists across calls (Jupyter-kernel semantics)."""
pytest.importorskip("pandas")
pytest.importorskip("numpy")
ex = get_executor("in_process")
ex.send_variables({"seed": 41})
assert ex.execute("print(seed + 1)") == "42"