mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 15:31:38 +08:00
Compare commits
1 Commits
fix/plugin
...
fix/ci-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1647dadba |
@@ -167,6 +167,7 @@ def _resolve_runtime_from_pool_entry(
|
|||||||
api_mode = "chat_completions"
|
api_mode = "chat_completions"
|
||||||
elif provider == "copilot":
|
elif provider == "copilot":
|
||||||
api_mode = _copilot_runtime_api_mode(model_cfg, getattr(entry, "runtime_api_key", ""))
|
api_mode = _copilot_runtime_api_mode(model_cfg, getattr(entry, "runtime_api_key", ""))
|
||||||
|
base_url = base_url or PROVIDER_REGISTRY["copilot"].inference_base_url
|
||||||
else:
|
else:
|
||||||
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
|
configured_provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||||
# Honour model.base_url from config.yaml when the configured provider
|
# Honour model.base_url from config.yaml when the configured provider
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ class TestResolveVisionProviderClientModelNormalization:
|
|||||||
|
|
||||||
assert provider == "zai"
|
assert provider == "zai"
|
||||||
assert client is not None
|
assert client is not None
|
||||||
assert model == "glm-5.1"
|
assert model == "glm-5v-turbo" # zai has dedicated vision model in _PROVIDER_VISION_MODELS
|
||||||
|
|
||||||
|
|
||||||
class TestVisionPathApiMode:
|
class TestVisionPathApiMode:
|
||||||
|
|||||||
66
tests/gateway/conftest.py
Normal file
66
tests/gateway/conftest.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"""Shared fixtures for gateway tests.
|
||||||
|
|
||||||
|
The ``_ensure_telegram_mock`` helper guarantees that a minimal mock of
|
||||||
|
the ``telegram`` package is registered in :data:`sys.modules` **before**
|
||||||
|
any test file triggers ``from gateway.platforms.telegram import ...``.
|
||||||
|
|
||||||
|
Without this, ``pytest-xdist`` workers that happen to collect
|
||||||
|
``test_telegram_caption_merge.py`` (bare top-level import, no per-file
|
||||||
|
mock) first will cache ``ChatType = None`` from the production
|
||||||
|
ImportError fallback, causing 30+ downstream test failures wherever
|
||||||
|
``ChatType.GROUP`` / ``ChatType.SUPERGROUP`` is accessed.
|
||||||
|
|
||||||
|
Individual test files may still call their own ``_ensure_telegram_mock``
|
||||||
|
— it short-circuits when the mock is already present.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_telegram_mock() -> None:
|
||||||
|
"""Install a comprehensive telegram mock in sys.modules.
|
||||||
|
|
||||||
|
Idempotent — skips when the real library is already imported.
|
||||||
|
Uses ``sys.modules[name] = mod`` (overwrite) instead of
|
||||||
|
``setdefault`` so it wins even if a partial/broken import
|
||||||
|
already cached a module with ``ChatType = None``.
|
||||||
|
"""
|
||||||
|
if "telegram" in sys.modules and hasattr(sys.modules["telegram"], "__file__"):
|
||||||
|
return # Real library is installed — nothing to mock
|
||||||
|
|
||||||
|
mod = MagicMock()
|
||||||
|
mod.ext.ContextTypes.DEFAULT_TYPE = type(None)
|
||||||
|
mod.constants.ParseMode.MARKDOWN = "Markdown"
|
||||||
|
mod.constants.ParseMode.MARKDOWN_V2 = "MarkdownV2"
|
||||||
|
mod.constants.ParseMode.HTML = "HTML"
|
||||||
|
mod.constants.ChatType.PRIVATE = "private"
|
||||||
|
mod.constants.ChatType.GROUP = "group"
|
||||||
|
mod.constants.ChatType.SUPERGROUP = "supergroup"
|
||||||
|
mod.constants.ChatType.CHANNEL = "channel"
|
||||||
|
|
||||||
|
# Real exception classes so ``except (NetworkError, ...)`` clauses
|
||||||
|
# in production code don't blow up with TypeError.
|
||||||
|
mod.error.NetworkError = type("NetworkError", (OSError,), {})
|
||||||
|
mod.error.TimedOut = type("TimedOut", (OSError,), {})
|
||||||
|
mod.error.BadRequest = type("BadRequest", (Exception,), {})
|
||||||
|
mod.error.Forbidden = type("Forbidden", (Exception,), {})
|
||||||
|
mod.error.InvalidToken = type("InvalidToken", (Exception,), {})
|
||||||
|
mod.error.RetryAfter = type("RetryAfter", (Exception,), {"retry_after": 1})
|
||||||
|
mod.error.Conflict = type("Conflict", (Exception,), {})
|
||||||
|
|
||||||
|
# Update.ALL_TYPES used in start_polling()
|
||||||
|
mod.Update.ALL_TYPES = []
|
||||||
|
|
||||||
|
for name in (
|
||||||
|
"telegram",
|
||||||
|
"telegram.ext",
|
||||||
|
"telegram.constants",
|
||||||
|
"telegram.request",
|
||||||
|
):
|
||||||
|
sys.modules[name] = mod
|
||||||
|
sys.modules["telegram.error"] = mod.error
|
||||||
|
|
||||||
|
|
||||||
|
# Run at collection time — before any test file's module-level imports.
|
||||||
|
_ensure_telegram_mock()
|
||||||
@@ -613,6 +613,7 @@ class TestDetectVenvDir:
|
|||||||
# Not inside a virtualenv
|
# Not inside a virtualenv
|
||||||
monkeypatch.setattr("sys.prefix", "/usr")
|
monkeypatch.setattr("sys.prefix", "/usr")
|
||||||
monkeypatch.setattr("sys.base_prefix", "/usr")
|
monkeypatch.setattr("sys.base_prefix", "/usr")
|
||||||
|
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
|
||||||
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
|
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
|
||||||
|
|
||||||
dot_venv = tmp_path / ".venv"
|
dot_venv = tmp_path / ".venv"
|
||||||
@@ -624,6 +625,7 @@ class TestDetectVenvDir:
|
|||||||
def test_falls_back_to_venv_directory(self, tmp_path, monkeypatch):
|
def test_falls_back_to_venv_directory(self, tmp_path, monkeypatch):
|
||||||
monkeypatch.setattr("sys.prefix", "/usr")
|
monkeypatch.setattr("sys.prefix", "/usr")
|
||||||
monkeypatch.setattr("sys.base_prefix", "/usr")
|
monkeypatch.setattr("sys.base_prefix", "/usr")
|
||||||
|
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
|
||||||
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
|
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
|
||||||
|
|
||||||
venv = tmp_path / "venv"
|
venv = tmp_path / "venv"
|
||||||
@@ -635,6 +637,7 @@ class TestDetectVenvDir:
|
|||||||
def test_prefers_dot_venv_over_venv(self, tmp_path, monkeypatch):
|
def test_prefers_dot_venv_over_venv(self, tmp_path, monkeypatch):
|
||||||
monkeypatch.setattr("sys.prefix", "/usr")
|
monkeypatch.setattr("sys.prefix", "/usr")
|
||||||
monkeypatch.setattr("sys.base_prefix", "/usr")
|
monkeypatch.setattr("sys.base_prefix", "/usr")
|
||||||
|
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
|
||||||
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
|
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
|
||||||
|
|
||||||
(tmp_path / ".venv").mkdir()
|
(tmp_path / ".venv").mkdir()
|
||||||
@@ -646,6 +649,7 @@ class TestDetectVenvDir:
|
|||||||
def test_returns_none_when_no_virtualenv(self, tmp_path, monkeypatch):
|
def test_returns_none_when_no_virtualenv(self, tmp_path, monkeypatch):
|
||||||
monkeypatch.setattr("sys.prefix", "/usr")
|
monkeypatch.setattr("sys.prefix", "/usr")
|
||||||
monkeypatch.setattr("sys.base_prefix", "/usr")
|
monkeypatch.setattr("sys.base_prefix", "/usr")
|
||||||
|
monkeypatch.delenv("VIRTUAL_ENV", raising=False)
|
||||||
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
|
monkeypatch.setattr(gateway_cli, "PROJECT_ROOT", tmp_path)
|
||||||
|
|
||||||
result = gateway_cli._detect_venv_dir()
|
result = gateway_cli._detect_venv_dir()
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ def _build_agent(model_cfg, custom_providers=None, model="anthropic/claude-opus-
|
|||||||
if custom_providers is not None:
|
if custom_providers is not None:
|
||||||
cfg["custom_providers"] = custom_providers
|
cfg["custom_providers"] = custom_providers
|
||||||
|
|
||||||
|
base_url = model_cfg.get("base_url", "")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("hermes_cli.config.load_config", return_value=cfg),
|
patch("hermes_cli.config.load_config", return_value=cfg),
|
||||||
patch("agent.model_metadata.get_model_context_length", return_value=128_000),
|
patch("agent.model_metadata.get_model_context_length", return_value=128_000),
|
||||||
@@ -21,6 +23,7 @@ def _build_agent(model_cfg, custom_providers=None, model="anthropic/claude-opus-
|
|||||||
agent = AIAgent(
|
agent = AIAgent(
|
||||||
model=model,
|
model=model,
|
||||||
api_key="test-key-1234567890",
|
api_key="test-key-1234567890",
|
||||||
|
base_url=base_url,
|
||||||
quiet_mode=True,
|
quiet_mode=True,
|
||||||
skip_context_files=True,
|
skip_context_files=True,
|
||||||
skip_memory=True,
|
skip_memory=True,
|
||||||
|
|||||||
@@ -805,7 +805,10 @@ class TestCodexReasoningPreflight:
|
|||||||
reasoning_items = [i for i in normalized if i.get("type") == "reasoning"]
|
reasoning_items = [i for i in normalized if i.get("type") == "reasoning"]
|
||||||
assert len(reasoning_items) == 1
|
assert len(reasoning_items) == 1
|
||||||
assert reasoning_items[0]["encrypted_content"] == "abc123encrypted"
|
assert reasoning_items[0]["encrypted_content"] == "abc123encrypted"
|
||||||
assert reasoning_items[0]["id"] == "r_001"
|
# Note: "id" is intentionally excluded from normalized output —
|
||||||
|
# with store=False the API returns 404 on server-side id resolution.
|
||||||
|
# The id is only used for local deduplication via seen_ids.
|
||||||
|
assert "id" not in reasoning_items[0]
|
||||||
assert reasoning_items[0]["summary"] == [{"type": "summary_text", "text": "Thinking about it"}]
|
assert reasoning_items[0]["summary"] == [{"type": "summary_text", "text": "Thinking about it"}]
|
||||||
|
|
||||||
def test_reasoning_item_without_id(self, monkeypatch):
|
def test_reasoning_item_without_id(self, monkeypatch):
|
||||||
|
|||||||
@@ -46,9 +46,18 @@ def api_module(monkeypatch, tmp_path):
|
|||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
assert spec.loader is not None
|
assert spec.loader is not None
|
||||||
spec.loader.exec_module(module)
|
spec.loader.exec_module(module)
|
||||||
|
# Ensure the gws CLI code path is taken even when the binary isn't
|
||||||
|
# installed (CI). Without this, calendar_list() falls through to the
|
||||||
|
# Python SDK path which imports ``googleapiclient`` — not in deps.
|
||||||
|
module._gws_binary = lambda: "/usr/bin/gws"
|
||||||
|
# Bypass authentication check — no real token file in CI.
|
||||||
|
module._ensure_authenticated = lambda: None
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
_gws_installed = importlib.util.find_spec("shutil") and __import__("shutil").which("gws")
|
||||||
|
|
||||||
|
|
||||||
def _write_token(path: Path, *, token="ya29.test", expiry=None, **extra):
|
def _write_token(path: Path, *, token="ya29.test", expiry=None, **extra):
|
||||||
data = {
|
data = {
|
||||||
"token": token,
|
"token": token,
|
||||||
@@ -124,13 +133,14 @@ def test_bridge_main_injects_token_env(bridge_module, tmp_path):
|
|||||||
assert captured["cmd"] == ["gws", "gmail", "+triage"]
|
assert captured["cmd"] == ["gws", "gmail", "+triage"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not _gws_installed, reason="gws CLI not installed")
|
||||||
def test_api_calendar_list_uses_agenda_by_default(api_module):
|
def test_api_calendar_list_uses_agenda_by_default(api_module):
|
||||||
"""calendar list without dates uses +agenda helper."""
|
"""calendar list without dates uses +agenda helper."""
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def capture_run(cmd, **kwargs):
|
def capture_run(cmd, **kwargs):
|
||||||
captured["cmd"] = cmd
|
captured["cmd"] = cmd
|
||||||
return MagicMock(returncode=0)
|
return MagicMock(returncode=0, stdout="{}", stderr="")
|
||||||
|
|
||||||
args = api_module.argparse.Namespace(
|
args = api_module.argparse.Namespace(
|
||||||
start="", end="", max=25, calendar="primary", func=api_module.calendar_list,
|
start="", end="", max=25, calendar="primary", func=api_module.calendar_list,
|
||||||
@@ -146,6 +156,7 @@ def test_api_calendar_list_uses_agenda_by_default(api_module):
|
|||||||
assert "--days" in gws_args
|
assert "--days" in gws_args
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not _gws_installed, reason="gws CLI not installed")
|
||||||
def test_api_calendar_list_respects_date_range(api_module):
|
def test_api_calendar_list_respects_date_range(api_module):
|
||||||
"""calendar list with --start/--end uses raw events list API."""
|
"""calendar list with --start/--end uses raw events list API."""
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class TestSendMatrix:
|
|||||||
session.put.assert_called_once()
|
session.put.assert_called_once()
|
||||||
call_kwargs = session.put.call_args
|
call_kwargs = session.put.call_args
|
||||||
url = call_kwargs[0][0]
|
url = call_kwargs[0][0]
|
||||||
assert url.startswith("https://matrix.example.com/_matrix/client/v3/rooms/!room:example.com/send/m.room.message/")
|
assert url.startswith("https://matrix.example.com/_matrix/client/v3/rooms/%21room%3Aexample.com/send/m.room.message/")
|
||||||
assert call_kwargs[1]["headers"]["Authorization"] == "Bearer syt_tok"
|
assert call_kwargs[1]["headers"]["Authorization"] == "Bearer syt_tok"
|
||||||
payload = call_kwargs[1]["json"]
|
payload = call_kwargs[1]["json"]
|
||||||
assert payload["msgtype"] == "m.text"
|
assert payload["msgtype"] == "m.text"
|
||||||
|
|||||||
Reference in New Issue
Block a user