fix(gateway-windows): anchor detached/startup cwd at HERMES_HOME

This commit is contained in:
Dusk1e
2026-05-30 02:20:32 +03:00
committed by Teknium
parent 40fbb0f3c6
commit 2059707fce
2 changed files with 71 additions and 7 deletions

View File

@ -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

View File

@ -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
assert gateway_windows._drain_gateway_pid(pid, drain_timeout=5.0) is True