refactor(tests): re-architect tests + fix CI failures (#5946)
* refactor: re-architect tests to mirror the codebase
* Update tests.yml
* fix: add missing tool_error imports after registry refactor
* fix(tests): replace patch.dict with monkeypatch to prevent env var leaks under xdist
patch.dict(os.environ) can leak TERMINAL_ENV across xdist workers,
causing test_code_execution tests to hit the Modal remote path.
* fix(tests): fix update_check and telegram xdist failures
- test_update_check: replace patch("hermes_cli.banner.os.getenv") with
monkeypatch.setenv("HERMES_HOME") — banner.py no longer imports os
directly, it uses get_hermes_home() from hermes_constants.
- test_telegram_conflict/approval_buttons: provide real exception classes
for telegram.error mock (NetworkError, TimedOut, BadRequest) so the
except clause in connect() doesn't fail with "catching classes that do
not inherit from BaseException" when xdist pollutes sys.modules.
* fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock
This commit is contained in:
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -19,6 +19,9 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y ripgrep
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from unittest.mock import patch, MagicMock
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
|
||||
def _run_auxiliary_bridge(config_dict, monkeypatch):
|
||||
@@ -199,7 +199,7 @@ class TestGatewayBridgeCodeParity:
|
||||
|
||||
def test_gateway_has_auxiliary_bridge(self):
|
||||
"""The gateway config bridge must include auxiliary.* bridging."""
|
||||
gateway_path = Path(__file__).parent.parent / "gateway" / "run.py"
|
||||
gateway_path = Path(__file__).parent.parent.parent / "gateway" / "run.py"
|
||||
content = gateway_path.read_text()
|
||||
# Check for key patterns that indicate the bridge is present
|
||||
assert "AUXILIARY_VISION_PROVIDER" in content
|
||||
@@ -213,7 +213,7 @@ class TestGatewayBridgeCodeParity:
|
||||
|
||||
def test_gateway_no_compression_env_bridge(self):
|
||||
"""Gateway should NOT bridge compression config to env vars (config-only)."""
|
||||
gateway_path = Path(__file__).parent.parent / "gateway" / "run.py"
|
||||
gateway_path = Path(__file__).parent.parent.parent / "gateway" / "run.py"
|
||||
content = gateway_path.read_text()
|
||||
assert "CONTEXT_COMPRESSION_PROVIDER" not in content
|
||||
assert "CONTEXT_COMPRESSION_MODEL" not in content
|
||||
0
tests/cli/__init__.py
Normal file
0
tests/cli/__init__.py
Normal file
@@ -330,7 +330,7 @@ def test_model_flow_nous_prints_subscription_guidance_without_mutating_explicit_
|
||||
"hermes_cli.auth.fetch_nous_models",
|
||||
lambda *args, **kwargs: ["claude-opus-4-6"],
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None: "claude-opus-4-6")
|
||||
monkeypatch.setattr("hermes_cli.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None, **kw: "claude-opus-4-6")
|
||||
monkeypatch.setattr("hermes_cli.auth._save_model_choice", lambda model: None)
|
||||
monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None)
|
||||
monkeypatch.setattr(
|
||||
@@ -368,7 +368,7 @@ def test_model_flow_nous_applies_managed_tts_default_when_unconfigured(monkeypat
|
||||
"hermes_cli.auth.fetch_nous_models",
|
||||
lambda *args, **kwargs: ["claude-opus-4-6"],
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None: "claude-opus-4-6")
|
||||
monkeypatch.setattr("hermes_cli.auth._prompt_model_selection", lambda model_ids, current_model="", pricing=None, **kw: "claude-opus-4-6")
|
||||
monkeypatch.setattr("hermes_cli.auth._save_model_choice", lambda model: None)
|
||||
monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None)
|
||||
monkeypatch.setattr(
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Regression tests for CLI /retry history replacement semantics."""
|
||||
|
||||
from tests.test_cli_init import _make_cli
|
||||
from tests.cli.test_cli_init import _make_cli
|
||||
|
||||
|
||||
def test_retry_last_truncates_history_before_requeueing_message():
|
||||
@@ -33,8 +33,15 @@ def _ensure_telegram_mock():
|
||||
mod.constants.ChatType.GROUP = "group"
|
||||
mod.constants.ChatType.SUPERGROUP = "supergroup"
|
||||
mod.constants.ChatType.CHANNEL = "channel"
|
||||
for name in ("telegram", "telegram.ext", "telegram.constants", "telegram.request", "telegram.error"):
|
||||
# Provide real exception classes so ``except (NetworkError, ...)`` in
|
||||
# connect() doesn't blow up under xdist when this mock leaks.
|
||||
mod.error.NetworkError = type("NetworkError", (OSError,), {})
|
||||
mod.error.TimedOut = type("TimedOut", (OSError,), {})
|
||||
mod.error.BadRequest = type("BadRequest", (Exception,), {})
|
||||
|
||||
for name in ("telegram", "telegram.ext", "telegram.constants", "telegram.request"):
|
||||
sys.modules.setdefault(name, mod)
|
||||
sys.modules.setdefault("telegram.error", mod.error)
|
||||
|
||||
|
||||
_ensure_telegram_mock()
|
||||
|
||||
@@ -20,8 +20,16 @@ def _ensure_telegram_mock():
|
||||
telegram_mod.constants.ChatType.CHANNEL = "channel"
|
||||
telegram_mod.constants.ChatType.PRIVATE = "private"
|
||||
|
||||
# Provide real exception classes so ``except (NetworkError, ...)`` in
|
||||
# connect() doesn't blow up with "catching classes that do not inherit
|
||||
# from BaseException" when another xdist worker pollutes sys.modules.
|
||||
telegram_mod.error.NetworkError = type("NetworkError", (OSError,), {})
|
||||
telegram_mod.error.TimedOut = type("TimedOut", (OSError,), {})
|
||||
telegram_mod.error.BadRequest = type("BadRequest", (Exception,), {})
|
||||
|
||||
for name in ("telegram", "telegram.ext", "telegram.constants", "telegram.request"):
|
||||
sys.modules.setdefault(name, telegram_mod)
|
||||
sys.modules.setdefault("telegram.error", telegram_mod.error)
|
||||
|
||||
|
||||
_ensure_telegram_mock()
|
||||
|
||||
@@ -15,7 +15,7 @@ def test_version_string_no_v_prefix():
|
||||
assert not __version__.startswith("v"), f"__version__ should not start with 'v', got {__version__!r}"
|
||||
|
||||
|
||||
def test_check_for_updates_uses_cache(tmp_path):
|
||||
def test_check_for_updates_uses_cache(tmp_path, monkeypatch):
|
||||
"""When cache is fresh, check_for_updates should return cached value without calling git."""
|
||||
from hermes_cli.banner import check_for_updates
|
||||
|
||||
@@ -27,15 +27,15 @@ def test_check_for_updates_uses_cache(tmp_path):
|
||||
cache_file = tmp_path / ".update_check"
|
||||
cache_file.write_text(json.dumps({"ts": time.time(), "behind": 3}))
|
||||
|
||||
with patch("hermes_cli.banner.os.getenv", return_value=str(tmp_path)):
|
||||
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
||||
result = check_for_updates()
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
||||
result = check_for_updates()
|
||||
|
||||
assert result == 3
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
def test_check_for_updates_expired_cache(tmp_path):
|
||||
def test_check_for_updates_expired_cache(tmp_path, monkeypatch):
|
||||
"""When cache is expired, check_for_updates should call git fetch."""
|
||||
from hermes_cli.banner import check_for_updates
|
||||
|
||||
@@ -49,15 +49,15 @@ def test_check_for_updates_expired_cache(tmp_path):
|
||||
|
||||
mock_result = MagicMock(returncode=0, stdout="5\n")
|
||||
|
||||
with patch("hermes_cli.banner.os.getenv", return_value=str(tmp_path)):
|
||||
with patch("hermes_cli.banner.subprocess.run", return_value=mock_result) as mock_run:
|
||||
result = check_for_updates()
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
with patch("hermes_cli.banner.subprocess.run", return_value=mock_result) as mock_run:
|
||||
result = check_for_updates()
|
||||
|
||||
assert result == 5
|
||||
assert mock_run.call_count == 2 # git fetch + git rev-list
|
||||
|
||||
|
||||
def test_check_for_updates_no_git_dir(tmp_path):
|
||||
def test_check_for_updates_no_git_dir(tmp_path, monkeypatch):
|
||||
"""Returns None when .git directory doesn't exist anywhere."""
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
@@ -66,19 +66,15 @@ def test_check_for_updates_no_git_dir(tmp_path):
|
||||
fake_banner.parent.mkdir(parents=True, exist_ok=True)
|
||||
fake_banner.touch()
|
||||
|
||||
original = banner.__file__
|
||||
try:
|
||||
banner.__file__ = str(fake_banner)
|
||||
with patch("hermes_cli.banner.os.getenv", return_value=str(tmp_path)):
|
||||
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
||||
result = banner.check_for_updates()
|
||||
assert result is None
|
||||
mock_run.assert_not_called()
|
||||
finally:
|
||||
banner.__file__ = original
|
||||
monkeypatch.setattr(banner, "__file__", str(fake_banner))
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
||||
result = banner.check_for_updates()
|
||||
assert result is None
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
def test_check_for_updates_fallback_to_project_root():
|
||||
def test_check_for_updates_fallback_to_project_root(tmp_path, monkeypatch):
|
||||
"""Dev install: falls back to Path(__file__).parent.parent when HERMES_HOME has no git repo."""
|
||||
import hermes_cli.banner as banner
|
||||
|
||||
@@ -87,14 +83,12 @@ def test_check_for_updates_fallback_to_project_root():
|
||||
pytest.skip("Not running from a git checkout")
|
||||
|
||||
# Point HERMES_HOME at a temp dir with no hermes-agent/.git
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
with patch("hermes_cli.banner.os.getenv", return_value=td):
|
||||
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="0\n")
|
||||
result = banner.check_for_updates()
|
||||
# Should have fallen back to project root and run git commands
|
||||
assert mock_run.call_count >= 1
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="0\n")
|
||||
result = banner.check_for_updates()
|
||||
# Should have fallen back to project root and run git commands
|
||||
assert mock_run.call_count >= 1
|
||||
|
||||
|
||||
def test_prefetch_non_blocking():
|
||||
|
||||
0
tests/run_agent/__init__.py
Normal file
0
tests/run_agent/__init__.py
Normal file
@@ -16,7 +16,7 @@ from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
# Ensure repo root is importable
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
||||
|
||||
try:
|
||||
from environments.agent_loop import (
|
||||
@@ -31,7 +31,7 @@ import pytest
|
||||
# pytestmark removed — tests skip gracefully via OPENROUTER_API_KEY check on line 59
|
||||
|
||||
# Ensure repo root is importable
|
||||
_repo_root = Path(__file__).resolve().parent.parent
|
||||
_repo_root = Path(__file__).resolve().parent.parent.parent
|
||||
if str(_repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(_repo_root))
|
||||
|
||||
@@ -30,7 +30,7 @@ import pytest
|
||||
import requests
|
||||
|
||||
# Ensure repo root is importable
|
||||
_repo_root = Path(__file__).resolve().parent.parent
|
||||
_repo_root = Path(__file__).resolve().parent.parent.parent
|
||||
if str(_repo_root) not in sys.path:
|
||||
sys.path.insert(0, str(_repo_root))
|
||||
|
||||
@@ -23,7 +23,7 @@ logging.basicConfig(level=logging.DEBUG, stream=sys.stderr,
|
||||
format="%(asctime)s [%(threadName)s] %(message)s")
|
||||
log = logging.getLogger("interrupt_test")
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from run_agent import AIAgent, IterationBudget
|
||||
@@ -122,7 +122,7 @@ class TestSourceLinesAreClamped:
|
||||
@staticmethod
|
||||
def _read_file(rel_path: str) -> str:
|
||||
import os
|
||||
base = os.path.dirname(os.path.dirname(__file__))
|
||||
base = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
with open(os.path.join(base, rel_path)) as f:
|
||||
return f.read()
|
||||
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
# Ensure repo root is importable
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
||||
|
||||
# Stub out optional heavy dependencies not installed in the test environment
|
||||
sys.modules.setdefault("fire", types.SimpleNamespace(Fire=lambda *a, **k: None))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user