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.
169 lines
4.8 KiB
Python
169 lines
4.8 KiB
Python
import pytest
|
|
|
|
from gateway.config import GatewayConfig, Platform, PlatformConfig
|
|
from gateway.platforms.base import MessageEvent, MessageType
|
|
from gateway.run import GatewayRunner
|
|
from gateway.session import SessionSource
|
|
|
|
|
|
def _make_runner() -> GatewayRunner:
|
|
runner = object.__new__(GatewayRunner)
|
|
runner.config = GatewayConfig(
|
|
platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="fake")}
|
|
)
|
|
runner.adapters = {}
|
|
runner._pending_native_image_paths_by_session = {}
|
|
runner._session_model_overrides = {}
|
|
runner._session_reasoning_overrides = {}
|
|
return runner
|
|
|
|
|
|
def _source() -> SessionSource:
|
|
return SessionSource(
|
|
platform=Platform.TELEGRAM,
|
|
chat_id="273403055",
|
|
chat_type="dm",
|
|
user_id="42",
|
|
user_name="Maxim",
|
|
)
|
|
|
|
|
|
def _image_event(text: str = "look") -> MessageEvent:
|
|
return MessageEvent(
|
|
text=text,
|
|
message_type=MessageType.PHOTO,
|
|
source=_source(),
|
|
media_urls=["/tmp/cashback.png"],
|
|
media_types=["image/png"],
|
|
)
|
|
|
|
|
|
def _auto_config() -> dict:
|
|
return {
|
|
"agent": {"image_input_mode": "auto"},
|
|
"auxiliary": {"vision": {"provider": "auto", "model": "", "base_url": ""}},
|
|
"model": {"provider": "xiaomi", "default": "mimo-v2.5-pro"},
|
|
}
|
|
|
|
|
|
def test_pre_turn_named_custom_provider_identity_selects_vision_override(monkeypatch):
|
|
"""Gateway preprocessing must use the name retained by runtime resolution."""
|
|
runner = _make_runner()
|
|
cfg = {
|
|
"agent": {"image_input_mode": "auto"},
|
|
"model": {"provider": "default-proxy", "default": "shared-model"},
|
|
"custom_providers": [
|
|
{
|
|
"name": "default-proxy",
|
|
"models": {"shared-model": {"supports_vision": False}},
|
|
},
|
|
{
|
|
"name": "vision-provider",
|
|
"models": {"shared-model": {"supports_vision": True}},
|
|
},
|
|
],
|
|
}
|
|
monkeypatch.setattr(
|
|
runner,
|
|
"_resolve_session_agent_runtime",
|
|
lambda **_: (
|
|
"shared-model",
|
|
{
|
|
"provider": "custom",
|
|
"requested_provider": "vision-provider",
|
|
},
|
|
),
|
|
)
|
|
|
|
assert runner._decide_image_input_mode(
|
|
source=_source(),
|
|
user_config=cfg,
|
|
) == "native"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_prepare_route_identity_check_keeps_event_loop_responsive(monkeypatch):
|
|
"""A slow route-identity check must not block gateway heartbeats."""
|
|
import asyncio
|
|
import threading
|
|
from types import SimpleNamespace
|
|
|
|
runner = _make_runner()
|
|
source = _source()
|
|
event = MessageEvent(
|
|
text="inspect @AGENTS.md",
|
|
message_type=MessageType.TEXT,
|
|
source=source,
|
|
)
|
|
started = threading.Event()
|
|
released_by_event_loop = threading.Event()
|
|
seen = {}
|
|
main_thread = threading.current_thread()
|
|
|
|
cfg = {
|
|
"model": {
|
|
"default": "test-model",
|
|
"provider": "test-provider",
|
|
"base_url": "https://example.invalid/v1",
|
|
"context_length": 128000,
|
|
}
|
|
}
|
|
monkeypatch.setattr("gateway.run._load_gateway_config", lambda: cfg)
|
|
monkeypatch.setattr(
|
|
runner,
|
|
"_resolve_session_agent_runtime",
|
|
lambda **_kwargs: (
|
|
"test-model",
|
|
{
|
|
"provider": "test-provider",
|
|
"base_url": "https://example.invalid/v1",
|
|
"api_key": "",
|
|
},
|
|
),
|
|
)
|
|
|
|
def blocking_route_identity_check(*_args):
|
|
seen["thread"] = threading.current_thread()
|
|
started.set()
|
|
seen["event_loop_progressed"] = released_by_event_loop.wait(timeout=2)
|
|
return False
|
|
|
|
monkeypatch.setattr(
|
|
"hermes_cli.route_identity.should_clear_context_pin",
|
|
blocking_route_identity_check,
|
|
)
|
|
|
|
async def fake_context_length(*_args, **_kwargs):
|
|
return 128000
|
|
|
|
async def fake_preprocess(message, **_kwargs):
|
|
return SimpleNamespace(
|
|
blocked=False,
|
|
expanded=False,
|
|
message=message,
|
|
warnings=[],
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"agent.model_metadata.get_model_context_length_async", fake_context_length
|
|
)
|
|
monkeypatch.setattr(
|
|
"agent.context_references.preprocess_context_references_async",
|
|
fake_preprocess,
|
|
)
|
|
|
|
async def heartbeat_ticker():
|
|
while not started.is_set():
|
|
await asyncio.sleep(0)
|
|
await asyncio.sleep(0)
|
|
released_by_event_loop.set()
|
|
|
|
heartbeat = asyncio.create_task(heartbeat_ticker())
|
|
result = await runner._prepare_inbound_message_text(
|
|
event=event, source=source, history=[]
|
|
)
|
|
await heartbeat
|
|
|
|
assert result == "inspect @AGENTS.md"
|
|
assert seen["event_loop_progressed"] is True
|
|
assert seen["thread"] is not main_thread
|