test_terminal_tool.py
1 """Regression tests for sudo detection and sudo password handling.""" 2 3 import tools.terminal_tool as terminal_tool 4 5 6 def setup_function(): 7 terminal_tool._reset_cached_sudo_passwords() 8 9 10 def teardown_function(): 11 terminal_tool._reset_cached_sudo_passwords() 12 13 14 def test_searching_for_sudo_does_not_trigger_rewrite(monkeypatch): 15 monkeypatch.delenv("SUDO_PASSWORD", raising=False) 16 monkeypatch.delenv("HERMES_INTERACTIVE", raising=False) 17 18 command = "rg --line-number --no-heading --with-filename 'sudo' . | head -n 20" 19 transformed, sudo_stdin = terminal_tool._transform_sudo_command(command) 20 21 assert transformed == command 22 assert sudo_stdin is None 23 24 25 def test_printf_literal_sudo_does_not_trigger_rewrite(monkeypatch): 26 monkeypatch.delenv("SUDO_PASSWORD", raising=False) 27 monkeypatch.delenv("HERMES_INTERACTIVE", raising=False) 28 29 command = "printf '%s\\n' sudo" 30 transformed, sudo_stdin = terminal_tool._transform_sudo_command(command) 31 32 assert transformed == command 33 assert sudo_stdin is None 34 35 36 def test_non_command_argument_named_sudo_does_not_trigger_rewrite(monkeypatch): 37 monkeypatch.delenv("SUDO_PASSWORD", raising=False) 38 monkeypatch.delenv("HERMES_INTERACTIVE", raising=False) 39 40 command = "grep -n sudo README.md" 41 transformed, sudo_stdin = terminal_tool._transform_sudo_command(command) 42 43 assert transformed == command 44 assert sudo_stdin is None 45 46 47 def test_actual_sudo_command_uses_configured_password(monkeypatch): 48 monkeypatch.setenv("SUDO_PASSWORD", "testpass") 49 monkeypatch.delenv("HERMES_INTERACTIVE", raising=False) 50 51 transformed, sudo_stdin = terminal_tool._transform_sudo_command("sudo apt install -y ripgrep") 52 53 assert transformed == "sudo -S -p '' apt install -y ripgrep" 54 assert sudo_stdin == "testpass\n" 55 56 57 def test_actual_sudo_after_leading_env_assignment_is_rewritten(monkeypatch): 58 monkeypatch.setenv("SUDO_PASSWORD", "testpass") 59 monkeypatch.delenv("HERMES_INTERACTIVE", raising=False) 60 61 transformed, sudo_stdin = terminal_tool._transform_sudo_command("DEBUG=1 sudo whoami") 62 63 assert transformed == "DEBUG=1 sudo -S -p '' whoami" 64 assert sudo_stdin == "testpass\n" 65 66 67 def test_explicit_empty_sudo_password_tries_empty_without_prompt(monkeypatch): 68 monkeypatch.setenv("SUDO_PASSWORD", "") 69 monkeypatch.setenv("HERMES_INTERACTIVE", "1") 70 71 def _fail_prompt(*_args, **_kwargs): 72 raise AssertionError("interactive sudo prompt should not run for explicit empty password") 73 74 monkeypatch.setattr(terminal_tool, "_prompt_for_sudo_password", _fail_prompt) 75 76 transformed, sudo_stdin = terminal_tool._transform_sudo_command("sudo true") 77 78 assert transformed == "sudo -S -p '' true" 79 assert sudo_stdin == "\n" 80 81 82 def test_cached_sudo_password_is_used_when_env_is_unset(monkeypatch): 83 monkeypatch.delenv("SUDO_PASSWORD", raising=False) 84 monkeypatch.delenv("HERMES_INTERACTIVE", raising=False) 85 terminal_tool._set_cached_sudo_password("cached-pass") 86 87 transformed, sudo_stdin = terminal_tool._transform_sudo_command("echo ok && sudo whoami") 88 89 assert transformed == "echo ok && sudo -S -p '' whoami" 90 assert sudo_stdin == "cached-pass\n" 91 92 93 def test_cached_sudo_password_isolated_by_session_key(monkeypatch): 94 monkeypatch.delenv("SUDO_PASSWORD", raising=False) 95 monkeypatch.delenv("HERMES_INTERACTIVE", raising=False) 96 97 monkeypatch.setenv("HERMES_SESSION_KEY", "session-a") 98 terminal_tool._set_cached_sudo_password("alpha-pass") 99 100 monkeypatch.setenv("HERMES_SESSION_KEY", "session-b") 101 assert terminal_tool._get_cached_sudo_password() == "" 102 103 monkeypatch.setenv("HERMES_SESSION_KEY", "session-a") 104 assert terminal_tool._get_cached_sudo_password() == "alpha-pass" 105 106 107 def test_passwordless_sudo_skips_interactive_prompt_and_rewrite(monkeypatch): 108 monkeypatch.delenv("SUDO_PASSWORD", raising=False) 109 monkeypatch.delenv("TERMINAL_ENV", raising=False) 110 monkeypatch.setenv("HERMES_INTERACTIVE", "1") 111 112 def _fail_prompt(*_args, **_kwargs): 113 raise AssertionError( 114 "interactive sudo prompt should not run when sudo -n already works" 115 ) 116 117 monkeypatch.setattr(terminal_tool, "_prompt_for_sudo_password", _fail_prompt) 118 monkeypatch.setattr(terminal_tool, "_sudo_nopasswd_works", lambda: True, raising=False) 119 120 transformed, sudo_stdin = terminal_tool._transform_sudo_command("sudo whoami") 121 122 assert transformed == "sudo whoami" 123 assert sudo_stdin is None 124 125 126 def test_passwordless_sudo_probe_rechecks_local_terminal(monkeypatch): 127 monkeypatch.delenv("TERMINAL_ENV", raising=False) 128 calls = [] 129 130 class Result: 131 def __init__(self, returncode): 132 self.returncode = returncode 133 134 def fake_run(args, **kwargs): 135 calls.append((args, kwargs)) 136 return Result(0 if len(calls) == 1 else 1) 137 138 monkeypatch.setattr(terminal_tool.subprocess, "run", fake_run) 139 140 assert terminal_tool._sudo_nopasswd_works() is True 141 assert terminal_tool._sudo_nopasswd_works() is False 142 assert len(calls) == 2 143 assert calls[0][0] == ["sudo", "-n", "true"] 144 assert calls[1][0] == ["sudo", "-n", "true"] 145 146 147 def test_passwordless_sudo_probe_is_disabled_for_nonlocal_terminal_env(monkeypatch): 148 monkeypatch.setenv("TERMINAL_ENV", "docker") 149 150 def _fail_run(*_args, **_kwargs): 151 raise AssertionError("host sudo probe must not run for non-local terminal envs") 152 153 monkeypatch.setattr(terminal_tool.subprocess, "run", _fail_run) 154 155 assert terminal_tool._sudo_nopasswd_works() is False 156 157 158 def test_validate_workdir_allows_windows_drive_paths(): 159 assert terminal_tool._validate_workdir(r"C:\Users\Alice\project") is None 160 assert terminal_tool._validate_workdir("C:/Users/Alice/project") is None 161 162 163 def test_validate_workdir_allows_windows_unc_paths(): 164 assert terminal_tool._validate_workdir(r"\\server\share\project") is None 165 166 167 def test_validate_workdir_blocks_shell_metacharacters_in_windows_paths(): 168 assert terminal_tool._validate_workdir(r"C:\Users\Alice\project; rm -rf /") 169 assert terminal_tool._validate_workdir(r"C:\Users\Alice\project$(whoami)") 170 assert terminal_tool._validate_workdir("C:\\Users\\Alice\\project\nwhoami")