diff --git a/hermes_cli/profiles.py b/hermes_cli/profiles.py index 490077884e5f..aa9c88c35f5b 100644 --- a/hermes_cli/profiles.py +++ b/hermes_cli/profiles.py @@ -615,10 +615,41 @@ def _check_gateway_running(profile_dir: Path) -> bool: """Check if a gateway is running for a given profile directory.""" try: from gateway.status import get_running_pid - return get_running_pid(profile_dir / "gateway.pid", cleanup_stale=False) is not None + + if get_running_pid(profile_dir / "gateway.pid", cleanup_stale=False) is not None: + return True + except Exception: + pass + return _gateway_runtime_snapshot_running_for_profile(profile_dir) + + +def _gateway_runtime_snapshot_running_for_profile(profile_dir: Path) -> bool: + """Return supervisor/process liveness for ``profile_dir``. + + ``profile list`` historically trusted only ``/gateway.pid``. On + supervised installs (launchd/systemd/s6), the service can be alive even when + that compatibility PID file is absent; ``hermes status --all`` already uses + the richer runtime snapshot. Reuse that path under a context-local + HERMES_HOME override so every profile is checked against its own service + label/process filter without mutating global environment state. + """ + try: + from hermes_constants import ( + reset_hermes_home_override, + set_hermes_home_override, + ) + from hermes_cli.gateway import get_gateway_runtime_snapshot except Exception: return False + token = set_hermes_home_override(profile_dir) + try: + return bool(get_gateway_runtime_snapshot().running) + except Exception: + return False + finally: + reset_hermes_home_override(token) + def _count_skills(profile_dir: Path) -> int: """Count installed skills in a profile.""" diff --git a/tests/hermes_cli/test_profiles.py b/tests/hermes_cli/test_profiles.py index 59afe84e5633..33d311a07653 100644 --- a/tests/hermes_cli/test_profiles.py +++ b/tests/hermes_cli/test_profiles.py @@ -1414,7 +1414,11 @@ def test_create_profile_returns_correct_path(self, profile_env): assert result == expected def test_list_profiles_default_info_fields(self, profile_env): - profiles = list_profiles() + with patch( + "hermes_cli.profiles._gateway_runtime_snapshot_running_for_profile", + return_value=False, + ): + profiles = list_profiles() default = [p for p in profiles if p.name == "default"][0] assert default.is_default is True assert default.gateway_running is False @@ -1426,25 +1430,50 @@ def test_gateway_running_check_with_pid_file(self, profile_env): tmp_path = profile_env default_home = tmp_path / ".hermes" - with patch("gateway.status.get_running_pid", return_value=99999) as mock_get_running_pid: + with patch("gateway.status.get_running_pid", return_value=99999) as mock_get_running_pid, patch( + "hermes_cli.profiles._gateway_runtime_snapshot_running_for_profile", + return_value=False, + ) as mock_snapshot: + assert _check_gateway_running(default_home) is True + mock_get_running_pid.assert_called_once_with( + default_home / "gateway.pid", + cleanup_stale=False, + ) + mock_snapshot.assert_not_called() + + def test_gateway_running_check_falls_back_to_runtime_snapshot(self, profile_env): + """No PID file can still be running when launchd/systemd supervises it.""" + from hermes_cli.profiles import _check_gateway_running + tmp_path = profile_env + default_home = tmp_path / ".hermes" + + with patch("gateway.status.get_running_pid", return_value=None) as mock_get_running_pid, patch( + "hermes_cli.profiles._gateway_runtime_snapshot_running_for_profile", + return_value=True, + ) as mock_snapshot: assert _check_gateway_running(default_home) is True mock_get_running_pid.assert_called_once_with( default_home / "gateway.pid", cleanup_stale=False, ) + mock_snapshot.assert_called_once_with(default_home) def test_gateway_running_check_plain_pid(self, profile_env): - """Shared PID validator returning None means the profile is not running.""" + """No PID file and no runtime snapshot means the profile is not running.""" from hermes_cli.profiles import _check_gateway_running tmp_path = profile_env default_home = tmp_path / ".hermes" - with patch("gateway.status.get_running_pid", return_value=None) as mock_get_running_pid: + with patch("gateway.status.get_running_pid", return_value=None) as mock_get_running_pid, patch( + "hermes_cli.profiles._gateway_runtime_snapshot_running_for_profile", + return_value=False, + ) as mock_snapshot: assert _check_gateway_running(default_home) is False mock_get_running_pid.assert_called_once_with( default_home / "gateway.pid", cleanup_stale=False, ) + mock_snapshot.assert_called_once_with(default_home) def test_profile_name_boundary_single_char(self): """Single alphanumeric character is valid."""