From d0f84c0964063c74cd588fe695fe6bb2044586ee Mon Sep 17 00:00:00 2001 From: 0xbyt4 <35742124+0xbyt4@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:06:34 +0300 Subject: [PATCH 1/2] fix: log exceptions instead of silently swallowing in cron scheduler Two 'except Exception: pass' blocks silently hide failures: - mirror_to_session failure: user's message never gets mirrored, no trace - config.yaml parse failure: wrong model used silently Replace with logger.warning so failures are visible in logs. --- cron/scheduler.py | 8 ++--- tests/cron/test_scheduler.py | 68 ++++++++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/cron/scheduler.py b/cron/scheduler.py index 4dfc91e09..473099cea 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -137,8 +137,8 @@ def _deliver_result(job: dict, content: str) -> None: try: from gateway.mirror import mirror_to_session mirror_to_session(platform_name, chat_id, content, source_label="cron") - except Exception: - pass + except Exception as e: + logger.warning("Job '%s': mirror_to_session failed: %s", job["id"], e) def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: @@ -189,8 +189,8 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: model = _model_cfg elif isinstance(_model_cfg, dict): model = _model_cfg.get("default", model) - except Exception: - pass + except Exception as e: + logger.warning("Job '%s': failed to load config.yaml, using defaults: %s", job_id, e) # Reasoning config from env or config.yaml reasoning_config = None diff --git a/tests/cron/test_scheduler.py b/tests/cron/test_scheduler.py index 33096c49b..4a4567277 100644 --- a/tests/cron/test_scheduler.py +++ b/tests/cron/test_scheduler.py @@ -1,8 +1,12 @@ -"""Tests for cron/scheduler.py — origin resolution and delivery routing.""" +"""Tests for cron/scheduler.py — origin resolution, delivery routing, and error logging.""" + +import asyncio +import logging +from unittest.mock import patch, MagicMock, AsyncMock import pytest -from cron.scheduler import _resolve_origin +from cron.scheduler import _resolve_origin, _deliver_result, run_job class TestResolveOrigin: @@ -36,3 +40,63 @@ class TestResolveOrigin: def test_empty_origin(self): job = {"origin": {}} assert _resolve_origin(job) is None + + +class TestDeliverResultMirrorLogging: + """Verify that mirror_to_session failures are logged, not silently swallowed.""" + + def test_mirror_failure_is_logged(self, caplog): + """When mirror_to_session raises, a warning should be logged.""" + from gateway.config import Platform + + pconfig = MagicMock() + pconfig.enabled = True + mock_cfg = MagicMock() + mock_cfg.platforms = {Platform.TELEGRAM: pconfig} + + async def fake_send(*args, **kwargs): + return None + + with patch("gateway.config.load_gateway_config", return_value=mock_cfg), \ + patch("tools.send_message_tool._send_to_platform", new=fake_send), \ + patch("gateway.mirror.mirror_to_session", side_effect=ConnectionError("network down")): + job = { + "id": "test-job", + "deliver": "origin", + "origin": {"platform": "telegram", "chat_id": "123"}, + } + with caplog.at_level(logging.WARNING, logger="cron.scheduler"): + _deliver_result(job, "Hello!") + + assert any("mirror_to_session failed" in r.message for r in caplog.records), \ + f"Expected 'mirror_to_session failed' warning in logs, got: {[r.message for r in caplog.records]}" + + +class TestRunJobConfigLogging: + """Verify that config.yaml parse failures are logged, not silently swallowed.""" + + def test_bad_config_yaml_is_logged(self, caplog, tmp_path): + """When config.yaml is malformed, a warning should be logged.""" + # Create a bad config.yaml + bad_yaml = tmp_path / "config.yaml" + bad_yaml.write_text("invalid: yaml: [[[bad") + + job = { + "id": "test-job", + "name": "test", + "prompt": "hello", + } + + with patch("cron.scheduler._hermes_home", tmp_path), \ + patch("cron.scheduler._resolve_origin", return_value=None), \ + patch("dotenv.load_dotenv"), \ + patch("run_agent.AIAgent") as mock_agent_cls: + mock_agent = MagicMock() + mock_agent.run.return_value = ("output doc", "final response") + mock_agent_cls.return_value = mock_agent + + with caplog.at_level(logging.WARNING, logger="cron.scheduler"): + run_job(job) + + assert any("failed to load config.yaml" in r.message for r in caplog.records), \ + f"Expected 'failed to load config.yaml' warning in logs, got: {[r.message for r in caplog.records]}" From 0c3253a4859cde2ef4972310e2763a25a84c07c0 Mon Sep 17 00:00:00 2001 From: 0xbyt4 <35742124+0xbyt4@users.noreply.github.com> Date: Mon, 9 Mar 2026 00:20:19 +0300 Subject: [PATCH 2/2] fix: mock asyncio.run in mirror test to prevent event loop destruction asyncio.run() closes the event loop after execution, which breaks subsequent tests using asyncio.get_event_loop() (test_send_image_file). --- tests/cron/test_scheduler.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/cron/test_scheduler.py b/tests/cron/test_scheduler.py index 4a4567277..6b817a28a 100644 --- a/tests/cron/test_scheduler.py +++ b/tests/cron/test_scheduler.py @@ -54,11 +54,8 @@ class TestDeliverResultMirrorLogging: mock_cfg = MagicMock() mock_cfg.platforms = {Platform.TELEGRAM: pconfig} - async def fake_send(*args, **kwargs): - return None - with patch("gateway.config.load_gateway_config", return_value=mock_cfg), \ - patch("tools.send_message_tool._send_to_platform", new=fake_send), \ + patch("asyncio.run", return_value=None), \ patch("gateway.mirror.mirror_to_session", side_effect=ConnectionError("network down")): job = { "id": "test-job",