Compare commits

...

6 Commits

Author SHA1 Message Date
alt-glitch
fcf64d5283 fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock 2026-04-07 16:41:11 -07:00
alt-glitch
8bbafdf3a6 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.
2026-04-07 16:34:09 -07:00
alt-glitch
04ee0ec0bc 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.
2026-04-07 16:30:22 -07:00
alt-glitch
b7903bca41 fix: add missing tool_error imports after registry refactor 2026-04-07 16:18:21 -07:00
alt-glitch
20e94662cc Update tests.yml 2026-04-07 16:06:25 -07:00
alt-glitch
6ed3f9ca80 refactor: re-architect tests to mirror the codebase 2026-04-07 14:29:51 -07:00
110 changed files with 153 additions and 150 deletions

View File

@@ -19,6 +19,9 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y ripgrep
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@v5

View File

@@ -13,7 +13,7 @@ from unittest.mock import patch, MagicMock
import pytest import pytest
import yaml 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): def _run_auxiliary_bridge(config_dict, monkeypatch):
@@ -199,7 +199,7 @@ class TestGatewayBridgeCodeParity:
def test_gateway_has_auxiliary_bridge(self): def test_gateway_has_auxiliary_bridge(self):
"""The gateway config bridge must include auxiliary.* bridging.""" """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() content = gateway_path.read_text()
# Check for key patterns that indicate the bridge is present # Check for key patterns that indicate the bridge is present
assert "AUXILIARY_VISION_PROVIDER" in content assert "AUXILIARY_VISION_PROVIDER" in content
@@ -213,7 +213,7 @@ class TestGatewayBridgeCodeParity:
def test_gateway_no_compression_env_bridge(self): def test_gateway_no_compression_env_bridge(self):
"""Gateway should NOT bridge compression config to env vars (config-only).""" """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() content = gateway_path.read_text()
assert "CONTEXT_COMPRESSION_PROVIDER" not in content assert "CONTEXT_COMPRESSION_PROVIDER" not in content
assert "CONTEXT_COMPRESSION_MODEL" not in content assert "CONTEXT_COMPRESSION_MODEL" not in content

0
tests/cli/__init__.py Normal file
View File

View File

