test_cli_secret_capture.py
1 import queue 2 import threading 3 import time 4 from unittest.mock import patch 5 6 import cli as cli_module 7 import tools.skills_tool as skills_tool_module 8 from cli import HermesCLI 9 from hermes_cli.callbacks import prompt_for_secret 10 from tools.skills_tool import set_secret_capture_callback 11 12 13 class _FakeBuffer: 14 def __init__(self): 15 self.reset_called = False 16 17 def reset(self): 18 self.reset_called = True 19 20 21 class _FakeApp: 22 def __init__(self): 23 self.invalidated = False 24 self.current_buffer = _FakeBuffer() 25 26 def invalidate(self): 27 self.invalidated = True 28 29 30 def _make_cli_stub(with_app=False): 31 cli = HermesCLI.__new__(HermesCLI) 32 cli._app = _FakeApp() if with_app else None 33 cli._last_invalidate = 0.0 34 cli._secret_state = None 35 cli._secret_deadline = 0 36 return cli 37 38 39 def test_secret_capture_callback_can_be_completed_from_cli_state_machine(): 40 cli = _make_cli_stub(with_app=True) 41 results = [] 42 43 with patch("hermes_cli.callbacks.save_env_value_secure") as save_secret: 44 save_secret.return_value = { 45 "success": True, 46 "stored_as": "TENOR_API_KEY", 47 "validated": False, 48 } 49 50 thread = threading.Thread( 51 target=lambda: results.append( 52 cli._secret_capture_callback("TENOR_API_KEY", "Tenor API key") 53 ) 54 ) 55 thread.start() 56 57 deadline = time.time() + 2 58 while cli._secret_state is None and time.time() < deadline: 59 time.sleep(0.01) 60 61 assert cli._secret_state is not None 62 cli._submit_secret_response("super-secret-value") 63 thread.join(timeout=2) 64 65 assert results[0]["success"] is True 66 assert results[0]["stored_as"] == "TENOR_API_KEY" 67 assert results[0]["skipped"] is False 68 69 70 def test_cancel_secret_capture_marks_setup_skipped(): 71 cli = _make_cli_stub() 72 cli._secret_state = { 73 "response_queue": queue.Queue(), 74 "var_name": "TENOR_API_KEY", 75 "prompt": "Tenor API key", 76 "metadata": {}, 77 } 78 cli._secret_deadline = 123 79 80 cli._cancel_secret_capture() 81 82 assert cli._secret_state is None 83 assert cli._secret_deadline == 0 84 85 86 def test_secret_capture_uses_getpass_without_tui(): 87 cli = _make_cli_stub() 88 89 with patch("hermes_cli.callbacks.getpass.getpass", return_value="secret-value"), patch( 90 "hermes_cli.callbacks.save_env_value_secure" 91 ) as save_secret: 92 save_secret.return_value = { 93 "success": True, 94 "stored_as": "TENOR_API_KEY", 95 "validated": False, 96 } 97 result = prompt_for_secret(cli, "TENOR_API_KEY", "Tenor API key") 98 99 assert result["success"] is True 100 assert result["stored_as"] == "TENOR_API_KEY" 101 assert result["skipped"] is False 102 103 104 def test_secret_capture_timeout_clears_hidden_input_buffer(): 105 cli = _make_cli_stub(with_app=True) 106 cleared = {"value": False} 107 108 def clear_buffer(): 109 cleared["value"] = True 110 111 cli._clear_secret_input_buffer = clear_buffer 112 113 with patch("hermes_cli.callbacks.queue.Queue.get", side_effect=queue.Empty), patch( 114 "hermes_cli.callbacks._time.monotonic", 115 side_effect=[0, 121], 116 ): 117 result = prompt_for_secret(cli, "TENOR_API_KEY", "Tenor API key") 118 119 assert result["success"] is True 120 assert result["skipped"] is True 121 assert result["reason"] == "timeout" 122 assert cleared["value"] is True 123 124 125 def test_cli_chat_registers_secret_capture_callback(): 126 clean_config = { 127 "model": { 128 "default": "anthropic/claude-opus-4.6", 129 "base_url": "https://openrouter.ai/api/v1", 130 "provider": "auto", 131 }, 132 "display": {"compact": False, "tool_progress": "all"}, 133 "agent": {}, 134 "terminal": {"env_type": "local"}, 135 } 136 137 with patch("cli.get_tool_definitions", return_value=[]), patch.dict( 138 "os.environ", {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}, clear=False 139 ), patch.dict(cli_module.__dict__, {"CLI_CONFIG": clean_config}): 140 cli_obj = HermesCLI() 141 with patch.object(cli_obj, "_ensure_runtime_credentials", return_value=False): 142 cli_obj.chat("hello") 143 144 try: 145 assert skills_tool_module._secret_capture_callback == cli_obj._secret_capture_callback 146 finally: 147 set_secret_capture_callback(None)