From 2059707fce0bfa6720af09cc0223429d6e6f87cd Mon Sep 17 00:00:00 2001 From: Dusk1e Date: Sat, 30 May 2026 02:20:32 +0300 Subject: [PATCH] fix(gateway-windows): anchor detached/startup cwd at HERMES_HOME --- hermes_cli/gateway_windows.py | 32 ++++++++++++++--- tests/hermes_cli/test_gateway_windows.py | 46 ++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/hermes_cli/gateway_windows.py b/hermes_cli/gateway_windows.py index c17469018..64b5fd6dc 100644 --- a/hermes_cli/gateway_windows.py +++ b/hermes_cli/gateway_windows.py @@ -308,6 +308,29 @@ def get_startup_entry_path() -> Path: return _startup_dir() / f"{_sanitize_filename(get_task_name())}.cmd" +# --------------------------------------------------------------------------- +# Stable working directory +# --------------------------------------------------------------------------- + +def _stable_gateway_working_dir(project_root: Path) -> str: + """Return a stable cwd for detached/startup gateway runs. + + Mirror the POSIX service invariant: anchor at ``HERMES_HOME`` whenever it + exists so Scheduled Task / Startup launches do not fail at the ``cd`` step + after a transient checkout or worktree is moved away. Fall back to the + source checkout only if ``HERMES_HOME`` cannot be resolved yet. + """ + from hermes_cli.config import get_hermes_home + + try: + home = get_hermes_home() + if home and Path(home).is_dir(): + return str(Path(home).resolve()) + except Exception: + pass + return str(project_root) + + # --------------------------------------------------------------------------- # Script rendering # --------------------------------------------------------------------------- @@ -321,7 +344,7 @@ def _build_gateway_cmd_script( """Build the ``gateway.cmd`` wrapper content (CRLF-terminated). The script: - - cd's into the project directory + - cd's into a stable working directory - exports HERMES_HOME, PYTHONIOENCODING, VIRTUAL_ENV - invokes ``pythonw -m hermes_cli.main [--profile X] gateway run`` directly so the wrapper cmd.exe exits without a visible gateway console @@ -380,7 +403,7 @@ def _write_task_script() -> Path: ) python_path = get_python_path() - working_dir = str(PROJECT_ROOT) + working_dir = _stable_gateway_working_dir(PROJECT_ROOT) hermes_home = str(Path(get_hermes_home()).resolve()) profile_arg = _profile_arg(hermes_home) @@ -547,7 +570,8 @@ def _build_gateway_argv() -> tuple[list[str], str, dict[str, str]]: ) python_exe, venv_dir, extra_pythonpath = _resolve_detached_python(get_python_path()) - working_dir = str(PROJECT_ROOT) + project_root = str(PROJECT_ROOT) + working_dir = _stable_gateway_working_dir(PROJECT_ROOT) hermes_home = str(Path(get_hermes_home()).resolve()) profile_arg = _profile_arg(hermes_home) @@ -562,7 +586,7 @@ def _build_gateway_argv() -> tuple[list[str], str, dict[str, str]]: "HERMES_GATEWAY_DETACHED": "1", "VIRTUAL_ENV": str(venv_dir), } - _prepend_pythonpath(env_overlay, [working_dir, *extra_pythonpath] if extra_pythonpath else []) + _prepend_pythonpath(env_overlay, [project_root, *extra_pythonpath] if extra_pythonpath else [project_root]) return argv, working_dir, env_overlay diff --git a/tests/hermes_cli/test_gateway_windows.py b/tests/hermes_cli/test_gateway_windows.py index ba2a7d4f4..43f2b01db 100644 --- a/tests/hermes_cli/test_gateway_windows.py +++ b/tests/hermes_cli/test_gateway_windows.py @@ -78,9 +78,11 @@ def test_build_gateway_argv_uses_base_pythonw_for_uv_venv_launcher(monkeypatch, project = tmp_path / "project" scripts = project / "venv" / "Scripts" site_packages = project / "venv" / "Lib" / "site-packages" + hermes_home = tmp_path / "hermes-home" base = tmp_path / "uv" / "python" / "cpython-3.11-windows-x86_64-none" scripts.mkdir(parents=True) site_packages.mkdir(parents=True) + hermes_home.mkdir() base.mkdir(parents=True) venv_python = scripts / "python.exe" @@ -99,17 +101,55 @@ def test_build_gateway_argv_uses_base_pythonw_for_uv_venv_launcher(monkeypatch, monkeypatch.setattr(gateway, "PROJECT_ROOT", project) monkeypatch.setattr(gateway, "get_python_path", lambda: str(venv_python)) monkeypatch.setattr(gateway, "_profile_arg", lambda hermes_home: "") - monkeypatch.setattr("hermes_cli.config.get_hermes_home", lambda: str(tmp_path / "hermes-home")) + monkeypatch.setattr("hermes_cli.config.get_hermes_home", lambda: str(hermes_home)) argv, cwd, env_overlay = gateway_windows._build_gateway_argv() assert argv[:3] == [str(base_pythonw), "-m", "hermes_cli.main"] - assert cwd == str(project) + assert cwd == str(hermes_home.resolve()) assert env_overlay["VIRTUAL_ENV"] == str(project / "venv") assert str(project) in env_overlay["PYTHONPATH"].split(gateway_windows.os.pathsep) assert str(site_packages) in env_overlay["PYTHONPATH"].split(gateway_windows.os.pathsep) +class TestStableWindowsGatewayWorkingDir: + def test_stable_gateway_working_dir_uses_hermes_home(self, tmp_path, monkeypatch): + home = tmp_path / ".hermes" + home.mkdir() + monkeypatch.setattr("hermes_cli.config.get_hermes_home", lambda: home) + assert gateway_windows._stable_gateway_working_dir(tmp_path / "checkout") == str(home.resolve()) + + def test_stable_gateway_working_dir_falls_back_to_project_root(self, tmp_path, monkeypatch): + missing = tmp_path / "missing" / ".hermes" + project = tmp_path / "checkout" + monkeypatch.setattr("hermes_cli.config.get_hermes_home", lambda: missing) + assert gateway_windows._stable_gateway_working_dir(project) == str(project) + + +def test_write_task_script_anchors_cmd_cd_at_hermes_home(monkeypatch, tmp_path): + project = tmp_path / "project" + hermes_home = tmp_path / "hermes-home" + hermes_home.mkdir() + python_exe = project / "venv" / "Scripts" / "python.exe" + python_exe.parent.mkdir(parents=True) + python_exe.write_text("", encoding="utf-8") + script_path = tmp_path / "gateway.cmd" + + monkeypatch.setattr(gateway_windows, "_assert_windows", lambda: None) + monkeypatch.setattr(gateway, "PROJECT_ROOT", project) + monkeypatch.setattr(gateway, "get_python_path", lambda: str(python_exe)) + monkeypatch.setattr(gateway, "_profile_arg", lambda hermes_home: "") + monkeypatch.setattr("hermes_cli.config.get_hermes_home", lambda: str(hermes_home)) + monkeypatch.setattr(gateway_windows, "get_task_script_path", lambda: script_path) + + written = gateway_windows._write_task_script() + content = script_path.read_text(encoding="utf-8") + + assert written == script_path + assert f"cd /d {gateway_windows._quote_cmd_script_arg(str(hermes_home.resolve()))}" in content + assert f"cd /d {gateway_windows._quote_cmd_script_arg(str(project))}" not in content + + def _arrange_startup_fallback(monkeypatch, tmp_path, running_pids): script_path = tmp_path / "Hermes_Gateway_alice.cmd" startup_entry = tmp_path / "Startup" / "Hermes_Gateway_alice.cmd" @@ -741,4 +781,4 @@ def test_drain_helper_still_waits_if_marker_write_fails(monkeypatch): monkeypatch.setattr(status_mod, "_pid_exists", lambda check_pid: False) # Returns True because _pid_exists immediately says "gone". - assert gateway_windows._drain_gateway_pid(pid, drain_timeout=5.0) is True \ No newline at end of file + assert gateway_windows._drain_gateway_pid(pid, drain_timeout=5.0) is True