mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
* fix: pass TUI Python env from dashboard chat * fix: share TUI Python env setup * fix: preserve TUI Python path semantics * chore: map contributor email for releases --------- Co-authored-by: AI on behalf of Álvaro Sánchez-Mariscal <alvaro.sanchez-mariscal@oracle.com>
This commit is contained in:
parent
9803b2fb89
commit
5122ddd478
5 changed files with 177 additions and 6 deletions
2
contributors/emails/alvaro.sanchez-mariscal@oracle.com
Normal file
2
contributors/emails/alvaro.sanchez-mariscal@oracle.com
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
alvarosanchez
|
||||
# PR #44797 salvage (dashboard TUI Python environment parity)
|
||||
|
|
@ -2003,6 +2003,39 @@ def _resolve_tui_heap_mb(default_mb: int = 8192) -> int:
|
|||
return max(1536, sized) if limit_mb > 2048 else sized
|
||||
|
||||
|
||||
def _safe_tui_cwd(env: Optional[dict] = None) -> str:
|
||||
"""Return a stable cwd value for the Node TUI child environment."""
|
||||
try:
|
||||
return os.getcwd()
|
||||
except FileNotFoundError:
|
||||
candidate = ((env or {}).get("PWD") or os.environ.get("PWD") or "").strip()
|
||||
if candidate and Path(candidate).is_dir():
|
||||
return candidate
|
||||
return str(PROJECT_ROOT)
|
||||
|
||||
|
||||
def _apply_tui_python_env(env: dict) -> None:
|
||||
"""Seed/repair Python-related env vars shared by CLI and dashboard TUI launches."""
|
||||
src_root = str(env.get("HERMES_PYTHON_SRC_ROOT") or "").strip()
|
||||
if not src_root or not Path(src_root).is_dir():
|
||||
env["HERMES_PYTHON_SRC_ROOT"] = str(PROJECT_ROOT)
|
||||
|
||||
cwd = str(env.get("HERMES_CWD") or "").strip()
|
||||
if not cwd or not Path(cwd).is_dir():
|
||||
env["HERMES_CWD"] = _safe_tui_cwd(env)
|
||||
|
||||
python = str(env.get("HERMES_PYTHON") or "").strip()
|
||||
if os.path.dirname(python):
|
||||
python_path = Path(python)
|
||||
if not python_path.is_absolute():
|
||||
python_path = Path(env["HERMES_CWD"]) / python_path
|
||||
python_is_executable = python_path.is_file() and os.access(python_path, os.X_OK)
|
||||
else:
|
||||
python_is_executable = bool(shutil.which(python, path=env.get("PATH")))
|
||||
if not python_is_executable:
|
||||
env["HERMES_PYTHON"] = sys.executable
|
||||
|
||||
|
||||
def _launch_tui(
|
||||
resume_session_id: Optional[str] = None,
|
||||
tui_dev: bool = False,
|
||||
|
|
@ -2036,11 +2069,6 @@ def _launch_tui(
|
|||
)
|
||||
os.close(active_session_fd)
|
||||
env["HERMES_TUI_ACTIVE_SESSION_FILE"] = active_session_file
|
||||
env["HERMES_PYTHON_SRC_ROOT"] = os.environ.get(
|
||||
"HERMES_PYTHON_SRC_ROOT", str(PROJECT_ROOT)
|
||||
)
|
||||
env.setdefault("HERMES_PYTHON", sys.executable)
|
||||
env.setdefault("HERMES_CWD", os.getcwd())
|
||||
env.setdefault("NODE_ENV", "development" if tui_dev else "production")
|
||||
|
||||
wt_info = None
|
||||
|
|
@ -2065,6 +2093,8 @@ def _launch_tui(
|
|||
env["HERMES_CWD"] = wt_info["path"]
|
||||
env["TERMINAL_CWD"] = wt_info["path"]
|
||||
|
||||
_apply_tui_python_env(env)
|
||||
|
||||
if model:
|
||||
env["HERMES_MODEL"] = model
|
||||
env["HERMES_INFERENCE_MODEL"] = model
|
||||
|
|
|
|||
|
|
@ -15346,7 +15346,7 @@ def _resolve_chat_argv(
|
|||
dashboard's in-memory gateway runs under the dashboard's own profile,
|
||||
so a profile-scoped chat must spawn its own gateway subprocess.
|
||||
"""
|
||||
from hermes_cli.main import PROJECT_ROOT, _make_tui_argv
|
||||
from hermes_cli.main import PROJECT_ROOT, _apply_tui_python_env, _make_tui_argv
|
||||
|
||||
profile_dir: Optional[Path] = None
|
||||
requested = (profile or "").strip()
|
||||
|
|
@ -15360,6 +15360,7 @@ def _resolve_chat_argv(
|
|||
apply_terminal_config_to_env(env=env)
|
||||
except Exception:
|
||||
_log.debug("Failed to apply terminal config bridge for dashboard chat", exc_info=True)
|
||||
_apply_tui_python_env(env)
|
||||
env.setdefault("NODE_ENV", "production")
|
||||
# Browser-embedded chat should prefer stable wheel-based scrollback over
|
||||
# native terminal mouse tracking. When mouse tracking is enabled, wheel
|
||||
|
|
|
|||
|
|
@ -924,6 +924,44 @@ def test_launch_tui_exports_model_provider_and_toolsets(monkeypatch, main_mod):
|
|||
assert env["NODE_ENV"] == "production"
|
||||
|
||||
|
||||
def test_launch_tui_worktree_validates_relative_python_against_final_cwd(
|
||||
monkeypatch, main_mod, tmp_path
|
||||
):
|
||||
import cli as cli_mod
|
||||
|
||||
parent_cwd = tmp_path / "parent"
|
||||
parent_cwd.mkdir()
|
||||
worktree = tmp_path / "worktree"
|
||||
relative_python = Path(".review-venv") / "bin" / Path(sys.executable).name
|
||||
python_path = worktree / relative_python
|
||||
python_path.parent.mkdir(parents=True)
|
||||
os.link(sys.executable, python_path)
|
||||
captured = {}
|
||||
|
||||
monkeypatch.setenv("HERMES_CWD", str(parent_cwd))
|
||||
monkeypatch.setenv("HERMES_PYTHON", str(relative_python))
|
||||
monkeypatch.setattr(cli_mod, "_git_repo_root", lambda: None)
|
||||
monkeypatch.setattr(cli_mod, "_prune_stale_worktrees", lambda _repo: None)
|
||||
monkeypatch.setattr(cli_mod, "_setup_worktree", lambda: {"path": str(worktree)})
|
||||
monkeypatch.setattr(cli_mod, "_cleanup_worktree", lambda _info: None)
|
||||
monkeypatch.setattr(
|
||||
main_mod,
|
||||
"_make_tui_argv",
|
||||
lambda tui_dir, tui_dev: (["node", "dist/entry.js"], Path(".")),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
main_mod.subprocess,
|
||||
"call",
|
||||
lambda argv, cwd=None, env=None: captured.update({"env": env}) or 1,
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main_mod._launch_tui(worktree=True)
|
||||
|
||||
assert captured["env"]["HERMES_CWD"] == str(worktree)
|
||||
assert captured["env"]["HERMES_PYTHON"] == str(relative_python)
|
||||
|
||||
|
||||
def test_launch_tui_applies_terminal_backend_config(
|
||||
monkeypatch, main_mod, _isolate_hermes_home
|
||||
):
|
||||
|
|
|
|||
|
|
@ -7104,6 +7104,106 @@ class TestPtyWebSocket:
|
|||
|
||||
assert env["COLORTERM"] == "24bit"
|
||||
|
||||
def test_resolve_chat_argv_sets_tui_python_environment(self, monkeypatch):
|
||||
"""Dashboard chat gives the Node TUI the same Python env as CLI launches."""
|
||||
import hermes_cli.main as main_mod
|
||||
|
||||
monkeypatch.delenv("HERMES_PYTHON_SRC_ROOT", raising=False)
|
||||
monkeypatch.delenv("HERMES_PYTHON", raising=False)
|
||||
monkeypatch.delenv("HERMES_CWD", raising=False)
|
||||
monkeypatch.setattr(
|
||||
main_mod,
|
||||
"_make_tui_argv",
|
||||
lambda project_root, tui_dev=False: (["node", "dist/entry.js"], "/tmp/ui-tui"),
|
||||
)
|
||||
|
||||
_argv, _cwd, env = self.ws_module._resolve_chat_argv()
|
||||
|
||||
assert env is not None
|
||||
assert env["HERMES_PYTHON_SRC_ROOT"] == str(main_mod.PROJECT_ROOT)
|
||||
assert env["HERMES_PYTHON"] == sys.executable
|
||||
assert env["HERMES_CWD"] == os.getcwd()
|
||||
|
||||
def test_resolve_chat_argv_replaces_invalid_tui_python_environment(self, monkeypatch):
|
||||
"""Dashboard chat does not preserve unusable inherited TUI Python env."""
|
||||
import hermes_cli.main as main_mod
|
||||
|
||||
monkeypatch.setenv("HERMES_PYTHON_SRC_ROOT", "/definitely/missing/hermes-src")
|
||||
monkeypatch.setenv("HERMES_PYTHON", "/definitely/missing/python")
|
||||
monkeypatch.setenv("HERMES_CWD", "/definitely/missing/cwd")
|
||||
monkeypatch.setattr(
|
||||
main_mod,
|
||||
"_make_tui_argv",
|
||||
lambda project_root, tui_dev=False: (["node", "dist/entry.js"], "/tmp/ui-tui"),
|
||||
)
|
||||
|
||||
_argv, _cwd, env = self.ws_module._resolve_chat_argv()
|
||||
|
||||
assert env is not None
|
||||
assert env["HERMES_PYTHON_SRC_ROOT"] == str(main_mod.PROJECT_ROOT)
|
||||
assert env["HERMES_PYTHON"] == sys.executable
|
||||
assert env["HERMES_CWD"] == os.getcwd()
|
||||
|
||||
def test_resolve_chat_argv_keeps_relative_python_under_tui_cwd(
|
||||
self, monkeypatch, tmp_path
|
||||
):
|
||||
"""Relative Python paths are resolved from the TUI child's cwd."""
|
||||
import hermes_cli.main as main_mod
|
||||
|
||||
relative_python = Path(".review-venv") / "bin" / Path(sys.executable).name
|
||||
python_path = tmp_path / relative_python
|
||||
python_path.parent.mkdir(parents=True)
|
||||
os.link(sys.executable, python_path)
|
||||
monkeypatch.setenv("HERMES_CWD", str(tmp_path))
|
||||
monkeypatch.setenv("HERMES_PYTHON", str(relative_python))
|
||||
monkeypatch.setattr(
|
||||
main_mod,
|
||||
"_make_tui_argv",
|
||||
lambda project_root, tui_dev=False: (["node", "dist/entry.js"], "/tmp/ui-tui"),
|
||||
)
|
||||
|
||||
_argv, _cwd, env = self.ws_module._resolve_chat_argv()
|
||||
|
||||
assert env is not None
|
||||
assert env["HERMES_PYTHON"] == str(relative_python)
|
||||
|
||||
def test_tui_python_command_uses_child_path(self, tmp_path):
|
||||
"""Bare Python commands are resolved from the TUI child's PATH."""
|
||||
import hermes_cli.main as main_mod
|
||||
|
||||
command = f"hermes-review-python{Path(sys.executable).suffix}"
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
executable = bin_dir / command
|
||||
os.link(sys.executable, executable)
|
||||
env = {
|
||||
"HERMES_CWD": str(tmp_path),
|
||||
"HERMES_PYTHON": command,
|
||||
"PATH": str(bin_dir),
|
||||
}
|
||||
|
||||
main_mod._apply_tui_python_env(env)
|
||||
|
||||
assert env["HERMES_PYTHON"] == command
|
||||
|
||||
def test_resolve_chat_argv_falls_back_when_getcwd_is_missing(self, monkeypatch, tmp_path):
|
||||
"""Dashboard chat still starts if the service cwd was deleted."""
|
||||
import hermes_cli.main as main_mod
|
||||
|
||||
monkeypatch.delenv("HERMES_CWD", raising=False)
|
||||
monkeypatch.setenv("PWD", str(tmp_path))
|
||||
monkeypatch.setattr(main_mod.os, "getcwd", lambda: (_ for _ in ()).throw(FileNotFoundError()))
|
||||
monkeypatch.setattr(
|
||||
main_mod,
|
||||
"_make_tui_argv",
|
||||
lambda project_root, tui_dev=False: (["node", "dist/entry.js"], "/tmp/ui-tui"),
|
||||
)
|
||||
|
||||
_argv, _cwd, env = self.ws_module._resolve_chat_argv()
|
||||
|
||||
assert env is not None
|
||||
assert env["HERMES_CWD"] == str(tmp_path)
|
||||
|
||||
def test_resolve_chat_argv_applies_terminal_backend_config(
|
||||
self, monkeypatch, _isolate_hermes_home
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue