/ tests / hermes_cli / test_gateway_linger.py
test_gateway_linger.py
  1  """Tests for gateway linger auto-enable behavior on headless Linux installs."""
  2  
  3  from types import SimpleNamespace
  4  
  5  import hermes_cli.gateway as gateway
  6  
  7  
  8  class TestEnsureLingerEnabled:
  9      def test_linger_already_enabled_via_file(self, monkeypatch, capsys):
 10          monkeypatch.setattr(gateway, "is_linux", lambda: True)
 11          monkeypatch.setattr(gateway, "is_termux", lambda: False)
 12          monkeypatch.setattr("getpass.getuser", lambda: "testuser")
 13          monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: True))
 14  
 15          calls = []
 16          monkeypatch.setattr(gateway.subprocess, "run", lambda *args, **kwargs: calls.append((args, kwargs)))
 17  
 18          gateway._ensure_linger_enabled()
 19  
 20          out = capsys.readouterr().out
 21          assert "Systemd linger is enabled" in out
 22          assert calls == []
 23  
 24      def test_status_enabled_skips_enable(self, monkeypatch, capsys):
 25          monkeypatch.setattr(gateway, "is_linux", lambda: True)
 26          monkeypatch.setattr(gateway, "is_termux", lambda: False)
 27          monkeypatch.setattr("getpass.getuser", lambda: "testuser")
 28          monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
 29          monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (True, ""))
 30  
 31          calls = []
 32          monkeypatch.setattr(gateway.subprocess, "run", lambda *args, **kwargs: calls.append((args, kwargs)))
 33  
 34          gateway._ensure_linger_enabled()
 35  
 36          out = capsys.readouterr().out
 37          assert "Systemd linger is enabled" in out
 38          assert calls == []
 39  
 40      def test_loginctl_success_enables_linger(self, monkeypatch, capsys):
 41          monkeypatch.setattr(gateway, "is_linux", lambda: True)
 42          monkeypatch.setattr(gateway, "is_termux", lambda: False)
 43          monkeypatch.setattr("getpass.getuser", lambda: "testuser")
 44          monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
 45          monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (False, ""))
 46          monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/loginctl")
 47  
 48          run_calls = []
 49  
 50          def fake_run(cmd, capture_output=False, text=False, check=False, **kwargs):
 51              run_calls.append((cmd, capture_output, text, check))
 52              return SimpleNamespace(returncode=0, stdout="", stderr="")
 53  
 54          monkeypatch.setattr(gateway.subprocess, "run", fake_run)
 55  
 56          gateway._ensure_linger_enabled()
 57  
 58          out = capsys.readouterr().out
 59          assert "Enabling linger" in out
 60          assert "Linger enabled" in out
 61          assert run_calls == [(["loginctl", "enable-linger", "testuser"], True, True, False)]
 62  
 63      def test_missing_loginctl_shows_manual_guidance(self, monkeypatch, capsys):
 64          monkeypatch.setattr(gateway, "is_linux", lambda: True)
 65          monkeypatch.setattr(gateway, "is_termux", lambda: False)
 66          monkeypatch.setattr("getpass.getuser", lambda: "testuser")
 67          monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
 68          monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (None, "loginctl not found"))
 69          monkeypatch.setattr("shutil.which", lambda name: None)
 70  
 71          calls = []
 72          monkeypatch.setattr(gateway.subprocess, "run", lambda *args, **kwargs: calls.append((args, kwargs)))
 73  
 74          gateway._ensure_linger_enabled()
 75  
 76          out = capsys.readouterr().out
 77          assert "sudo loginctl enable-linger testuser" in out
 78          assert "loginctl not found" in out
 79          assert calls == []
 80  
 81      def test_loginctl_failure_shows_manual_guidance(self, monkeypatch, capsys):
 82          monkeypatch.setattr(gateway, "is_linux", lambda: True)
 83          monkeypatch.setattr(gateway, "is_termux", lambda: False)
 84          monkeypatch.setattr("getpass.getuser", lambda: "testuser")
 85          monkeypatch.setattr(gateway, "Path", lambda _path: SimpleNamespace(exists=lambda: False))
 86          monkeypatch.setattr(gateway, "get_systemd_linger_status", lambda: (False, ""))
 87          monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/loginctl")
 88          monkeypatch.setattr(
 89              gateway.subprocess,
 90              "run",
 91              lambda *args, **kwargs: SimpleNamespace(returncode=1, stdout="", stderr="Permission denied"),
 92          )
 93  
 94          gateway._ensure_linger_enabled()
 95  
 96          out = capsys.readouterr().out
 97          assert "sudo loginctl enable-linger testuser" in out
 98          assert "Permission denied" in out
 99  
100  
101  def test_systemd_install_calls_linger_helper(monkeypatch, tmp_path, capsys):
102      unit_path = tmp_path / "systemd" / "user" / "hermes-gateway.service"
103  
104      monkeypatch.setattr(gateway, "get_systemd_unit_path", lambda system=False: unit_path)
105  
106      calls = []
107  
108      def fake_run(cmd, check=False, **kwargs):
109          calls.append((cmd, check))
110          return SimpleNamespace(returncode=0, stdout="", stderr="")
111  
112      helper_calls = []
113      monkeypatch.setattr(gateway.subprocess, "run", fake_run)
114      monkeypatch.setattr(gateway, "_ensure_linger_enabled", lambda: helper_calls.append(True))
115  
116      gateway.systemd_install(force=False)
117  
118      out = capsys.readouterr().out
119      assert unit_path.exists()
120      assert [cmd for cmd, _ in calls] == [
121          ["systemctl", "--user", "daemon-reload"],
122          ["systemctl", "--user", "enable", gateway.get_service_name()],
123      ]
124      assert helper_calls == [True]
125      assert "User service installed and enabled" in out