mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 23:11:37 +08:00
Compare commits
6 Commits
codex-port
...
sid/restru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcf64d5283 | ||
|
|
8bbafdf3a6 | ||
|
|
04ee0ec0bc | ||
|
|
b7903bca41 | ||
|
|
20e94662cc | ||
|
|
6ed3f9ca80 |
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
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",
|
"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(
|
||||||
@@ -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():
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,7 +27,7 @@ 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()
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ def test_check_for_updates_uses_cache(tmp_path):
|
|||||||
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,7 +49,7 @@ 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()
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ def test_check_for_updates_expired_cache(tmp_path):
|
|||||||
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.os.getenv", return_value=str(tmp_path)):
|
|
||||||
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
||||||
result = banner.check_for_updates()
|
result = banner.check_for_updates()
|
||||||
assert result is None
|
assert result is None
|
||||||
mock_run.assert_not_called()
|
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,9 +83,7 @@ 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.os.getenv", return_value=td):
|
|
||||||
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
with patch("hermes_cli.banner.subprocess.run") as mock_run:
|
||||||
mock_run.return_value = MagicMock(returncode=0, stdout="0\n")
|
mock_run.return_value = MagicMock(returncode=0, stdout="0\n")
|
||||||
result = banner.check_for_updates()
|
result = banner.check_for_updates()
|
||||||
|
|||||||
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
|
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 (
|
||||||
@@ -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))
|
||||||
|
|
||||||
@@ -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))
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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
Reference in New Issue
Block a user