From e0a6b307a21c46e12706c291a8040d18689780b3 Mon Sep 17 00:00:00 2001 From: agentson Date: Sat, 14 Feb 2026 23:56:04 +0900 Subject: [PATCH] fix: add error handling to evolution loop telegram notification Wrap evolution notification in try/except so telegram failures don't crash the evolution loop. Add integration tests for market close flow. Co-Authored-By: Claude Opus 4.6 --- src/main.py | 17 ++++---- tests/test_main.py | 100 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/src/main.py b/src/main.py index 5a7f711..f0e8f17 100644 --- a/src/main.py +++ b/src/main.py @@ -832,13 +832,16 @@ async def _run_evolution_loop( logger.info("Evolution loop skipped on %s (no actionable failures)", market_date) return - await telegram.send_message( - "Evolution Update\n" - f"Date: {market_date}\n" - f"PR: {pr_info.get('title', 'N/A')}\n" - f"Branch: {pr_info.get('branch', 'N/A')}\n" - f"Status: {pr_info.get('status', 'N/A')}" - ) + try: + await telegram.send_message( + "Evolution Update\n" + f"Date: {market_date}\n" + f"PR: {pr_info.get('title', 'N/A')}\n" + f"Branch: {pr_info.get('branch', 'N/A')}\n" + f"Status: {pr_info.get('status', 'N/A')}" + ) + except Exception as exc: + logger.warning("Evolution notification failed on %s: %s", market_date, exc) async def run(settings: Settings) -> None: diff --git a/tests/test_main.py b/tests/test_main.py index 378077a..9afc36f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1304,6 +1304,82 @@ async def test_handle_market_close_without_lessons_stores_once() -> None: assert reviewer.store_scorecard_in_context.call_count == 1 +@pytest.mark.asyncio +async def test_handle_market_close_triggers_evolution_for_us() -> None: + telegram = MagicMock() + telegram.notify_market_close = AsyncMock() + telegram.send_message = AsyncMock() + + context_aggregator = MagicMock() + reviewer = MagicMock() + reviewer.generate_scorecard.return_value = DailyScorecard( + date="2026-02-14", + market="US", + total_decisions=2, + buys=1, + sells=1, + holds=0, + total_pnl=3.0, + win_rate=50.0, + avg_confidence=80.0, + scenario_match_rate=100.0, + ) + reviewer.generate_lessons = AsyncMock(return_value=[]) + + evolution_optimizer = MagicMock() + evolution_optimizer.evolve = AsyncMock(return_value=None) + + await _handle_market_close( + market_code="US", + market_name="United States", + market_timezone=UTC, + telegram=telegram, + context_aggregator=context_aggregator, + daily_reviewer=reviewer, + evolution_optimizer=evolution_optimizer, + ) + + evolution_optimizer.evolve.assert_called_once() + + +@pytest.mark.asyncio +async def test_handle_market_close_skips_evolution_for_kr() -> None: + telegram = MagicMock() + telegram.notify_market_close = AsyncMock() + telegram.send_message = AsyncMock() + + context_aggregator = MagicMock() + reviewer = MagicMock() + reviewer.generate_scorecard.return_value = DailyScorecard( + date="2026-02-14", + market="KR", + total_decisions=1, + buys=1, + sells=0, + holds=0, + total_pnl=1.0, + win_rate=100.0, + avg_confidence=90.0, + scenario_match_rate=100.0, + ) + reviewer.generate_lessons = AsyncMock(return_value=[]) + + evolution_optimizer = MagicMock() + evolution_optimizer.evolve = AsyncMock(return_value=None) + + await _handle_market_close( + market_code="KR", + market_name="Korea", + market_timezone=UTC, + telegram=telegram, + context_aggregator=context_aggregator, + daily_reviewer=reviewer, + evolution_optimizer=evolution_optimizer, + ) + + evolution_optimizer.evolve.assert_not_called() + + def test_run_context_scheduler_invokes_scheduler() -> None: """Scheduler helper should call run_if_due with provided datetime.""" scheduler = MagicMock() @@ -1354,3 +1430,27 @@ async def test_run_evolution_loop_notifies_when_pr_generated() -> None: optimizer.evolve.assert_called_once() telegram.send_message.assert_called_once() + + +@pytest.mark.asyncio +async def test_run_evolution_loop_notification_error_is_ignored() -> None: + optimizer = MagicMock() + optimizer.evolve = AsyncMock( + return_value={ + "title": "[Evolution] New strategy: v20260214_050000", + "branch": "evolution/v20260214_050000", + "status": "ready_for_review", + } + ) + telegram = MagicMock() + telegram.send_message = AsyncMock(side_effect=RuntimeError("telegram down")) + + await _run_evolution_loop( + evolution_optimizer=optimizer, + telegram=telegram, + market_code="US", + market_date="2026-02-14", + ) + + optimizer.evolve.assert_called_once() + telegram.send_message.assert_called_once()