mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
304 lines
11 KiB
Python
304 lines
11 KiB
Python
"""Tests for Xiaomi MiMo provider support."""
|
|
|
|
|
|
import pytest
|
|
|
|
from hermes_cli.auth import (
|
|
PROVIDER_REGISTRY,
|
|
resolve_provider,
|
|
get_api_key_provider_status,
|
|
resolve_api_key_provider_credentials,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Provider Registry
|
|
# =============================================================================
|
|
|
|
|
|
class TestXiaomiProviderRegistry:
|
|
"""Verify Xiaomi is registered correctly in the PROVIDER_REGISTRY."""
|
|
|
|
def test_registered(self):
|
|
assert "xiaomi" in PROVIDER_REGISTRY
|
|
|
|
|
|
def test_inference_base_url(self):
|
|
assert PROVIDER_REGISTRY["xiaomi"].inference_base_url == "https://api.xiaomimimo.com/v1"
|
|
|
|
|
|
# =============================================================================
|
|
# Aliases
|
|
# =============================================================================
|
|
|
|
|
|
class TestXiaomiAliases:
|
|
"""All aliases should resolve to 'xiaomi'."""
|
|
|
|
@pytest.mark.parametrize("alias", [
|
|
"xiaomi", "mimo", "xiaomi-mimo",
|
|
])
|
|
def test_alias_resolves(self, alias, monkeypatch):
|
|
# Clear env to avoid auto-detection interfering
|
|
for key in ("XIAOMI_API_KEY",):
|
|
monkeypatch.delenv(key, raising=False)
|
|
monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-key-12345678")
|
|
assert resolve_provider(alias) == "xiaomi"
|
|
|
|
def test_normalize_provider_models_py(self):
|
|
from hermes_cli.models import normalize_provider
|
|
assert normalize_provider("mimo") == "xiaomi"
|
|
assert normalize_provider("xiaomi-mimo") == "xiaomi"
|
|
|
|
|
|
# =============================================================================
|
|
# Auto-detection
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
# Credentials
|
|
# =============================================================================
|
|
|
|
|
|
class TestXiaomiCredentials:
|
|
"""Test credential resolution for the xiaomi provider."""
|
|
|
|
|
|
|
|
def test_resolve_credentials(self, monkeypatch):
|
|
monkeypatch.setenv("XIAOMI_API_KEY", "sk-test-12345678")
|
|
monkeypatch.delenv("XIAOMI_BASE_URL", raising=False)
|
|
creds = resolve_api_key_provider_credentials("xiaomi")
|
|
assert creds["api_key"] == "sk-test-12345678"
|
|
assert creds["base_url"] == "https://api.xiaomimimo.com/v1"
|
|
|
|
|
|
def test_resolve_credentials_reads_home_external_secret_scope(
|
|
self, tmp_path, monkeypatch
|
|
):
|
|
"""BWS-injected keys belong in the profile scope that loaded them."""
|
|
from agent import secret_scope as ss
|
|
from hermes_cli import config as config_module
|
|
from hermes_cli import env_loader
|
|
|
|
home = tmp_path / "hermes"
|
|
home.mkdir()
|
|
(home / ".env").write_text("", encoding="utf-8")
|
|
monkeypatch.setattr(config_module, "get_env_path", lambda: home / ".env")
|
|
config_module.invalidate_env_cache()
|
|
|
|
monkeypatch.delenv("XIAOMI_BASE_URL", raising=False)
|
|
monkeypatch.setitem(
|
|
env_loader._SECRET_SOURCE_VALUES_BY_HOME,
|
|
str(home.resolve()),
|
|
{"XIAOMI_API_KEY": "sk-bws-xiaomi-12345678"},
|
|
)
|
|
|
|
ss.set_multiplex_active(True)
|
|
token = ss.set_secret_scope(ss.build_profile_secret_scope(home))
|
|
try:
|
|
creds = resolve_api_key_provider_credentials("xiaomi")
|
|
finally:
|
|
ss.reset_secret_scope(token)
|
|
ss.set_multiplex_active(False)
|
|
|
|
assert creds["api_key"] == "sk-bws-xiaomi-12345678"
|
|
assert creds["source"] == "XIAOMI_API_KEY"
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
# Model catalog (dynamic — no static list)
|
|
# =============================================================================
|
|
|
|
|
|
class TestXiaomiModelCatalog:
|
|
"""Xiaomi uses dynamic model discovery via models.dev."""
|
|
|
|
def test_models_dev_mapping(self):
|
|
from agent.models_dev import PROVIDER_TO_MODELS_DEV
|
|
assert PROVIDER_TO_MODELS_DEV["xiaomi"] == "xiaomi"
|
|
|
|
def test_static_model_list_fallback(self):
|
|
"""Static _PROVIDER_MODELS fallback must exist for model picker.
|
|
|
|
We only assert the provider key is present — the specific model
|
|
names are data that changes with upstream releases and doesn't
|
|
belong in tests.
|
|
"""
|
|
from hermes_cli.models import _PROVIDER_MODELS
|
|
assert "xiaomi" in _PROVIDER_MODELS
|
|
assert len(_PROVIDER_MODELS["xiaomi"]) >= 1
|
|
|
|
def test_list_agentic_models_mock(self, monkeypatch):
|
|
"""When models.dev returns Xiaomi data, list_agentic_models should return models."""
|
|
from agent import models_dev as md
|
|
|
|
fake_data = {
|
|
"xiaomi": {
|
|
"name": "Xiaomi",
|
|
"api": "https://api.xiaomimimo.com/v1",
|
|
"env": ["XIAOMI_API_KEY"],
|
|
"models": {
|
|
"mimo-v2-pro": {
|
|
"limit": {"context": 1000000},
|
|
"tool_call": True,
|
|
},
|
|
"mimo-v2-omni": {
|
|
"limit": {"context": 256000},
|
|
"tool_call": True,
|
|
},
|
|
"mimo-v2-flash": {
|
|
"limit": {"context": 256000},
|
|
"tool_call": True,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
monkeypatch.setattr(md, "fetch_models_dev", lambda: fake_data)
|
|
|
|
result = md.list_agentic_models("xiaomi")
|
|
assert "mimo-v2-pro" in result
|
|
assert "mimo-v2-flash" in result
|
|
|
|
|
|
# =============================================================================
|
|
# Normalization
|
|
# =============================================================================
|
|
|
|
|
|
class TestXiaomiNormalization:
|
|
"""Model name normalization — Xiaomi is a direct provider."""
|
|
|
|
|
|
def test_matching_prefix_strip(self):
|
|
"""xiaomi/mimo-v2-pro should normalize to mimo-v2-pro for direct API."""
|
|
from hermes_cli.model_normalize import _MATCHING_PREFIX_STRIP_PROVIDERS
|
|
assert "xiaomi" in _MATCHING_PREFIX_STRIP_PROVIDERS
|
|
|
|
|
|
def test_lowercase_subset_of_matching_prefix(self):
|
|
"""_LOWERCASE_MODEL_PROVIDERS must be a subset of _MATCHING_PREFIX_STRIP_PROVIDERS.
|
|
|
|
Otherwise the .lower() code path is unreachable dead code — the
|
|
provider check at line 422 gates entry to the block.
|
|
"""
|
|
from hermes_cli.model_normalize import (
|
|
_LOWERCASE_MODEL_PROVIDERS,
|
|
_MATCHING_PREFIX_STRIP_PROVIDERS,
|
|
)
|
|
assert _LOWERCASE_MODEL_PROVIDERS.issubset(_MATCHING_PREFIX_STRIP_PROVIDERS), (
|
|
f"_LOWERCASE_MODEL_PROVIDERS has entries not in _MATCHING_PREFIX_STRIP_PROVIDERS: "
|
|
f"{_LOWERCASE_MODEL_PROVIDERS - _MATCHING_PREFIX_STRIP_PROVIDERS}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("input_name,expected", [
|
|
("MiMo-V2.5-Pro", "mimo-v2.5-pro"),
|
|
("MIMO-V2.5-PRO", "mimo-v2.5-pro"),
|
|
("MiMo-v2.5-pro", "mimo-v2.5-pro"),
|
|
("mimo-v2.5-pro", "mimo-v2.5-pro"), # already lowercase
|
|
("MiMo-V2-Pro", "mimo-v2-pro"),
|
|
("MiMo-V2-Omni", "mimo-v2-omni"),
|
|
("MiMo-V2-Flash", "mimo-v2-flash"),
|
|
("MiMo-V2.5", "mimo-v2.5"),
|
|
])
|
|
def test_normalize_lowercases_mixed_case(self, input_name, expected):
|
|
"""Xiaomi's API requires lowercase model IDs — mixed case from docs must be lowered."""
|
|
from hermes_cli.model_normalize import normalize_model_for_provider
|
|
result = normalize_model_for_provider(input_name, "xiaomi")
|
|
assert result == expected
|
|
|
|
|
|
|
|
# =============================================================================
|
|
# URL mapping
|
|
# =============================================================================
|
|
|
|
|
|
class TestXiaomiURLMapping:
|
|
"""Test URL → provider inference for Xiaomi endpoints."""
|
|
|
|
|
|
def test_provider_prefixes(self):
|
|
from agent.model_metadata import _PROVIDER_PREFIXES
|
|
assert "xiaomi" in _PROVIDER_PREFIXES
|
|
assert "mimo" in _PROVIDER_PREFIXES
|
|
assert "xiaomi-mimo" in _PROVIDER_PREFIXES
|
|
|
|
|
|
def test_infer_from_regional_urls(self):
|
|
"""Regional token-plan endpoints should also resolve to xiaomi."""
|
|
from agent.model_metadata import _infer_provider_from_url
|
|
assert _infer_provider_from_url("https://token-plan-ams.xiaomimimo.com/v1") == "xiaomi"
|
|
assert _infer_provider_from_url("https://token-plan-cn.xiaomimimo.com/v1") == "xiaomi"
|
|
assert _infer_provider_from_url("https://token-plan-sgp.xiaomimimo.com/v1") == "xiaomi"
|
|
|
|
|
|
# =============================================================================
|
|
# providers.py
|
|
# =============================================================================
|
|
|
|
|
|
class TestXiaomiProvidersModule:
|
|
"""Test Xiaomi in the unified providers module."""
|
|
|
|
def test_overlay_exists(self):
|
|
from hermes_cli.providers import HERMES_OVERLAYS
|
|
assert "xiaomi" in HERMES_OVERLAYS
|
|
overlay = HERMES_OVERLAYS["xiaomi"]
|
|
assert overlay.transport == "openai_chat"
|
|
assert overlay.base_url_env_var == "XIAOMI_BASE_URL"
|
|
assert not overlay.is_aggregator
|
|
|
|
|
|
|
|
def test_get_provider(self):
|
|
pdef = None
|
|
try:
|
|
from hermes_cli.providers import get_provider
|
|
pdef = get_provider("xiaomi")
|
|
except Exception:
|
|
pass
|
|
if pdef is not None:
|
|
assert pdef.id == "xiaomi"
|
|
assert pdef.transport == "openai_chat"
|
|
|
|
|
|
# =============================================================================
|
|
# Auxiliary client
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
# Agent init (no SyntaxError, correct api_mode)
|
|
# =============================================================================
|
|
|
|
|
|
class TestXiaomiDoctor:
|
|
"""Verify hermes doctor recognizes Xiaomi env vars."""
|
|
|
|
def test_provider_env_hints(self):
|
|
from hermes_cli.doctor import _PROVIDER_ENV_HINTS
|
|
assert "XIAOMI_API_KEY" in _PROVIDER_ENV_HINTS
|
|
|
|
|
|
class TestXiaomiAgentInit:
|
|
"""Verify the agent can be constructed with xiaomi provider without errors."""
|
|
|
|
def test_no_syntax_errors(self):
|
|
"""Importing run_agent with xiaomi should not raise."""
|
|
import importlib
|
|
importlib.import_module("run_agent")
|
|
|
|
def test_api_mode_is_chat_completions(self):
|
|
from hermes_cli.providers import HERMES_OVERLAYS, TRANSPORT_TO_API_MODE
|
|
overlay = HERMES_OVERLAYS["xiaomi"]
|
|
api_mode = TRANSPORT_TO_API_MODE[overlay.transport]
|
|
assert api_mode == "chat_completions"
|