diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 1348e3155..c99761d5c 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -1121,7 +1121,7 @@ def launchd_start(): try: subprocess.run(["launchctl", "kickstart", f"{_launchd_domain()}/{label}"], check=True, timeout=30) except subprocess.CalledProcessError as e: - if e.returncode != 3: + if e.returncode not in (3, 113): raise print("↻ launchd job was unloaded; reloading service definition") subprocess.run(["launchctl", "bootstrap", _launchd_domain(), str(plist_path)], check=True, timeout=30) @@ -1183,7 +1183,7 @@ def launchd_restart(): subprocess.run(["launchctl", "kickstart", "-k", target], check=True, timeout=90) print("✓ Service restarted") except subprocess.CalledProcessError as e: - if e.returncode != 3: + if e.returncode not in (3, 113): raise # Job not loaded — bootstrap and start fresh print("↻ launchd job was unloaded; reloading") diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index b08fb46c3..03c9c56ec 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -205,6 +205,33 @@ class TestLaunchdServiceRecovery: ["launchctl", "kickstart", target], ] + def test_launchd_start_reloads_on_kickstart_exit_code_113(self, tmp_path, monkeypatch): + """Exit code 113 (\"Could not find service\") should also trigger bootstrap recovery.""" + plist_path = tmp_path / "ai.hermes.gateway.plist" + plist_path.write_text(gateway_cli.generate_launchd_plist(), encoding="utf-8") + label = gateway_cli.get_launchd_label() + + calls = [] + domain = gateway_cli._launchd_domain() + target = f"{domain}/{label}" + + def fake_run(cmd, check=False, **kwargs): + calls.append(cmd) + if cmd == ["launchctl", "kickstart", target] and calls.count(cmd) == 1: + raise gateway_cli.subprocess.CalledProcessError(113, cmd, stderr="Could not find service") + return SimpleNamespace(returncode=0, stdout="", stderr="") + + monkeypatch.setattr(gateway_cli, "get_launchd_plist_path", lambda: plist_path) + monkeypatch.setattr(gateway_cli.subprocess, "run", fake_run) + + gateway_cli.launchd_start() + + assert calls == [ + ["launchctl", "kickstart", target], + ["launchctl", "bootstrap", domain, str(plist_path)], + ["launchctl", "kickstart", target], + ] + def test_launchd_status_reports_local_stale_plist_when_unloaded(self, tmp_path, monkeypatch, capsys): plist_path = tmp_path / "ai.hermes.gateway.plist" plist_path.write_text("old content", encoding="utf-8")