| """Shared fixtures for the hermes-agent test suite.""" |
|
|
| import asyncio |
| import os |
| import signal |
| import sys |
| import tempfile |
| from pathlib import Path |
| from unittest.mock import patch |
|
|
| import pytest |
|
|
| |
| PROJECT_ROOT = Path(__file__).parent.parent |
| if str(PROJECT_ROOT) not in sys.path: |
| sys.path.insert(0, str(PROJECT_ROOT)) |
|
|
|
|
| @pytest.fixture(autouse=True) |
| def _isolate_hermes_home(tmp_path, monkeypatch): |
| """Redirect HERMES_HOME to a temp dir so tests never write to ~/.hermes/.""" |
| fake_home = tmp_path / "hermes_test" |
| fake_home.mkdir() |
| (fake_home / "sessions").mkdir() |
| (fake_home / "cron").mkdir() |
| (fake_home / "memories").mkdir() |
| (fake_home / "skills").mkdir() |
| monkeypatch.setenv("HERMES_HOME", str(fake_home)) |
| |
| try: |
| import hermes_cli.plugins as _plugins_mod |
| monkeypatch.setattr(_plugins_mod, "_plugin_manager", None) |
| except Exception: |
| pass |
| |
| |
| monkeypatch.delenv("HERMES_SESSION_PLATFORM", raising=False) |
| monkeypatch.delenv("HERMES_SESSION_CHAT_ID", raising=False) |
| monkeypatch.delenv("HERMES_SESSION_CHAT_NAME", raising=False) |
| monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False) |
|
|
|
|
| @pytest.fixture() |
| def tmp_dir(tmp_path): |
| """Provide a temporary directory that is cleaned up automatically.""" |
| return tmp_path |
|
|
|
|
| @pytest.fixture() |
| def mock_config(): |
| """Return a minimal hermes config dict suitable for unit tests.""" |
| return { |
| "model": "test/mock-model", |
| "toolsets": ["terminal", "file"], |
| "max_turns": 10, |
| "terminal": { |
| "backend": "local", |
| "cwd": "/tmp", |
| "timeout": 30, |
| }, |
| "compression": {"enabled": False}, |
| "memory": {"memory_enabled": False, "user_profile_enabled": False}, |
| "command_allowlist": [], |
| } |
|
|
|
|
| |
| |
| |
| |
|
|
| def _timeout_handler(signum, frame): |
| raise TimeoutError("Test exceeded 30 second timeout") |
|
|
| @pytest.fixture(autouse=True) |
| def _ensure_current_event_loop(request): |
| """Provide a default event loop for sync tests that call get_event_loop(). |
| |
| Python 3.11+ no longer guarantees a current loop for plain synchronous tests. |
| A number of gateway tests still use asyncio.get_event_loop().run_until_complete(...). |
| Ensure they always have a usable loop without interfering with pytest-asyncio's |
| own loop management for @pytest.mark.asyncio tests. |
| """ |
| if request.node.get_closest_marker("asyncio") is not None: |
| yield |
| return |
|
|
| try: |
| loop = asyncio.get_event_loop_policy().get_event_loop() |
| except RuntimeError: |
| loop = None |
|
|
| created = loop is None or loop.is_closed() |
| if created: |
| loop = asyncio.new_event_loop() |
| asyncio.set_event_loop(loop) |
|
|
| try: |
| yield |
| finally: |
| if created and loop is not None: |
| try: |
| loop.close() |
| finally: |
| asyncio.set_event_loop(None) |
|
|
|
|
| @pytest.fixture(autouse=True) |
| def _enforce_test_timeout(): |
| """Kill any individual test that takes longer than 30 seconds. |
| SIGALRM is Unix-only; skip on Windows.""" |
| if sys.platform == "win32": |
| yield |
| return |
| old = signal.signal(signal.SIGALRM, _timeout_handler) |
| signal.alarm(30) |
| yield |
| signal.alarm(0) |
| signal.signal(signal.SIGALRM, old) |
|
|