diff --git a/tests/tools/test_ssh_environment.py b/tests/tools/test_ssh_environment.py new file mode 100644 index 0000000000..aca8b22592 --- /dev/null +++ b/tests/tools/test_ssh_environment.py @@ -0,0 +1,39 @@ +import pytest + +from tools.environments import ssh as ssh_env + + +def test_ensure_ssh_available_raises_clear_error_when_missing(monkeypatch): + monkeypatch.setattr(ssh_env.shutil, "which", lambda _name: None) + + with pytest.raises(RuntimeError, match="SSH is not installed or not in PATH"): + ssh_env._ensure_ssh_available() + + +def test_ssh_environment_checks_availability_before_connect(monkeypatch): + monkeypatch.setattr(ssh_env.shutil, "which", lambda _name: None) + monkeypatch.setattr( + ssh_env.SSHEnvironment, + "_establish_connection", + lambda self: pytest.fail("_establish_connection should not run when ssh is missing"), + ) + + with pytest.raises(RuntimeError, match="openssh-client"): + ssh_env.SSHEnvironment(host="example.com", user="alice") + + +def test_ssh_environment_connects_when_ssh_exists(monkeypatch): + called = {"count": 0} + + monkeypatch.setattr(ssh_env.shutil, "which", lambda _name: "/usr/bin/ssh") + + def _fake_establish(self): + called["count"] += 1 + + monkeypatch.setattr(ssh_env.SSHEnvironment, "_establish_connection", _fake_establish) + + env = ssh_env.SSHEnvironment(host="example.com", user="alice") + + assert called["count"] == 1 + assert env.host == "example.com" + assert env.user == "alice" diff --git a/tools/environments/ssh.py b/tools/environments/ssh.py index 83cc335b1e..3a9095cf88 100644 --- a/tools/environments/ssh.py +++ b/tools/environments/ssh.py @@ -1,6 +1,7 @@ """SSH remote execution environment with ControlMaster connection persistence.""" import logging +import shutil import subprocess import tempfile import threading @@ -13,6 +14,14 @@ from tools.interrupt import is_interrupted logger = logging.getLogger(__name__) +def _ensure_ssh_available() -> None: + """Fail fast with a clear error when the SSH client is unavailable.""" + if not shutil.which("ssh"): + raise RuntimeError( + "SSH is not installed or not in PATH. Install OpenSSH client: apt install openssh-client" + ) + + class SSHEnvironment(BaseEnvironment): """Run commands on a remote machine over SSH. @@ -35,6 +44,7 @@ class SSHEnvironment(BaseEnvironment): self.control_dir = Path(tempfile.gettempdir()) / "hermes-ssh" self.control_dir.mkdir(parents=True, exist_ok=True) self.control_socket = self.control_dir / f"{user}@{host}:{port}.sock" + _ensure_ssh_available() self._establish_connection() def _build_ssh_command(self, extra_args: list = None) -> list: