2026-03-31 01:04:07 -07:00
|
|
|
"""Regression tests for interactive setup provider/model persistence.
|
|
|
|
|
|
|
|
|
|
Since setup_model_provider delegates to select_provider_and_model()
|
|
|
|
|
from hermes_cli.main, these tests mock the delegation point and verify
|
|
|
|
|
that the setup wizard correctly syncs config from disk after the call.
|
|
|
|
|
"""
|
2026-03-11 22:47:08 +07:00
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from hermes_cli.config import load_config, save_config, save_env_value
|
2026-03-31 01:04:07 -07:00
|
|
|
from hermes_cli.setup import setup_model_provider
|
2026-03-11 22:47:08 +07:00
|
|
|
|
|
|
|
|
|
2026-03-17 04:01:37 -07:00
|
|
|
def _maybe_keep_current_tts(question, choices):
|
|
|
|
|
if question != "Select TTS provider:":
|
|
|
|
|
return None
|
|
|
|
|
assert choices[-1].startswith("Keep current (")
|
|
|
|
|
return len(choices) - 1
|
|
|
|
|
|
|
|
|
|
|
2026-03-11 22:47:08 +07:00
|
|
|
def _clear_provider_env(monkeypatch):
|
|
|
|
|
for key in (
|
|
|
|
|
"HERMES_INFERENCE_PROVIDER",
|
|
|
|
|
"OPENAI_BASE_URL",
|
|
|
|
|
"OPENAI_API_KEY",
|
|
|
|
|
"OPENROUTER_API_KEY",
|
2026-03-17 23:40:22 -07:00
|
|
|
"GITHUB_TOKEN",
|
|
|
|
|
"GH_TOKEN",
|
2026-03-11 22:47:08 +07:00
|
|
|
"GLM_API_KEY",
|
|
|
|
|
"KIMI_API_KEY",
|
|
|
|
|
"MINIMAX_API_KEY",
|
|
|
|
|
"MINIMAX_CN_API_KEY",
|
|
|
|
|
"ANTHROPIC_TOKEN",
|
|
|
|
|
"ANTHROPIC_API_KEY",
|
|
|
|
|
):
|
|
|
|
|
monkeypatch.delenv(key, raising=False)
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def _stub_tts(monkeypatch):
|
|
|
|
|
monkeypatch.setattr("hermes_cli.setup.prompt_choice", lambda q, c, d=0: (
|
|
|
|
|
_maybe_keep_current_tts(q, c) if _maybe_keep_current_tts(q, c) is not None
|
|
|
|
|
else d
|
|
|
|
|
))
|
|
|
|
|
monkeypatch.setattr("hermes_cli.setup.prompt_yes_no", lambda *a, **kw: False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_model_config(provider, base_url="", model_name="test-model"):
|
|
|
|
|
"""Simulate what a _model_flow_* function writes to disk."""
|
|
|
|
|
cfg = load_config()
|
|
|
|
|
m = cfg.get("model")
|
|
|
|
|
if not isinstance(m, dict):
|
|
|
|
|
m = {"default": m} if m else {}
|
|
|
|
|
cfg["model"] = m
|
|
|
|
|
m["provider"] = provider
|
|
|
|
|
if base_url:
|
|
|
|
|
m["base_url"] = base_url
|
|
|
|
|
else:
|
|
|
|
|
m.pop("base_url", None)
|
|
|
|
|
if model_name:
|
|
|
|
|
m["default"] = model_name
|
|
|
|
|
m.pop("api_mode", None)
|
|
|
|
|
save_config(cfg)
|
|
|
|
|
|
|
|
|
|
|
2026-03-11 22:47:08 +07:00
|
|
|
def test_setup_keep_current_custom_from_config_does_not_fall_through(tmp_path, monkeypatch):
|
|
|
|
|
"""Keep-current custom should not fall through to the generic model menu."""
|
|
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
|
_clear_provider_env(monkeypatch)
|
2026-03-31 01:04:07 -07:00
|
|
|
_stub_tts(monkeypatch)
|
|
|
|
|
|
|
|
|
|
# Pre-set custom provider
|
|
|
|
|
_write_model_config("custom", "http://localhost:8080/v1", "local-model")
|
2026-03-11 22:47:08 +07:00
|
|
|
|
|
|
|
|
config = load_config()
|
2026-03-31 01:04:07 -07:00
|
|
|
assert config["model"]["provider"] == "custom"
|
2026-03-11 22:47:08 +07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def fake_select():
|
|
|
|
|
pass # user chose "cancel" or "keep current"
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("hermes_cli.main.select_provider_and_model", fake_select)
|
2026-03-11 22:47:08 +07:00
|
|
|
|
|
|
|
|
setup_model_provider(config)
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
|
|
|
|
reloaded = load_config()
|
2026-03-31 01:04:07 -07:00
|
|
|
assert isinstance(reloaded["model"], dict)
|
2026-03-11 22:47:08 +07:00
|
|
|
assert reloaded["model"]["provider"] == "custom"
|
2026-03-31 01:04:07 -07:00
|
|
|
assert reloaded["model"]["base_url"] == "http://localhost:8080/v1"
|
2026-03-11 22:47:08 +07:00
|
|
|
|
|
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def test_setup_keep_current_config_provider_uses_provider_specific_model_menu(
|
|
|
|
|
tmp_path, monkeypatch
|
|
|
|
|
):
|
|
|
|
|
"""Keeping current provider preserves the config on disk."""
|
2026-03-15 20:09:50 -07:00
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
|
_clear_provider_env(monkeypatch)
|
2026-03-31 01:04:07 -07:00
|
|
|
_stub_tts(monkeypatch)
|
2026-03-15 20:09:50 -07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
_write_model_config("zai", "https://open.bigmodel.cn/api/paas/v4", "glm-5")
|
2026-03-11 22:47:08 +07:00
|
|
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def fake_select():
|
|
|
|
|
pass # keep current
|
2026-03-14 10:37:45 -07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
monkeypatch.setattr("hermes_cli.main.select_provider_and_model", fake_select)
|
2026-03-14 10:37:45 -07:00
|
|
|
|
|
|
|
|
setup_model_provider(config)
|
2026-03-31 01:04:07 -07:00
|
|
|
save_config(config)
|
2026-03-14 10:37:45 -07:00
|
|
|
|
refactor: make config.yaml the single source of truth for endpoint URLs (#4165)
OPENAI_BASE_URL was written to .env AND config.yaml, creating a dual-source
confusion. Users (especially Docker) would see the URL in .env and assume
that's where all config lives, then wonder why LLM_MODEL in .env didn't work.
Changes:
- Remove all 27 save_env_value("OPENAI_BASE_URL", ...) calls across main.py,
setup.py, and tools_config.py
- Remove OPENAI_BASE_URL env var reading from runtime_provider.py, cli.py,
models.py, and gateway/run.py
- Remove LLM_MODEL/HERMES_MODEL env var reading from gateway/run.py and
auxiliary_client.py — config.yaml model.default is authoritative
- Vision base URL now saved to config.yaml auxiliary.vision.base_url
(both setup wizard and tools_config paths)
- Tests updated to set config values instead of env vars
Convention enforced: .env is for SECRETS only (API keys). All other
configuration (model names, base URLs, provider selection) lives
exclusively in config.yaml.
2026-03-30 22:02:53 -07:00
|
|
|
reloaded = load_config()
|
2026-03-31 01:04:07 -07:00
|
|
|
assert isinstance(reloaded["model"], dict)
|
|
|
|
|
assert reloaded["model"]["provider"] == "zai"
|
2026-03-11 22:47:08 +07:00
|
|
|
|
|
|
|
|
|
2026-03-17 23:40:22 -07:00
|
|
|
def test_setup_copilot_uses_gh_auth_and_saves_provider(tmp_path, monkeypatch):
|
2026-03-31 01:04:07 -07:00
|
|
|
"""Copilot provider saves correctly through delegation."""
|
2026-03-17 23:40:22 -07:00
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
|
_clear_provider_env(monkeypatch)
|
2026-03-31 01:04:07 -07:00
|
|
|
_stub_tts(monkeypatch)
|
2026-03-17 23:40:22 -07:00
|
|
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def fake_select():
|
|
|
|
|
_write_model_config("copilot", "https://models.github.ai/inference/v1", "gpt-4o")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("hermes_cli.main.select_provider_and_model", fake_select)
|
2026-03-17 23:40:22 -07:00
|
|
|
|
|
|
|
|
setup_model_provider(config)
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
|
|
|
|
reloaded = load_config()
|
2026-03-31 01:04:07 -07:00
|
|
|
assert isinstance(reloaded["model"], dict)
|
2026-03-17 23:40:22 -07:00
|
|
|
assert reloaded["model"]["provider"] == "copilot"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_setup_copilot_acp_uses_model_picker_and_saves_provider(tmp_path, monkeypatch):
|
2026-03-31 01:04:07 -07:00
|
|
|
"""Copilot ACP provider saves correctly through delegation."""
|
2026-03-17 23:40:22 -07:00
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
|
_clear_provider_env(monkeypatch)
|
2026-03-31 01:04:07 -07:00
|
|
|
_stub_tts(monkeypatch)
|
2026-03-17 23:40:22 -07:00
|
|
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def fake_select():
|
|
|
|
|
_write_model_config("copilot-acp", "", "claude-sonnet-4")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("hermes_cli.main.select_provider_and_model", fake_select)
|
2026-03-17 23:40:22 -07:00
|
|
|
|
|
|
|
|
setup_model_provider(config)
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
|
|
|
|
reloaded = load_config()
|
2026-03-31 01:04:07 -07:00
|
|
|
assert isinstance(reloaded["model"], dict)
|
2026-03-17 23:40:22 -07:00
|
|
|
assert reloaded["model"]["provider"] == "copilot-acp"
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def test_setup_switch_custom_to_codex_clears_custom_endpoint_and_updates_config(
|
|
|
|
|
tmp_path, monkeypatch
|
|
|
|
|
):
|
|
|
|
|
"""Switching from custom to codex updates config correctly."""
|
2026-03-11 22:47:08 +07:00
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
|
_clear_provider_env(monkeypatch)
|
2026-03-31 01:04:07 -07:00
|
|
|
_stub_tts(monkeypatch)
|
2026-03-11 22:47:08 +07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
# Start with custom
|
|
|
|
|
_write_model_config("custom", "http://localhost:11434/v1", "qwen3.5:32b")
|
2026-03-11 22:47:08 +07:00
|
|
|
|
|
|
|
|
config = load_config()
|
2026-03-31 01:04:07 -07:00
|
|
|
assert config["model"]["provider"] == "custom"
|
2026-03-11 22:47:08 +07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def fake_select():
|
|
|
|
|
_write_model_config("openai-codex", "https://api.openai.com/v1", "gpt-4o")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("hermes_cli.main.select_provider_and_model", fake_select)
|
2026-03-11 22:47:08 +07:00
|
|
|
|
|
|
|
|
setup_model_provider(config)
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
|
|
|
|
reloaded = load_config()
|
2026-03-31 01:04:07 -07:00
|
|
|
assert isinstance(reloaded["model"], dict)
|
2026-03-11 22:47:08 +07:00
|
|
|
assert reloaded["model"]["provider"] == "openai-codex"
|
2026-03-31 01:04:07 -07:00
|
|
|
assert reloaded["model"]["default"] == "gpt-4o"
|
2026-03-14 10:37:45 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def test_setup_switch_preserves_non_model_config(tmp_path, monkeypatch):
|
|
|
|
|
"""Provider switch preserves other config sections (terminal, display, etc.)."""
|
2026-03-14 10:37:45 -07:00
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
|
|
|
_clear_provider_env(monkeypatch)
|
2026-03-31 01:04:07 -07:00
|
|
|
_stub_tts(monkeypatch)
|
2026-03-14 10:37:45 -07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
config = load_config()
|
|
|
|
|
config["terminal"]["timeout"] = 999
|
|
|
|
|
save_config(config)
|
2026-03-14 10:37:45 -07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
config = load_config()
|
2026-03-14 21:14:20 -07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
def fake_select():
|
|
|
|
|
_write_model_config("openrouter", model_name="gpt-4o")
|
2026-03-14 21:14:20 -07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
monkeypatch.setattr("hermes_cli.main.select_provider_and_model", fake_select)
|
2026-03-14 21:14:20 -07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
setup_model_provider(config)
|
|
|
|
|
save_config(config)
|
2026-03-14 21:14:20 -07:00
|
|
|
|
2026-03-31 01:04:07 -07:00
|
|
|
reloaded = load_config()
|
|
|
|
|
assert reloaded["terminal"]["timeout"] == 999
|
|
|
|
|
assert reloaded["model"]["provider"] == "openrouter"
|