Spaces:
Paused
Paused
| """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 | |
| # Ensure project root is importable | |
| PROJECT_ROOT = Path(__file__).parent.parent | |
| if str(PROJECT_ROOT) not in sys.path: | |
| sys.path.insert(0, str(PROJECT_ROOT)) | |
| 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)) | |
| # Reset plugin singleton so tests don't leak plugins from ~/.hermes/plugins/ | |
| try: | |
| import hermes_cli.plugins as _plugins_mod | |
| monkeypatch.setattr(_plugins_mod, "_plugin_manager", None) | |
| except Exception: | |
| pass | |
| # Tests should not inherit the agent's current gateway/messaging surface. | |
| # Individual tests that need gateway behavior set these explicitly. | |
| 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) | |
| # Avoid making real calls during tests if this key is set in the env files | |
| monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) | |
| def tmp_dir(tmp_path): | |
| """Provide a temporary directory that is cleaned up automatically.""" | |
| return tmp_path | |
| 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": [], | |
| } | |
| # ββ Global test timeout βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Kill any individual test that takes longer than 30 seconds. | |
| # Prevents hanging tests (subprocess spawns, blocking I/O) from stalling the | |
| # entire test suite. | |
| def _timeout_handler(signum, frame): | |
| raise TimeoutError("Test exceeded 30 second timeout") | |
| 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) | |
| 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) | |