mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-02 02:01:47 +00:00
fix: handle empty sudo password and false prompts
This commit is contained in:
parent
a94099908a
commit
e22416dd9b
6 changed files with 293 additions and 35 deletions
|
|
@ -2,22 +2,65 @@ import queue
|
|||
import threading
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import cli as cli_module
|
||||
from cli import HermesCLI
|
||||
|
||||
|
||||
class _FakeBuffer:
|
||||
def __init__(self, text="", cursor_position=None):
|
||||
self.text = text
|
||||
self.cursor_position = len(text) if cursor_position is None else cursor_position
|
||||
|
||||
def reset(self, append_to_history=False):
|
||||
self.text = ""
|
||||
self.cursor_position = 0
|
||||
|
||||
|
||||
def _make_cli_stub():
|
||||
cli = HermesCLI.__new__(HermesCLI)
|
||||
cli._approval_state = None
|
||||
cli._approval_deadline = 0
|
||||
cli._approval_lock = threading.Lock()
|
||||
cli._sudo_state = None
|
||||
cli._sudo_deadline = 0
|
||||
cli._modal_input_snapshot = None
|
||||
cli._invalidate = MagicMock()
|
||||
cli._app = SimpleNamespace(invalidate=MagicMock())
|
||||
cli._app = SimpleNamespace(invalidate=MagicMock(), current_buffer=_FakeBuffer())
|
||||
return cli
|
||||
|
||||
|
||||
class TestCliApprovalUi:
|
||||
def test_sudo_prompt_restores_existing_draft_after_response(self):
|
||||
cli = _make_cli_stub()
|
||||
cli._app.current_buffer = _FakeBuffer("draft command", cursor_position=5)
|
||||
result = {}
|
||||
|
||||
def _run_callback():
|
||||
result["value"] = cli._sudo_password_callback()
|
||||
|
||||
with patch.object(cli_module, "_cprint"):
|
||||
thread = threading.Thread(target=_run_callback, daemon=True)
|
||||
thread.start()
|
||||
|
||||
deadline = time.time() + 2
|
||||
while cli._sudo_state is None and time.time() < deadline:
|
||||
time.sleep(0.01)
|
||||
|
||||
assert cli._sudo_state is not None
|
||||
assert cli._app.current_buffer.text == ""
|
||||
|
||||
cli._app.current_buffer.text = "secret"
|
||||
cli._app.current_buffer.cursor_position = len("secret")
|
||||
cli._sudo_state["response_queue"].put("secret")
|
||||
|
||||
thread.join(timeout=2)
|
||||
|
||||
assert result["value"] == "secret"
|
||||
assert cli._app.current_buffer.text == "draft command"
|
||||
assert cli._app.current_buffer.cursor_position == 5
|
||||
|
||||
def test_approval_callback_includes_view_for_long_commands(self):
|
||||
cli = _make_cli_stub()
|
||||
command = "sudo dd if=/tmp/githubcli-keyring.gpg of=/usr/share/keyrings/githubcli-archive-keyring.gpg bs=4M status=progress"
|
||||
|
|
|
|||
90
tests/tools/test_terminal_tool.py
Normal file
90
tests/tools/test_terminal_tool.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""Regression tests for sudo detection and sudo password handling."""
|
||||
|
||||
import tools.terminal_tool as terminal_tool
|
||||
|
||||
|
||||
def setup_function():
|
||||
terminal_tool._cached_sudo_password = ""
|
||||
|
||||
|
||||
def teardown_function():
|
||||
terminal_tool._cached_sudo_password = ""
|
||||
|
||||
|
||||
def test_searching_for_sudo_does_not_trigger_rewrite(monkeypatch):
|
||||
monkeypatch.delenv("SUDO_PASSWORD", raising=False)
|
||||
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
|
||||
|
||||
command = "rg --line-number --no-heading --with-filename 'sudo' . | head -n 20"
|
||||
transformed, sudo_stdin = terminal_tool._transform_sudo_command(command)
|
||||
|
||||
assert transformed == command
|
||||
assert sudo_stdin is None
|
||||
|
||||
|
||||
def test_printf_literal_sudo_does_not_trigger_rewrite(monkeypatch):
|
||||
monkeypatch.delenv("SUDO_PASSWORD", raising=False)
|
||||
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
|
||||
|
||||
command = "printf '%s\\n' sudo"
|
||||
transformed, sudo_stdin = terminal_tool._transform_sudo_command(command)
|
||||
|
||||
assert transformed == command
|
||||
assert sudo_stdin is None
|
||||
|
||||
|
||||
def test_non_command_argument_named_sudo_does_not_trigger_rewrite(monkeypatch):
|
||||
monkeypatch.delenv("SUDO_PASSWORD", raising=False)
|
||||
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
|
||||
|
||||
command = "grep -n sudo README.md"
|
||||
transformed, sudo_stdin = terminal_tool._transform_sudo_command(command)
|
||||
|
||||
assert transformed == command
|
||||
assert sudo_stdin is None
|
||||
|
||||
|
||||
def test_actual_sudo_command_uses_configured_password(monkeypatch):
|
||||
monkeypatch.setenv("SUDO_PASSWORD", "testpass")
|
||||
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
|
||||
|
||||
transformed, sudo_stdin = terminal_tool._transform_sudo_command("sudo apt install -y ripgrep")
|
||||
|
||||
assert transformed == "sudo -S -p '' apt install -y ripgrep"
|
||||
assert sudo_stdin == "testpass\n"
|
||||
|
||||
|
||||
def test_actual_sudo_after_leading_env_assignment_is_rewritten(monkeypatch):
|
||||
monkeypatch.setenv("SUDO_PASSWORD", "testpass")
|
||||
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
|
||||
|
||||
transformed, sudo_stdin = terminal_tool._transform_sudo_command("DEBUG=1 sudo whoami")
|
||||
|
||||
assert transformed == "DEBUG=1 sudo -S -p '' whoami"
|
||||
assert sudo_stdin == "testpass\n"
|
||||
|
||||
|
||||
def test_explicit_empty_sudo_password_tries_empty_without_prompt(monkeypatch):
|
||||
monkeypatch.setenv("SUDO_PASSWORD", "")
|
||||
monkeypatch.setenv("HERMES_INTERACTIVE", "1")
|
||||
|
||||
def _fail_prompt(*_args, **_kwargs):
|
||||
raise AssertionError("interactive sudo prompt should not run for explicit empty password")
|
||||
|
||||
monkeypatch.setattr(terminal_tool, "_prompt_for_sudo_password", _fail_prompt)
|
||||
|
||||
transformed, sudo_stdin = terminal_tool._transform_sudo_command("sudo true")
|
||||
|
||||
assert transformed == "sudo -S -p '' true"
|
||||
assert sudo_stdin == "\n"
|
||||
|
||||
|
||||
def test_cached_sudo_password_is_used_when_env_is_unset(monkeypatch):
|
||||
monkeypatch.delenv("SUDO_PASSWORD", raising=False)
|
||||
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
|
||||
terminal_tool._cached_sudo_password = "cached-pass"
|
||||
|
||||
transformed, sudo_stdin = terminal_tool._transform_sudo_command("echo ok && sudo whoami")
|
||||
|
||||
assert transformed == "echo ok && sudo -S -p '' whoami"
|
||||
assert sudo_stdin == "cached-pass\n"
|
||||
Loading…
Add table
Add a link
Reference in a new issue