@@ -330,7 +330,7 @@ def test_model_flow_nous_prints_subscription_guidance_without_mutating_explicit_
"hermes_cli.auth.fetch_nous_models", "hermes_cli.auth.fetch_nous_models",
lambda *args, **kwargs: ["claude-opus-4-6"], 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._save_model_choice", lambda model: None)
monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None) monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None)
monkeypatch.setattr( monkeypatch.setattr(
@@ -368,7 +368,7 @@ def test_model_flow_nous_applies_managed_tts_default_when_unconfigured(monkeypat
"hermes_cli.auth.fetch_nous_models", "hermes_cli.auth.fetch_nous_models",
lambda *args, **kwargs: ["claude-opus-4-6"], 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._save_model_choice", lambda model: None)
monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None) monkeypatch.setattr("hermes_cli.auth._update_config_for_provider", lambda provider, url: None)
monkeypatch.setattr( monkeypatch.setattr(

View File

@@ -1,6 +1,6 @@
"""Regression tests for CLI /retry history replacement semantics.""" """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(): def test_retry_last_truncates_history_before_requeueing_message():

View File

@@ -33,8 +33,15 @@ def _ensure_telegram_mock():
mod.constants.ChatType.GROUP = "group" mod.constants.ChatType.GROUP = "group"
mod.constants.ChatType.SUPERGROUP = "supergroup" mod.constants.ChatType.SUPERGROUP = "supergroup"
mod.constants.ChatType.CHANNEL = "channel" 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(name, mod)
sys.modules.setdefault("telegram.error", mod.error)
_ensure_telegram_mock() _ensure_telegram_mock()

View File

@@ -20,8 +20,16 @@ def _ensure_telegram_mock():
telegram_mod.constants.ChatType.CHANNEL = "channel" telegram_mod.constants.ChatType.CHANNEL = "channel"
telegram_mod.constants.ChatType.PRIVATE = "private" 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"): for name in ("telegram", "telegram.ext", "telegram.constants", "telegram.request"):
sys.modules.setdefault(name, telegram_mod) sys.modules.setdefault(name, telegram_mod)
sys.modules.setdefault("telegram.error", telegram_mod.error)
_ensure_telegram_mock() _ensure_telegram_mock()

View File

@@ -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}" 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.""" """When cache is fresh, check_for_updates should return cached value without calling git."""
from hermes_cli.banner import check_for_updates 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 = tmp_path / ".update_check"
cache_file.write_text(json.dumps({"ts": time.time(), "behind": 3})) cache_file.write_text(json.dumps({"ts": time.time(), "behind": 3}))
with patch("hermes_cli.banner.os.getenv", return_value=str(tmp_path)): monkeypatch.setenv("HERMES_HOME", str(tmp_path))
with patch("hermes_cli.banner.subprocess.run") as mock_run: with patch("hermes_cli.banner.subprocess.run") as mock_run:
result = check_for_updates() result = check_for_updates()
assert result == 3 assert result == 3
mock_run.assert_not_called() 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.""" """When cache is expired, check_for_updates should call git fetch."""
from hermes_cli.banner import check_for_updates 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") mock_result = MagicMock(returncode=0, stdout="5\n")
with patch("hermes_cli.banner.os.getenv", return_value=str(tmp_path)): monkeypatch.setenv("HERMES_HOME", str(tmp_path))
with patch("hermes_cli.banner.subprocess.run", return_value=mock_result) as mock_run: with patch("hermes_cli.banner.subprocess.run", return_value=mock_result) as mock_run:
result = check_for_updates() result = check_for_updates()
assert result == 5 assert result == 5
assert mock_run.call_count == 2 # git fetch + git rev-list 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.""" """Returns None when .git directory doesn't exist anywhere."""
import hermes_cli.banner as banner 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.parent.mkdir(parents=True, exist_ok=True)
fake_banner.touch() fake_banner.touch()
original = banner.__file__ monkeypatch.setattr(banner, "__file__", str(fake_banner))
try: monkeypatch.setenv("HERMES_HOME", str(tmp_path))
banner.__file__ = str(fake_banner) with patch("hermes_cli.banner.subprocess.run") as mock_run:
with patch("hermes_cli.banner.os.getenv", return_value=str(tmp_path)): result = banner.check_for_updates()
with patch("hermes_cli.banner.subprocess.run") as mock_run: assert result is None
result = banner.check_for_updates() mock_run.assert_not_called()
assert result is None
mock_run.assert_not_called()
finally:
banner.__file__ = original
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.""" """Dev install: falls back to Path(__file__).parent.parent when HERMES_HOME has no git repo."""
import hermes_cli.banner as banner 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") pytest.skip("Not running from a git checkout")
# Point HERMES_HOME at a temp dir with no hermes-agent/.git # Point HERMES_HOME at a temp dir with no hermes-agent/.git
import tempfile monkeypatch.setenv("HERMES_HOME", str(tmp_path))
with tempfile.TemporaryDirectory() as td: with patch("hermes_cli.banner.subprocess.run") as mock_run:
with patch("hermes_cli.banner.os.getenv", return_value=td): mock_run.return_value = MagicMock(returncode=0, stdout="0\n")
with patch("hermes_cli.banner.subprocess.run") as mock_run: result = banner.check_for_updates()
mock_run.return_value = MagicMock(returncode=0, stdout="0\n") # Should have fallen back to project root and run git commands
result = banner.check_for_updates() assert mock_run.call_count >= 1
# Should have fallen back to project root and run git commands
assert mock_run.call_count >= 1
def test_prefetch_non_blocking(): def test_prefetch_non_blocking():

View File

View File

@@ -16,7 +16,7 @@ from unittest.mock import MagicMock
import pytest import pytest
# Ensure repo root is importable # 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: try:
from environments.agent_loop import ( from environments.agent_loop import (

View File

@@ -31,7 +31,7 @@ import pytest
# pytestmark removed — tests skip gracefully via OPENROUTER_API_KEY check on line 59 # pytestmark removed — tests skip gracefully via OPENROUTER_API_KEY check on line 59
# Ensure repo root is importable # 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: if str(_repo_root) not in sys.path:
sys.path.insert(0, str(_repo_root)) sys.path.insert(0, str(_repo_root))

View File

@@ -30,7 +30,7 @@ import pytest
import requests import requests
# Ensure repo root is importable # 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: if str(_repo_root) not in sys.path:
sys.path.insert(0, str(_repo_root)) sys.path.insert(0, str(_repo_root))

View File

@@ -23,7 +23,7 @@ logging.basicConfig(level=logging.DEBUG, stream=sys.stderr,
format="%(asctime)s [%(threadName)s] %(message)s") format="%(asctime)s [%(threadName)s] %(message)s")
log = logging.getLogger("interrupt_test") 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 unittest.mock import MagicMock, patch
from run_agent import AIAgent, IterationBudget from run_agent import AIAgent, IterationBudget

View File

@@ -122,7 +122,7 @@ class TestSourceLinesAreClamped:
@staticmethod @staticmethod
def _read_file(rel_path: str) -> str: def _read_file(rel_path: str) -> str:
import os 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: with open(os.path.join(base, rel_path)) as f:
return f.read() return f.read()

View File

@@ -13,7 +13,7 @@ from pathlib import Path
import pytest import pytest
# Ensure repo root is importable # 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 # Stub out optional heavy dependencies not installed in the test environment
sys.modules.setdefault("fire", types.SimpleNamespace(Fire=lambda *a, **k: None)) 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