188 lines
7.0 KiB
Python
188 lines
7.0 KiB
Python
import importlib
|
|
import sys
|
|
import types
|
|
from contextlib import nullcontext
|
|
from types import SimpleNamespace
|
|
|
|
from hermes_cli.auth import AuthError
|
|
from hermes_cli import main as hermes_main
|
|
|
|
|
|
def _install_prompt_toolkit_stubs():
|
|
class _Dummy:
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
|
|
class _Condition:
|
|
def __init__(self, func):
|
|
self.func = func
|
|
|
|
def __bool__(self):
|
|
return bool(self.func())
|
|
|
|
class _ANSI(str):
|
|
pass
|
|
|
|
root = types.ModuleType("prompt_toolkit")
|
|
history = types.ModuleType("prompt_toolkit.history")
|
|
styles = types.ModuleType("prompt_toolkit.styles")
|
|
patch_stdout = types.ModuleType("prompt_toolkit.patch_stdout")
|
|
application = types.ModuleType("prompt_toolkit.application")
|
|
layout = types.ModuleType("prompt_toolkit.layout")
|
|
processors = types.ModuleType("prompt_toolkit.layout.processors")
|
|
filters = types.ModuleType("prompt_toolkit.filters")
|
|
dimension = types.ModuleType("prompt_toolkit.layout.dimension")
|
|
menus = types.ModuleType("prompt_toolkit.layout.menus")
|
|
widgets = types.ModuleType("prompt_toolkit.widgets")
|
|
key_binding = types.ModuleType("prompt_toolkit.key_binding")
|
|
completion = types.ModuleType("prompt_toolkit.completion")
|
|
formatted_text = types.ModuleType("prompt_toolkit.formatted_text")
|
|
|
|
history.FileHistory = _Dummy
|
|
styles.Style = _Dummy
|
|
patch_stdout.patch_stdout = lambda *args, **kwargs: nullcontext()
|
|
application.Application = _Dummy
|
|
layout.Layout = _Dummy
|
|
layout.HSplit = _Dummy
|
|
layout.Window = _Dummy
|
|
layout.FormattedTextControl = _Dummy
|
|
layout.ConditionalContainer = _Dummy
|
|
processors.Processor = _Dummy
|
|
processors.Transformation = _Dummy
|
|
processors.PasswordProcessor = _Dummy
|
|
processors.ConditionalProcessor = _Dummy
|
|
filters.Condition = _Condition
|
|
dimension.Dimension = _Dummy
|
|
menus.CompletionsMenu = _Dummy
|
|
widgets.TextArea = _Dummy
|
|
key_binding.KeyBindings = _Dummy
|
|
completion.Completer = _Dummy
|
|
completion.Completion = _Dummy
|
|
formatted_text.ANSI = _ANSI
|
|
root.print_formatted_text = lambda *args, **kwargs: None
|
|
|
|
sys.modules.setdefault("prompt_toolkit", root)
|
|
sys.modules.setdefault("prompt_toolkit.history", history)
|
|
sys.modules.setdefault("prompt_toolkit.styles", styles)
|
|
sys.modules.setdefault("prompt_toolkit.patch_stdout", patch_stdout)
|
|
sys.modules.setdefault("prompt_toolkit.application", application)
|
|
sys.modules.setdefault("prompt_toolkit.layout", layout)
|
|
sys.modules.setdefault("prompt_toolkit.layout.processors", processors)
|
|
sys.modules.setdefault("prompt_toolkit.filters", filters)
|
|
sys.modules.setdefault("prompt_toolkit.layout.dimension", dimension)
|
|
sys.modules.setdefault("prompt_toolkit.layout.menus", menus)
|
|
sys.modules.setdefault("prompt_toolkit.widgets", widgets)
|
|
sys.modules.setdefault("prompt_toolkit.key_binding", key_binding)
|
|
sys.modules.setdefault("prompt_toolkit.completion", completion)
|
|
sys.modules.setdefault("prompt_toolkit.formatted_text", formatted_text)
|
|
|
|
|
|
def _import_cli():
|
|
try:
|
|
importlib.import_module("prompt_toolkit")
|
|
except ModuleNotFoundError:
|
|
_install_prompt_toolkit_stubs()
|
|
return importlib.import_module("cli")
|
|
|
|
|
|
def test_hermes_cli_init_does_not_eagerly_resolve_runtime_provider(monkeypatch):
|
|
cli = _import_cli()
|
|
calls = {"count": 0}
|
|
|
|
def _unexpected_runtime_resolve(**kwargs):
|
|
calls["count"] += 1
|
|
raise AssertionError("resolve_runtime_provider should not be called in HermesCLI.__init__")
|
|
|
|
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _unexpected_runtime_resolve)
|
|
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
|
|
|
|
shell = cli.HermesCLI(model="gpt-5", compact=True, max_turns=1)
|
|
|
|
assert shell is not None
|
|
assert calls["count"] == 0
|
|
|
|
|
|
def test_runtime_resolution_failure_is_not_sticky(monkeypatch):
|
|
cli = _import_cli()
|
|
calls = {"count": 0}
|
|
|
|
def _runtime_resolve(**kwargs):
|
|
calls["count"] += 1
|
|
if calls["count"] == 1:
|
|
raise RuntimeError("temporary auth failure")
|
|
return {
|
|
"provider": "openrouter",
|
|
"api_mode": "chat_completions",
|
|
"base_url": "https://openrouter.ai/api/v1",
|
|
"api_key": "test-key",
|
|
"source": "env/config",
|
|
}
|
|
|
|
class _DummyAgent:
|
|
def __init__(self, *args, **kwargs):
|
|
self.kwargs = kwargs
|
|
|
|
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
|
|
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
|
|
monkeypatch.setattr(cli, "AIAgent", _DummyAgent)
|
|
|
|
shell = cli.HermesCLI(model="gpt-5", compact=True, max_turns=1)
|
|
|
|
assert shell._init_agent() is False
|
|
assert shell._init_agent() is True
|
|
assert calls["count"] == 2
|
|
assert shell.agent is not None
|
|
|
|
|
|
def test_runtime_resolution_rebuilds_agent_on_routing_change(monkeypatch):
|
|
cli = _import_cli()
|
|
|
|
def _runtime_resolve(**kwargs):
|
|
return {
|
|
"provider": "openai-codex",
|
|
"api_mode": "codex_responses",
|
|
"base_url": "https://same-endpoint.example/v1",
|
|
"api_key": "same-key",
|
|
"source": "env/config",
|
|
}
|
|
|
|
monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve)
|
|
monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc))
|
|
|
|
shell = cli.HermesCLI(model="gpt-5", compact=True, max_turns=1)
|
|
shell.provider = "openrouter"
|
|
shell.api_mode = "chat_completions"
|
|
shell.base_url = "https://same-endpoint.example/v1"
|
|
shell.api_key = "same-key"
|
|
shell.agent = object()
|
|
|
|
assert shell._ensure_runtime_credentials() is True
|
|
assert shell.agent is None
|
|
assert shell.provider == "openai-codex"
|
|
assert shell.api_mode == "codex_responses"
|
|
|
|
|
|
def test_cmd_model_falls_back_to_auto_on_invalid_provider(monkeypatch, capsys):
|
|
monkeypatch.setattr(
|
|
"hermes_cli.config.load_config",
|
|
lambda: {"model": {"default": "gpt-5", "provider": "invalid-provider"}},
|
|
)
|
|
monkeypatch.setattr("hermes_cli.config.save_config", lambda cfg: None)
|
|
monkeypatch.setattr("hermes_cli.config.get_env_value", lambda key: "")
|
|
monkeypatch.setattr("hermes_cli.config.save_env_value", lambda key, value: None)
|
|
|
|
def _resolve_provider(requested, **kwargs):
|
|
if requested == "invalid-provider":
|
|
raise AuthError("Unknown provider 'invalid-provider'.", code="invalid_provider")
|
|
return "openrouter"
|
|
|
|
monkeypatch.setattr("hermes_cli.auth.resolve_provider", _resolve_provider)
|
|
monkeypatch.setattr(hermes_main, "_prompt_provider_choice", lambda choices: len(choices) - 1)
|
|
|
|
hermes_main.cmd_model(SimpleNamespace())
|
|
output = capsys.readouterr().out
|
|
|
|
assert "Warning:" in output
|
|
assert "falling back to auto provider detection" in output.lower()
|
|
assert "No change." in output
|