From 49770949daa7ca4ef4744a2f089100f8b872b869 Mon Sep 17 00:00:00 2001 From: dinos Date: Sat, 11 Jul 2026 13:59:27 +0200 Subject: [PATCH] fix(langgraph): prefer executable in venv over path (#341) --- EvoScientist/langgraph_dev/manager.py | 16 ++++-- tests/test_langgraph_manager.py | 72 +++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/EvoScientist/langgraph_dev/manager.py b/EvoScientist/langgraph_dev/manager.py index b7539a4..69e5068 100644 --- a/EvoScientist/langgraph_dev/manager.py +++ b/EvoScientist/langgraph_dev/manager.py @@ -306,14 +306,19 @@ def is_async_subagents_available() -> bool: def _langgraph_exe() -> str | None: """Return the path to the langgraph CLI binary, or None if not found.""" + import sys + + executable_dir = os.path.dirname(sys.executable) + candidate_names = ( + ["langgraph.exe", "langgraph"] if os.name == "nt" else ["langgraph"] + ) + for candidate_name in candidate_names: + candidate = os.path.join(executable_dir, candidate_name) + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate found = shutil.which("langgraph") if found: return found - import sys as _sys - - candidate = os.path.join(os.path.dirname(_sys.executable), "langgraph") - if os.path.isfile(candidate) and os.access(candidate, os.X_OK): - return candidate return None @@ -709,6 +714,7 @@ def start_langgraph_dev( sub_env["EVOSCIENTIST_DEPLOY_MODE"] = "full" if deploy_mode else "stripped" try: + logger.info("Starting langgraph dev with CLI: %s", exe) proc = subprocess.Popen( [ exe, diff --git a/tests/test_langgraph_manager.py b/tests/test_langgraph_manager.py index 845064e..353013c 100644 --- a/tests/test_langgraph_manager.py +++ b/tests/test_langgraph_manager.py @@ -8,6 +8,7 @@ to be available. from __future__ import annotations import dataclasses +import sys from types import SimpleNamespace from unittest.mock import MagicMock, patch @@ -32,6 +33,77 @@ def reset_module_state(): manager._LOG_OFFSET_AT_START = 0 +# ============================================================================= +# langgraph CLI resolution +# ============================================================================= + + +class TestLanggraphCliResolution: + def _make_executable(self, path): + path.write_text("#!/bin/sh\n", encoding="utf-8") + path.chmod(0o755) + + def test_prefers_current_python_environment_over_path(self, tmp_path, monkeypatch): + local_bin = tmp_path / "local" / "bin" + local_bin.mkdir(parents=True) + local_langgraph = local_bin / "langgraph" + self._make_executable(local_langgraph) + + path_bin = tmp_path / "path" / "bin" + path_bin.mkdir(parents=True) + path_langgraph = path_bin / "langgraph" + self._make_executable(path_langgraph) + + monkeypatch.setattr(sys, "executable", str(local_bin / "python")) + monkeypatch.setattr( + manager.shutil, + "which", + lambda command: str(path_langgraph) if command == "langgraph" else None, + ) + + assert manager._langgraph_exe() == str(local_langgraph) + + def test_falls_back_to_path_when_environment_binary_missing( + self, tmp_path, monkeypatch + ): + path_bin = tmp_path / "path" / "bin" + path_bin.mkdir(parents=True) + path_langgraph = path_bin / "langgraph" + self._make_executable(path_langgraph) + + monkeypatch.setattr( + sys, "executable", str(tmp_path / "local" / "bin" / "python") + ) + monkeypatch.setattr( + manager.shutil, + "which", + lambda command: str(path_langgraph) if command == "langgraph" else None, + ) + + assert manager._langgraph_exe() == str(path_langgraph) + + def test_checks_windows_suffix_next_to_current_python(self, tmp_path, monkeypatch): + scripts_dir = tmp_path / "Scripts" + scripts_dir.mkdir() + local_langgraph = scripts_dir / "langgraph.exe" + self._make_executable(local_langgraph) + + path_bin = tmp_path / "path" / "bin" + path_bin.mkdir(parents=True) + path_langgraph = path_bin / "langgraph.exe" + self._make_executable(path_langgraph) + + monkeypatch.setattr(sys, "executable", str(scripts_dir / "python.exe")) + monkeypatch.setattr(manager.os, "name", "nt", raising=False) + monkeypatch.setattr( + manager.shutil, + "which", + lambda command: str(path_langgraph) if command == "langgraph" else None, + ) + + assert manager._langgraph_exe() == str(local_langgraph) + + # ============================================================================= # is_langgraph_dev_running # =============================================================================