fix(gateway): anchor service WorkingDirectory at HERMES_HOME, not the source checkout
The systemd unit (and launchd plist) pinned WorkingDirectory to PROJECT_ROOT (the checkout the unit was generated from). When that checkout is transient — a git worktree, or a clone hermes update later relocates/removes — the path rots. systemd then fails the start at the CHDIR step (status=200/CHDIR) BEFORE Python loads, so the on-boot refresh_systemd_unit_if_needed() self-heal never runs and Restart=always crash-loops forever on a dead directory. Observed in the wild: a gateway that crash-looped 153 times overnight, bot offline until a manual 'hermes gateway restart' regenerated the unit. Anchor cwd at HERMES_HOME instead — it never moves, always exists, and the gateway never needed cwd to be the checkout (ExecStart uses an absolute python + -m hermes_cli.main). Existing broken units now differ from the generated unit and self-heal on the next start/restart/update.
This commit is contained in:
@@ -2531,3 +2531,46 @@ class TestGatewayCommandCatchesSystemScopeError:
|
||||
# Renders the message, NOT the ``('msg', 'action')`` tuple repr
|
||||
assert "System gateway start requires root. Re-run with sudo." in out
|
||||
assert "('" not in out # no tuple repr leaking through
|
||||
|
||||
|
||||
class TestServiceWorkingDirIsStable:
|
||||
"""The gateway service must anchor WorkingDirectory at a stable path
|
||||
(HERMES_HOME), never the source checkout / worktree, so a relocated or
|
||||
deleted checkout can't crash-loop the unit on CHDIR (status=200).
|
||||
"""
|
||||
|
||||
def test_stable_working_dir_uses_hermes_home(self, tmp_path, monkeypatch):
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: home)
|
||||
assert Path(gateway_cli._stable_service_working_dir()) == home.resolve()
|
||||
|
||||
def test_stable_working_dir_falls_back_to_project_root(self, tmp_path, monkeypatch):
|
||||
# HERMES_HOME points somewhere that does not exist -> fall back.
|
||||
missing = tmp_path / "does-not-exist" / ".hermes"
|
||||
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: missing)
|
||||
assert gateway_cli._stable_service_working_dir() == str(gateway_cli.PROJECT_ROOT)
|
||||
|
||||
def test_user_unit_workingdirectory_is_hermes_home_not_checkout(self, tmp_path, monkeypatch):
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: home)
|
||||
unit = gateway_cli.generate_systemd_unit(system=False)
|
||||
wd = [l for l in unit.splitlines() if l.startswith("WorkingDirectory=")]
|
||||
assert wd, "unit has no WorkingDirectory line"
|
||||
value = wd[0].split("=", 1)[1]
|
||||
assert Path(value).resolve() == home.resolve()
|
||||
# The bug class: never pin cwd inside a transient worktree checkout.
|
||||
assert "/.worktrees/" not in value
|
||||
|
||||
def test_launchd_workingdirectory_is_hermes_home(self, tmp_path, monkeypatch):
|
||||
import re
|
||||
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setattr(gateway_cli, "get_hermes_home", lambda: home)
|
||||
plist = gateway_cli.generate_launchd_plist()
|
||||
m = re.search(r"<key>WorkingDirectory</key>\s*<string>(.*?)</string>", plist)
|
||||
assert m, "plist has no WorkingDirectory entry"
|
||||
assert Path(m.group(1)).resolve() == home.resolve()
|
||||
assert "/.worktrees/" not in m.group(1)
|
||||
|
||||
Reference in New Issue
Block a user