"""Tests for marketing_intelligence.agent_workflows.monitor_agent.""" import asyncio from collections.abc import Coroutine from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from marketing_intelligence.agent_workflows.monitor_agent import ( ALLOWED_TOOLS, run_monitor_agent, ) def _run(coro: Coroutine[Any, Any, Any]) -> Any: return asyncio.run(coro) def _make_post(post_id: str = "p1", campaign_config_key: str = "ck1_ak1") -> MagicMock: p = MagicMock() p.post_id = post_id p.url = f"https://t.co/{post_id}" p.sound_id = None p.campaign_config_key = campaign_config_key return p def _patch_monitor( campaign_ids: list, posts: list | None = None, checkpoint: dict | None = None, run_agent_result: str = "done", run_agent_exc: Exception | None = None, prev_metrics: dict | None = None, ) -> Any: """Context manager stack for run_monitor_agent dependencies.""" import contextlib @contextlib.asynccontextmanager async def _ctx(): mock_backend = MagicMock() mock_backend.read_latest_metrics = AsyncMock(return_value=prev_metrics or {}) with ( patch( "marketing_intelligence.agent_workflows.monitor_agent.list_active_campaign_ids", return_value=campaign_ids, ), patch( "marketing_intelligence.agent_workflows.monitor_agent.list_posts", return_value=posts if posts is not None else [], ), patch( "marketing_intelligence.agent_workflows.monitor_agent.load_checkpoint", return_value=checkpoint or {}, ), patch( "marketing_intelligence.agent_workflows.monitor_agent.save_checkpoint", ), patch( "marketing_intelligence.agent_workflows.monitor_agent.clear_checkpoint", ), patch( "marketing_intelligence.agent_workflows.monitor_agent.get_backend", return_value=mock_backend, ), patch( "marketing_intelligence.agent_workflows.monitor_agent.settings" ) as mock_settings, ): mock_settings.agent_backend = "anthropic" mock_settings.anthropic_watch_model_id = "watch-model" mock_settings.bedrock_watch_model_id = "bedrock-watch" if run_agent_exc: run_agent_mock = AsyncMock(side_effect=run_agent_exc) else: run_agent_mock = AsyncMock(return_value=run_agent_result) with patch( "marketing_intelligence.agent_workflows.monitor_agent.run_agent", run_agent_mock, ) as mock_run: yield mock_run, mock_backend return _ctx() class TestRunMonitorAgentEarlyExits: def test_empty_campaigns_returns_immediately(self) -> None: async def _run_test() -> None: async with _patch_monitor(campaign_ids=[]) as (mock_run, _): await run_monitor_agent("run1") mock_run.assert_not_awaited() asyncio.run(_run_test()) def test_no_posts_skips_campaign(self) -> None: async def _run_test() -> None: async with _patch_monitor(campaign_ids=["c1"], posts=[]) as (mock_run, _): await run_monitor_agent("run1") mock_run.assert_not_awaited() asyncio.run(_run_test()) def test_all_posts_in_checkpoint_skips_campaign(self) -> None: post = _make_post("p1") async def _run_test() -> None: async with _patch_monitor( campaign_ids=["c1"], posts=[post], checkpoint={"completed": ["p1"]}, ) as (mock_run, _): await run_monitor_agent("run1") mock_run.assert_not_awaited() asyncio.run(_run_test()) class TestRunMonitorAgentFullRun: def test_calls_run_agent_with_campaign_run_id(self) -> None: post = _make_post("p1") async def _run_test() -> None: async with _patch_monitor(campaign_ids=["c1"], posts=[post]) as ( mock_run, _, ): await run_monitor_agent("run42") mock_run.assert_awaited_once() call_kwargs = mock_run.call_args[1] assert call_kwargs["run_id"] == "run42_c1" asyncio.run(_run_test()) def test_passes_allowed_tools_to_run_agent(self) -> None: post = _make_post("p1") async def _run_test() -> None: async with _patch_monitor(campaign_ids=["c1"], posts=[post]) as ( mock_run, _, ): await run_monitor_agent("run1") call_kwargs = mock_run.call_args[1] assert call_kwargs["allowed_tools"] == ALLOWED_TOOLS asyncio.run(_run_test()) def test_passes_watch_type_params(self) -> None: post = _make_post("p1", campaign_config_key="ck1_ak1") async def _run_test() -> None: async with _patch_monitor(campaign_ids=["c1"], posts=[post]) as ( mock_run, _, ): await run_monitor_agent("run1") params = mock_run.call_args[0][1] assert params["run_type"] == "watch" assert params["campaign_id"] == "c1" asyncio.run(_run_test()) def test_clears_checkpoint_on_success(self) -> None: post = _make_post("p1") async def _run_test() -> None: async with _patch_monitor(campaign_ids=["c1"], posts=[post]) as ( mock_run, _, ): with patch( "marketing_intelligence.agent_workflows.monitor_agent.clear_checkpoint" ) as mock_clear: await run_monitor_agent("run1") mock_clear.assert_called_once() asyncio.run(_run_test()) def test_run_agent_failure_saves_checkpoint(self) -> None: post = _make_post("p1") async def _run_test() -> None: async with _patch_monitor( campaign_ids=["c1"], posts=[post], run_agent_exc=RuntimeError("agent crash"), ) as (_, __): with patch( "marketing_intelligence.agent_workflows.monitor_agent.save_checkpoint" ) as mock_save: await run_monitor_agent("run1") mock_save.assert_called_once() ck_key, data = mock_save.call_args[0] assert "c1" in ck_key assert "p1" in data["completed"] asyncio.run(_run_test()) def test_partial_checkpoint_excludes_done_posts(self) -> None: p1 = _make_post("p1") p2 = _make_post("p2") async def _run_test() -> None: async with _patch_monitor( campaign_ids=["c1"], posts=[p1, p2], checkpoint={"completed": ["p1"]}, ) as (mock_run, _): await run_monitor_agent("run1") task_text = mock_run.call_args[0][0] assert "p2" in task_text assert "p1" not in task_text asyncio.run(_run_test()) def test_multiple_campaigns_each_processed(self) -> None: post_a = _make_post("a1", "ck1_ak1") post_b = _make_post("b1", "ck2_ak2") async def _run_test() -> None: async with _patch_monitor( campaign_ids=["c1", "c2"], posts=[post_a, post_b], ) as (mock_run, _): await run_monitor_agent("run1") assert mock_run.await_count == 2 asyncio.run(_run_test())