diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 7dec83cbff..dff0a4aa75 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -1971,6 +1971,15 @@ def systemd_uninstall(system: bool = False): print(f"✓ {_service_scope_label(system).capitalize()} service uninstalled") +def _require_service_installed(action: str, system: bool = False) -> None: + unit_path = get_systemd_unit_path(system=system) + if not unit_path.exists(): + scope_flag = " --system" if system else "" + print(f"✗ Gateway service is not installed") + print(f" Run: {'sudo ' if system else ''}hermes gateway install{scope_flag}") + sys.exit(1) + + def systemd_start(system: bool = False): system = _select_systemd_scope(system) if system: @@ -1980,6 +1989,7 @@ def systemd_start(system: bool = False): # reachable (common on fresh RHEL/Debian SSH sessions without linger). # Raises UserSystemdUnavailableError with a remediation message. _preflight_user_systemd() + _require_service_installed("start", system=system) refresh_systemd_unit_if_needed(system=system) _run_systemctl(["start", get_service_name()], system=system, check=True, timeout=30) print(f"✓ {_service_scope_label(system).capitalize()} service started") @@ -1990,6 +2000,7 @@ def systemd_stop(system: bool = False): system = _select_systemd_scope(system) if system: _require_root_for_system_service("stop") + _require_service_installed("stop", system=system) _run_systemctl(["stop", get_service_name()], system=system, check=True, timeout=90) print(f"✓ {_service_scope_label(system).capitalize()} service stopped") @@ -2001,6 +2012,7 @@ def systemd_restart(system: bool = False): _require_root_for_system_service("restart") else: _preflight_user_systemd() + _require_service_installed("restart", system=system) refresh_systemd_unit_if_needed(system=system) from gateway.status import get_running_pid diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index a2e3869c8c..210c9c144e 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -141,6 +141,27 @@ class TestSystemdServiceRefresh: assert ["systemctl", "--user", "daemon-reload"] in calls +class TestRequireServiceInstalled: + def test_exits_with_install_hint_when_unit_missing(self, tmp_path, monkeypatch, capsys): + unit_path = tmp_path / "hermes-gateway.service" + monkeypatch.setattr(gateway_cli, "get_systemd_unit_path", lambda system=False: unit_path) + + with pytest.raises(SystemExit) as exc_info: + gateway_cli._require_service_installed("start") + + assert exc_info.value.code == 1 + out = capsys.readouterr().out + assert "not installed" in out + assert "hermes gateway install" in out + + def test_passes_when_unit_exists(self, tmp_path, monkeypatch): + unit_path = tmp_path / "hermes-gateway.service" + unit_path.write_text("[Unit]\n", encoding="utf-8") + monkeypatch.setattr(gateway_cli, "get_systemd_unit_path", lambda system=False: unit_path) + + gateway_cli._require_service_installed("start") + + class TestGeneratedSystemdUnits: def test_user_unit_avoids_recursive_execstop_and_uses_extended_stop_timeout(self): unit = gateway_cli.generate_systemd_unit(system=False) @@ -521,6 +542,7 @@ class TestGatewaySystemServiceRouting: calls = [] monkeypatch.setattr(gateway_cli, "_select_systemd_scope", lambda system=False: False) + monkeypatch.setattr(gateway_cli, "_require_service_installed", lambda action, system=False: None) monkeypatch.setattr(gateway_cli, "refresh_systemd_unit_if_needed", lambda system=False: calls.append(("refresh", system))) monkeypatch.setattr( "gateway.status.get_running_pid", @@ -575,6 +597,7 @@ class TestGatewaySystemServiceRouting: def test_systemd_restart_recovers_failed_planned_restart(self, monkeypatch, capsys): monkeypatch.setattr(gateway_cli, "_select_systemd_scope", lambda system=False: False) + monkeypatch.setattr(gateway_cli, "_require_service_installed", lambda action, system=False: None) monkeypatch.setattr(gateway_cli, "refresh_systemd_unit_if_needed", lambda system=False: None) monkeypatch.setattr( "gateway.status.read_runtime_status",