Files
hermes-agent/tests/run_agent/test_exit_cleanup_interrupt.py
Siddharth Balyan f3006ebef9 refactor(tests): re-architect tests + fix CI failures (#5946)
* refactor: re-architect tests to mirror the codebase

* Update tests.yml

* fix: add missing tool_error imports after registry refactor

* 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.

* 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.

* fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock
2026-04-07 17:19:07 -07:00

74 lines
2.8 KiB
Python

"""Tests for KeyboardInterrupt handling in exit cleanup paths.
``except Exception`` does not catch ``KeyboardInterrupt`` (which inherits
from ``BaseException``). A second Ctrl+C during exit cleanup must not
abort remaining cleanup steps. These tests exercise the actual production
code paths — not a copy of the try/except pattern.
"""
import atexit
import weakref
from unittest.mock import MagicMock, patch, call
import pytest
class TestCronJobCleanup:
"""cron/scheduler.py — end_session + close in the finally block."""
def test_keyboard_interrupt_in_end_session_does_not_skip_close(self):
"""If end_session raises KeyboardInterrupt, close() must still run."""
mock_db = MagicMock()
mock_db.end_session.side_effect = KeyboardInterrupt
from cron import scheduler
job = {
"id": "test-job-1",
"name": "test cleanup",
"prompt": "hello",
"schedule": "0 9 * * *",
"model": "test/model",
}
with patch("hermes_state.SessionDB", return_value=mock_db), \
patch.object(scheduler, "_build_job_prompt", return_value="hello"), \
patch.object(scheduler, "_resolve_origin", return_value=None), \
patch.object(scheduler, "_resolve_delivery_target", return_value=None), \
patch("dotenv.load_dotenv", return_value=None), \
patch("run_agent.AIAgent") as MockAgent:
# Make the agent raise immediately so we hit the finally block
MockAgent.return_value.run_conversation.side_effect = RuntimeError("boom")
scheduler.run_job(job)
mock_db.end_session.assert_called_once()
mock_db.close.assert_called_once()
def test_keyboard_interrupt_in_close_does_not_propagate(self):
"""If close() raises KeyboardInterrupt, it must not escape run_job."""
mock_db = MagicMock()
mock_db.close.side_effect = KeyboardInterrupt
from cron import scheduler
job = {
"id": "test-job-2",
"name": "test close interrupt",
"prompt": "hello",
"schedule": "0 9 * * *",
"model": "test/model",
}
with patch("hermes_state.SessionDB", return_value=mock_db), \
patch.object(scheduler, "_build_job_prompt", return_value="hello"), \
patch.object(scheduler, "_resolve_origin", return_value=None), \
patch.object(scheduler, "_resolve_delivery_target", return_value=None), \
patch("dotenv.load_dotenv", return_value=None), \
patch("run_agent.AIAgent") as MockAgent:
MockAgent.return_value.run_conversation.side_effect = RuntimeError("boom")
# Must not raise
scheduler.run_job(job)
mock_db.end_session.assert_called_once()
mock_db.close.assert_called_once()