import queue import threading import time from unittest.mock import patch import cli as cli_module import tools.skills_tool as skills_tool_module from cli import HermesCLI from hermes_cli.callbacks import prompt_for_secret from tools.skills_tool import set_secret_capture_callback class _FakeBuffer: def __init__(self): self.reset_called = False def reset(self): self.reset_called = True class _FakeApp: def __init__(self): self.invalidated = False self.current_buffer = _FakeBuffer() def invalidate(self): self.invalidated = True def _make_cli_stub(with_app=False): cli = HermesCLI.__new__(HermesCLI) cli._app = _FakeApp() if with_app else None cli._last_invalidate = 0.0 cli._secret_state = None cli._secret_deadline = 0 return cli def test_secret_capture_callback_can_be_completed_from_cli_state_machine(): cli = _make_cli_stub(with_app=True) results = [] with patch("hermes_cli.callbacks.save_env_value_secure") as save_secret: save_secret.return_value = { "success": True, "stored_as": "TENOR_API_KEY", "validated": False, } thread = threading.Thread( target=lambda: results.append( cli._secret_capture_callback("TENOR_API_KEY", "Tenor API key") ) ) thread.start() deadline = time.time() + 2 while cli._secret_state is None and time.time() < deadline: time.sleep(0.01) assert cli._secret_state is not None cli._submit_secret_response("super-secret-value") thread.join(timeout=2) assert results[0]["success"] is True assert results[0]["stored_as"] == "TENOR_API_KEY" assert results[0]["skipped"] is False def test_cancel_secret_capture_marks_setup_skipped(): cli = _make_cli_stub() cli._secret_state = { "response_queue": queue.Queue(), "var_name": "TENOR_API_KEY", "prompt": "Tenor API key", "metadata": {}, } cli._secret_deadline = 123 cli._cancel_secret_capture() assert cli._secret_state is None assert cli._secret_deadline == 0 def test_secret_capture_uses_getpass_without_tui(): cli = _make_cli_stub() with patch("hermes_cli.callbacks.getpass.getpass", return_value="secret-value"), patch( "hermes_cli.callbacks.save_env_value_secure" ) as save_secret: save_secret.return_value = { "success": True, "stored_as": "TENOR_API_KEY", "validated": False, } result = prompt_for_secret(cli, "TENOR_API_KEY", "Tenor API key") assert result["success"] is True assert result["stored_as"] == "TENOR_API_KEY" assert result["skipped"] is False def test_secret_capture_timeout_clears_hidden_input_buffer(): cli = _make_cli_stub(with_app=True) cleared = {"value": False} def clear_buffer(): cleared["value"] = True cli._clear_secret_input_buffer = clear_buffer with patch("hermes_cli.callbacks.queue.Queue.get", side_effect=queue.Empty), patch( "hermes_cli.callbacks._time.monotonic", side_effect=[0, 121], ): result = prompt_for_secret(cli, "TENOR_API_KEY", "Tenor API key") assert result["success"] is True assert result["skipped"] is True assert result["reason"] == "timeout" assert cleared["value"] is True def test_cli_chat_registers_secret_capture_callback(): clean_config = { "model": { "default": "anthropic/claude-opus-4.6", "base_url": "https://openrouter.ai/api/v1", "provider": "auto", }, "display": {"compact": False, "tool_progress": "all"}, "agent": {}, "terminal": {"env_type": "local"}, } with patch("cli.get_tool_definitions", return_value=[]), patch.dict( "os.environ", {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}, clear=False ), patch.dict(cli_module.__dict__, {"CLI_CONFIG": clean_config}): cli_obj = HermesCLI() with patch.object(cli_obj, "_ensure_runtime_credentials", return_value=False): cli_obj.chat("hello") try: assert skills_tool_module._secret_capture_callback == cli_obj._secret_capture_callback finally: set_secret_capture_callback(None)