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.
78 lines
3 KiB
Python
78 lines
3 KiB
Python
"""Integration tests for the docker orphan-reaper wiring in terminal_tool.
|
||
|
||
The reaper itself is unit-tested in tests/tools/test_docker_environment.py
|
||
under the "Orphan reaper" section. These tests cover the terminal_tool-side
|
||
gates: once-per-process behavior, the disable flag, and the
|
||
``lifetime_seconds`` doubling that determines the reaper's age threshold.
|
||
|
||
Issue #20561 — without these gates, parallel subagents would each fire the
|
||
reaper on container creation, and the ``terminal.docker_orphan_reaper: false``
|
||
opt-out would silently do nothing.
|
||
"""
|
||
|
||
import os
|
||
from unittest.mock import patch
|
||
|
||
import tools.terminal_tool as terminal_tool
|
||
|
||
|
||
def _reset_reaper_gate():
|
||
"""Clear the once-per-process flag between tests."""
|
||
terminal_tool._docker_orphan_reaper_ran = False
|
||
|
||
|
||
def test_maybe_reap_runs_once_per_process(monkeypatch):
|
||
"""The reaper sweep must run at most once per Python interpreter.
|
||
Parallel subagents that each call _create_environment(env_type='docker')
|
||
would otherwise fire N concurrent docker ps + inspect storms against the
|
||
daemon and waste 5–10s of startup."""
|
||
_reset_reaper_gate()
|
||
call_count = {"reap": 0}
|
||
|
||
def _fake_reap(**kwargs):
|
||
call_count["reap"] += 1
|
||
return 0
|
||
|
||
with patch("tools.environments.docker.reap_orphan_containers", _fake_reap):
|
||
config = {"docker_orphan_reaper": True}
|
||
terminal_tool._maybe_reap_docker_orphans(config)
|
||
terminal_tool._maybe_reap_docker_orphans(config)
|
||
terminal_tool._maybe_reap_docker_orphans(config)
|
||
|
||
assert call_count["reap"] == 1, (
|
||
f"reaper must run exactly once per process; got {call_count['reap']} calls"
|
||
)
|
||
|
||
|
||
def test_maybe_reap_passes_current_profile_as_filter(monkeypatch):
|
||
"""The reaper must be scoped to the current Hermes profile — a research
|
||
profile must NEVER reap default's containers. Verifies the
|
||
profile-filter wiring."""
|
||
_reset_reaper_gate()
|
||
captured_args = {}
|
||
|
||
def _fake_reap(**kwargs):
|
||
captured_args.update(kwargs)
|
||
return 0
|
||
|
||
with patch("tools.environments.docker.reap_orphan_containers", _fake_reap), \
|
||
patch("tools.environments.docker._get_active_profile_name", return_value="research-bot"):
|
||
terminal_tool._maybe_reap_docker_orphans({"docker_orphan_reaper": True})
|
||
|
||
assert captured_args.get("profile_filter") == "research-bot", (
|
||
f"expected profile_filter='research-bot', got {captured_args.get('profile_filter')!r}"
|
||
)
|
||
|
||
|
||
def test_maybe_reap_swallows_exceptions(monkeypatch):
|
||
"""A reaper crash (docker daemon down, parse error in helper) must NOT
|
||
block env creation. The reaper is best-effort plumbing, not a critical
|
||
path; failures get logged at debug level and execution continues."""
|
||
_reset_reaper_gate()
|
||
|
||
def _exploding_reap(**kwargs):
|
||
raise RuntimeError("docker daemon ate the cat")
|
||
|
||
with patch("tools.environments.docker.reap_orphan_containers", _exploding_reap):
|
||
# Must not raise
|
||
terminal_tool._maybe_reap_docker_orphans({"docker_orphan_reaper": True})
|