mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: repair 57 failing CI tests across 14 files (#5823)
* fix: repair 57 failing CI tests across 14 files
Categories of fixes:
**Test isolation under xdist (-n auto):**
- test_hermes_logging: Strip ALL RotatingFileHandlers before each test
to prevent handlers leaked from other xdist workers from polluting counts
- test_code_execution: Force TERMINAL_ENV=local in setUp — prevents Modal
AuthError when another test leaks TERMINAL_ENV=modal
- test_timezone: Same TERMINAL_ENV fix for execute_code timezone tests
- test_codex_execution_paths: Mock _resolve_turn_agent_config to ensure
model resolution works regardless of xdist worker state
**Matrix adapter tests (nio not installed in CI):**
- Add _make_fake_nio() helper with real response classes for isinstance()
checks in production code
- Replace MagicMock(spec=nio.XxxResponse) with fake_nio instances
- Wrap production method calls with patch.dict('sys.modules', {'nio': ...})
so import nio succeeds in method bodies
- Use try/except instead of pytest.importorskip for nio.crypto imports
(importorskip can be fooled by MagicMock in sys.modules)
- test_matrix_voice: Skip entire file if nio is a mock, not just missing
**Stale test expectations:**
- test_cli_provider_resolution: _prompt_provider_choice now takes **kwargs
(default param added); mock getpass.getpass alongside input
- test_anthropic_oauth_flow: Mock getpass.getpass (code switched from input)
- test_gemini_provider: Mock models.dev + OpenRouter API lookups to test
hardcoded defaults without external API variance
- test_code_execution: Add notify_on_complete to blocked terminal params
- test_setup_openclaw_migration: Mock prompt_choice to select 'Full setup'
(new quick-setup path leads to _require_tty → sys.exit in CI)
- test_skill_manager_tool: Patch get_all_skills_dirs alongside SKILLS_DIR
so _find_skill searches tmp_path, not real ~/.hermes/skills/
**Missing attributes in object.__new__ test runners:**
- test_platform_reconnect: Add session_store to _make_runner()
- test_session_race_guard: Add hooks, _running_agents_ts, session_store,
delivery_router to _make_runner()
**Production bug fix (gateway/run.py):**
- Fix sentinel eviction race: _AGENT_PENDING_SENTINEL was immediately
evicted by the stale-detection logic because sentinels have no
get_activity_summary() method, causing _stale_idle=inf >= timeout.
Guard _should_evict with 'is not _AGENT_PENDING_SENTINEL'.
* fix: address remaining CI failures
- test_setup_openclaw_migration: Also mock _offer_launch_chat (called at
end of both quick and full setup paths)
- test_code_execution: Move TERMINAL_ENV=local to module level to protect
ALL test classes (TestEnvVarFiltering, TestExecuteCodeEdgeCases,
TestInterruptHandling, TestHeadTailTruncation) from xdist env leaks
- test_matrix: Use try/except for nio.crypto imports (importorskip can be
fooled by MagicMock in sys.modules under xdist)
This commit is contained in:
parent
f18a2aa634
commit
caded0a5e7
14 changed files with 208 additions and 69 deletions
|
|
@ -2,12 +2,54 @@
|
|||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, AsyncMock
|
||||
|
||||
from gateway.config import Platform, PlatformConfig
|
||||
|
||||
|
||||
def _make_fake_nio():
|
||||
"""Create a lightweight fake ``nio`` module with real response classes.
|
||||
|
||||
Tests that call production methods doing ``import nio`` / ``isinstance(resp, nio.XxxResponse)``
|
||||
need real classes (not MagicMock auto-attributes) to satisfy isinstance checks.
|
||||
Use via ``patch.dict("sys.modules", {"nio": _make_fake_nio()})``.
|
||||
"""
|
||||
mod = types.ModuleType("nio")
|
||||
|
||||
class RoomSendResponse:
|
||||
def __init__(self, event_id="$fake"):
|
||||
self.event_id = event_id
|
||||
|
||||
class RoomRedactResponse:
|
||||
pass
|
||||
|
||||
class RoomCreateResponse:
|
||||
def __init__(self, room_id="!fake:example.org"):
|
||||
self.room_id = room_id
|
||||
|
||||
class RoomInviteResponse:
|
||||
pass
|
||||
|
||||
class UploadResponse:
|
||||
def __init__(self, content_uri="mxc://example.org/fake"):
|
||||
self.content_uri = content_uri
|
||||
|
||||
# Minimal Api stub for code that checks nio.Api.RoomPreset
|
||||
class _Api:
|
||||
pass
|
||||
mod.Api = _Api
|
||||
|
||||
mod.RoomSendResponse = RoomSendResponse
|
||||
mod.RoomRedactResponse = RoomRedactResponse
|
||||
mod.RoomCreateResponse = RoomCreateResponse
|
||||
mod.RoomInviteResponse = RoomInviteResponse
|
||||
mod.UploadResponse = UploadResponse
|
||||
return mod
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Platform & Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -1450,7 +1492,10 @@ class TestMatrixEncryptedMedia:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_room_message_media_decrypts_encrypted_image_and_passes_local_path(self):
|
||||
from nio.crypto.attachments import encrypt_attachment
|
||||
try:
|
||||
from nio.crypto.attachments import encrypt_attachment
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pytest.skip("matrix-nio[e2e] required for encryption tests")
|
||||
|
||||
adapter = _make_adapter()
|
||||
adapter._user_id = "@bot:example.org"
|
||||
|
|
@ -1518,7 +1563,10 @@ class TestMatrixEncryptedMedia:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_room_message_media_decrypts_encrypted_voice_and_caches_audio(self):
|
||||
from nio.crypto.attachments import encrypt_attachment
|
||||
try:
|
||||
from nio.crypto.attachments import encrypt_attachment
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pytest.skip("matrix-nio[e2e] required for encryption tests")
|
||||
|
||||
adapter = _make_adapter()
|
||||
adapter._user_id = "@bot:example.org"
|
||||
|
|
@ -1587,7 +1635,10 @@ class TestMatrixEncryptedMedia:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_room_message_media_decrypts_encrypted_file_and_caches_document(self):
|
||||
from nio.crypto.attachments import encrypt_attachment
|
||||
try:
|
||||
from nio.crypto.attachments import encrypt_attachment
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
pytest.skip("matrix-nio[e2e] required for encryption tests")
|
||||
|
||||
adapter = _make_adapter()
|
||||
adapter._user_id = "@bot:example.org"
|
||||
|
|
@ -1883,14 +1934,15 @@ class TestMatrixReactions:
|
|||
@pytest.mark.asyncio
|
||||
async def test_send_reaction(self):
|
||||
"""_send_reaction should call room_send with m.reaction."""
|
||||
nio = pytest.importorskip("nio")
|
||||
fake_nio = _make_fake_nio()
|
||||
mock_client = MagicMock()
|
||||
mock_client.room_send = AsyncMock(
|
||||
return_value=MagicMock(spec=nio.RoomSendResponse)
|
||||
return_value=fake_nio.RoomSendResponse("$reaction1")
|
||||
)
|
||||
self.adapter._client = mock_client
|
||||
|
||||
result = await self.adapter._send_reaction("!room:ex", "$event1", "👍")
|
||||
with patch.dict("sys.modules", {"nio": fake_nio}):
|
||||
result = await self.adapter._send_reaction("!room:ex", "$event1", "👍")
|
||||
assert result is True
|
||||
mock_client.room_send.assert_called_once()
|
||||
args = mock_client.room_send.call_args
|
||||
|
|
@ -1902,7 +1954,8 @@ class TestMatrixReactions:
|
|||
@pytest.mark.asyncio
|
||||
async def test_send_reaction_no_client(self):
|
||||
self.adapter._client = None
|
||||
result = await self.adapter._send_reaction("!room:ex", "$ev", "👍")
|
||||
with patch.dict("sys.modules", {"nio": _make_fake_nio()}):
|
||||
result = await self.adapter._send_reaction("!room:ex", "$ev", "👍")
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -1999,21 +2052,23 @@ class TestMatrixRedaction:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redact_message(self):
|
||||
nio = pytest.importorskip("nio")
|
||||
fake_nio = _make_fake_nio()
|
||||
mock_client = MagicMock()
|
||||
mock_client.room_redact = AsyncMock(
|
||||
return_value=MagicMock(spec=nio.RoomRedactResponse)
|
||||
return_value=fake_nio.RoomRedactResponse()
|
||||
)
|
||||
self.adapter._client = mock_client
|
||||
|
||||
result = await self.adapter.redact_message("!room:ex", "$ev1", "oops")
|
||||
with patch.dict("sys.modules", {"nio": fake_nio}):
|
||||
result = await self.adapter.redact_message("!room:ex", "$ev1", "oops")
|
||||
assert result is True
|
||||
mock_client.room_redact.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redact_no_client(self):
|
||||
self.adapter._client = None
|
||||
result = await self.adapter.redact_message("!room:ex", "$ev1")
|
||||
with patch.dict("sys.modules", {"nio": _make_fake_nio()}):
|
||||
result = await self.adapter.redact_message("!room:ex", "$ev1")
|
||||
assert result is False
|
||||
|
||||
|
||||
|
|
@ -2027,33 +2082,35 @@ class TestMatrixRoomManagement:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_room(self):
|
||||
nio = pytest.importorskip("nio")
|
||||
mock_resp = MagicMock(spec=nio.RoomCreateResponse)
|
||||
mock_resp.room_id = "!new:example.org"
|
||||
fake_nio = _make_fake_nio()
|
||||
mock_resp = fake_nio.RoomCreateResponse(room_id="!new:example.org")
|
||||
mock_client = MagicMock()
|
||||
mock_client.room_create = AsyncMock(return_value=mock_resp)
|
||||
self.adapter._client = mock_client
|
||||
|
||||
room_id = await self.adapter.create_room(name="Test Room", topic="A test")
|
||||
with patch.dict("sys.modules", {"nio": fake_nio}):
|
||||
room_id = await self.adapter.create_room(name="Test Room", topic="A test")
|
||||
assert room_id == "!new:example.org"
|
||||
assert "!new:example.org" in self.adapter._joined_rooms
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invite_user(self):
|
||||
nio = pytest.importorskip("nio")
|
||||
fake_nio = _make_fake_nio()
|
||||
mock_client = MagicMock()
|
||||
mock_client.room_invite = AsyncMock(
|
||||
return_value=MagicMock(spec=nio.RoomInviteResponse)
|
||||
return_value=fake_nio.RoomInviteResponse()
|
||||
)
|
||||
self.adapter._client = mock_client
|
||||
|
||||
result = await self.adapter.invite_user("!room:ex", "@user:ex")
|
||||
with patch.dict("sys.modules", {"nio": fake_nio}):
|
||||
result = await self.adapter.invite_user("!room:ex", "@user:ex")
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_room_no_client(self):
|
||||
self.adapter._client = None
|
||||
result = await self.adapter.create_room()
|
||||
with patch.dict("sys.modules", {"nio": _make_fake_nio()}):
|
||||
result = await self.adapter.create_room()
|
||||
assert result is None
|
||||
|
||||
|
||||
|
|
@ -2099,28 +2156,28 @@ class TestMatrixMessageTypes:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_emote(self):
|
||||
nio = pytest.importorskip("nio")
|
||||
fake_nio = _make_fake_nio()
|
||||
mock_client = MagicMock()
|
||||
mock_resp = MagicMock(spec=nio.RoomSendResponse)
|
||||
mock_resp.event_id = "$emote1"
|
||||
mock_resp = fake_nio.RoomSendResponse(event_id="$emote1")
|
||||
mock_client.room_send = AsyncMock(return_value=mock_resp)
|
||||
self.adapter._client = mock_client
|
||||
|
||||
result = await self.adapter.send_emote("!room:ex", "waves hello")
|
||||
with patch.dict("sys.modules", {"nio": fake_nio}):
|
||||
result = await self.adapter.send_emote("!room:ex", "waves hello")
|
||||
assert result.success is True
|
||||
call_args = mock_client.room_send.call_args[0]
|
||||
assert call_args[2]["msgtype"] == "m.emote"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_notice(self):
|
||||
nio = pytest.importorskip("nio")
|
||||
fake_nio = _make_fake_nio()
|
||||
mock_client = MagicMock()
|
||||
mock_resp = MagicMock(spec=nio.RoomSendResponse)
|
||||
mock_resp.event_id = "$notice1"
|
||||
mock_resp = fake_nio.RoomSendResponse(event_id="$notice1")
|
||||
mock_client.room_send = AsyncMock(return_value=mock_resp)
|
||||
self.adapter._client = mock_client
|
||||
|
||||
result = await self.adapter.send_notice("!room:ex", "System message")
|
||||
with patch.dict("sys.modules", {"nio": fake_nio}):
|
||||
result = await self.adapter.send_notice("!room:ex", "System message")
|
||||
assert result.success is True
|
||||
call_args = mock_client.room_send.call_args[0]
|
||||
assert call_args[2]["msgtype"] == "m.notice"
|
||||
|
|
@ -2128,5 +2185,6 @@ class TestMatrixMessageTypes:
|
|||
@pytest.mark.asyncio
|
||||
async def test_send_emote_empty_text(self):
|
||||
self.adapter._client = MagicMock()
|
||||
result = await self.adapter.send_emote("!room:ex", "")
|
||||
with patch.dict("sys.modules", {"nio": _make_fake_nio()}):
|
||||
result = await self.adapter.send_emote("!room:ex", "")
|
||||
assert result.success is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue