| """Tests for the interactive CLI /model picker (provider → model drill-down).""" |
|
|
| from types import SimpleNamespace |
| from unittest.mock import MagicMock, patch |
|
|
|
|
| class _FakeBuffer: |
| def __init__(self, text="draft text"): |
| self.text = text |
| self.cursor_position = len(text) |
| self.reset_calls = [] |
|
|
| def reset(self, append_to_history=False): |
| self.reset_calls.append(append_to_history) |
| self.text = "" |
| self.cursor_position = 0 |
|
|
|
|
| def _make_providers(): |
| return [ |
| { |
| "slug": "openrouter", |
| "name": "OpenRouter", |
| "is_current": True, |
| "is_user_defined": False, |
| "models": ["anthropic/claude-opus-4.6", "openai/gpt-5.4"], |
| "total_models": 2, |
| "source": "built-in", |
| }, |
| { |
| "slug": "anthropic", |
| "name": "Anthropic", |
| "is_current": False, |
| "is_user_defined": False, |
| "models": ["claude-opus-4.6", "claude-sonnet-4.6"], |
| "total_models": 2, |
| "source": "built-in", |
| }, |
| { |
| "slug": "custom:my-ollama", |
| "name": "My Ollama", |
| "is_current": False, |
| "is_user_defined": True, |
| "models": ["llama3", "mistral"], |
| "total_models": 2, |
| "source": "user-config", |
| "api_url": "http://localhost:11434/v1", |
| }, |
| ] |
|
|
|
|
| def _make_picker_cli(picker_return_value): |
| cli = MagicMock() |
| cli._run_curses_picker = MagicMock(return_value=picker_return_value) |
| cli._app = MagicMock() |
| cli._status_bar_visible = True |
| return cli |
|
|
|
|
| def _make_modal_cli(): |
| from cli import HermesCLI |
|
|
| cli = HermesCLI.__new__(HermesCLI) |
| cli.model = "gpt-5.4" |
| cli.provider = "openrouter" |
| cli.requested_provider = "openrouter" |
| cli.base_url = "" |
| cli.api_key = "" |
| cli.api_mode = "" |
| cli._explicit_api_key = "" |
| cli._explicit_base_url = "" |
| cli._pending_model_switch_note = None |
| cli._model_picker_state = None |
| cli._modal_input_snapshot = None |
| cli._status_bar_visible = True |
| cli._invalidate = MagicMock() |
| cli.agent = None |
| cli.config = {} |
| cli.console = MagicMock() |
| cli._app = SimpleNamespace( |
| current_buffer=_FakeBuffer(), |
| invalidate=MagicMock(), |
| ) |
| return cli |
|
|
|
|
| def test_provider_selection_returns_slug_on_choice(): |
| providers = _make_providers() |
| cli = _make_picker_cli(1) |
| from cli import HermesCLI |
|
|
| result = HermesCLI._interactive_provider_selection(cli, providers, "gpt-5.4", "OpenRouter") |
|
|
| assert result == "anthropic" |
| cli._run_curses_picker.assert_called_once() |
|
|
|
|
| def test_provider_selection_returns_none_on_cancel(): |
| providers = _make_providers() |
| cli = _make_picker_cli(None) |
| from cli import HermesCLI |
|
|
| result = HermesCLI._interactive_provider_selection(cli, providers, "gpt-5.4", "OpenRouter") |
|
|
| assert result is None |
|
|
|
|
| def test_provider_selection_default_is_current(): |
| providers = _make_providers() |
| cli = _make_picker_cli(0) |
| from cli import HermesCLI |
|
|
| HermesCLI._interactive_provider_selection(cli, providers, "gpt-5.4", "OpenRouter") |
|
|
| assert cli._run_curses_picker.call_args.kwargs["default_index"] == 0 |
|
|
|
|
| def test_model_selection_returns_model_on_choice(): |
| provider_data = _make_providers()[0] |
| cli = _make_picker_cli(0) |
| from cli import HermesCLI |
|
|
| result = HermesCLI._interactive_model_selection(cli, provider_data["models"], provider_data) |
|
|
| assert result == "anthropic/claude-opus-4.6" |
|
|
|
|
| def test_model_selection_custom_entry_prompts_for_input(): |
| provider_data = _make_providers()[0] |
| cli = _make_picker_cli(2) |
| from cli import HermesCLI |
|
|
| cli._prompt_text_input = MagicMock(return_value="my-custom-model") |
| result = HermesCLI._interactive_model_selection(cli, provider_data["models"], provider_data) |
|
|
| assert result == "my-custom-model" |
| cli._prompt_text_input.assert_called_once_with(" Enter model name: ") |
|
|
|
|
| def test_model_selection_empty_prompts_for_manual_input(): |
| provider_data = { |
| "slug": "custom:empty", |
| "name": "Empty Provider", |
| "models": [], |
| "total_models": 0, |
| } |
| cli = _make_picker_cli(None) |
| from cli import HermesCLI |
|
|
| cli._prompt_text_input = MagicMock(return_value="my-model") |
| result = HermesCLI._interactive_model_selection(cli, [], provider_data) |
|
|
| assert result == "my-model" |
| cli._prompt_text_input.assert_called_once_with(" Enter model name manually (or Enter to cancel): ") |
|
|
|
|
| def test_prompt_text_input_uses_run_in_terminal_when_app_active(): |
| from cli import HermesCLI |
|
|
| cli = _make_modal_cli() |
|
|
| with ( |
| patch("prompt_toolkit.application.run_in_terminal", side_effect=lambda fn: fn()) as run_mock, |
| patch("builtins.input", return_value="manual-value"), |
| ): |
| result = HermesCLI._prompt_text_input(cli, "Enter value: ") |
|
|
| assert result == "manual-value" |
| run_mock.assert_called_once() |
| assert cli._status_bar_visible is True |
|
|
|
|
| def test_should_handle_model_command_inline_uses_command_name_resolution(): |
| from cli import HermesCLI |
|
|
| cli = _make_modal_cli() |
|
|
| with patch("hermes_cli.commands.resolve_command", return_value=SimpleNamespace(name="model")): |
| assert HermesCLI._should_handle_model_command_inline(cli, "/model") is True |
|
|
| with patch("hermes_cli.commands.resolve_command", return_value=SimpleNamespace(name="help")): |
| assert HermesCLI._should_handle_model_command_inline(cli, "/model") is False |
|
|
| assert HermesCLI._should_handle_model_command_inline(cli, "/model", has_images=True) is False |
|
|
|
|
| def test_process_command_model_without_args_opens_modal_picker_and_captures_draft(): |
| from cli import HermesCLI |
|
|
| cli = _make_modal_cli() |
| providers = _make_providers() |
|
|
| with ( |
| patch("hermes_cli.model_switch.list_authenticated_providers", return_value=providers), |
| patch("cli._cprint"), |
| ): |
| result = cli.process_command("/model") |
|
|
| assert result is True |
| assert cli._model_picker_state is not None |
| assert cli._model_picker_state["stage"] == "provider" |
| assert cli._model_picker_state["selected"] == 0 |
| assert cli._modal_input_snapshot == {"text": "draft text", "cursor_position": len("draft text")} |
| assert cli._app.current_buffer.text == "" |
|
|
|
|
| def test_model_picker_provider_then_model_selection_applies_switch_result_and_restores_draft(): |
| from cli import HermesCLI |
|
|
| cli = _make_modal_cli() |
| providers = _make_providers() |
|
|
| with ( |
| patch("hermes_cli.model_switch.list_authenticated_providers", return_value=providers), |
| patch("cli._cprint"), |
| ): |
| assert cli.process_command("/model") is True |
|
|
| cli._model_picker_state["selected"] = 1 |
| with patch("hermes_cli.models.provider_model_ids", return_value=["claude-opus-4.6", "claude-sonnet-4.6"]): |
| HermesCLI._handle_model_picker_selection(cli) |
|
|
| assert cli._model_picker_state["stage"] == "model" |
| assert cli._model_picker_state["provider_data"]["slug"] == "anthropic" |
| assert cli._model_picker_state["model_list"] == ["claude-opus-4.6", "claude-sonnet-4.6"] |
|
|
| cli._model_picker_state["selected"] = 0 |
| switch_result = SimpleNamespace( |
| success=True, |
| error_message=None, |
| new_model="claude-opus-4.6", |
| target_provider="anthropic", |
| api_key="", |
| base_url="", |
| api_mode="anthropic_messages", |
| provider_label="Anthropic", |
| model_info=None, |
| warning_message=None, |
| provider_changed=True, |
| ) |
|
|
| with ( |
| patch("hermes_cli.model_switch.switch_model", return_value=switch_result) as switch_mock, |
| patch("cli._cprint"), |
| ): |
| HermesCLI._handle_model_picker_selection(cli) |
|
|
| assert cli._model_picker_state is None |
| assert cli.model == "claude-opus-4.6" |
| assert cli.provider == "anthropic" |
| assert cli.requested_provider == "anthropic" |
| assert cli._app.current_buffer.text == "draft text" |
| switch_mock.assert_called_once() |
| assert switch_mock.call_args.kwargs["explicit_provider"] == "anthropic" |
|
|