"""Unit tests for CrmAgent — plan, gather, reason, report, peer_bus, sweep.""" import pytest from unittest.mock import AsyncMock, MagicMock from agent_service.agents.crm_agent import CrmAgent, CRM_TOOLS from agent_service.agents.base_agent import AgentDirective, AgentReport, SweepReport def _directive(intent='', context=None): return AgentDirective( directive_id='crm-d1', user_id='1', intent=intent, context=context or {}, agent_name='crm_agent', ) def _make_agent(): agent = CrmAgent(odoo=MagicMock(), llm=MagicMock()) agent._ct = MagicMock() agent._ct.get_pipeline_summary = AsyncMock(return_value={ 'total_opportunities': 10, 'weighted_pipeline': 50000.0, }) agent._ct.get_leads = AsyncMock(return_value=[]) agent._ct.get_opportunities = AsyncMock(return_value=[]) agent._ct.get_won_lost_analysis = AsyncMock(return_value={'won_count': 5, 'lost_count': 2}) agent._ct.update_lead_stage = AsyncMock(return_value=True) agent._ct.assign_lead = AsyncMock(return_value=True) agent._ct.log_activity = AsyncMock(return_value=True) agent._ct.post_chatter_note = AsyncMock(return_value=True) return agent # ── Meta ──────────────────────────────────────────────────────────────────── def test_tool_count(): assert len(CRM_TOOLS) <= 8 def test_agent_name(): assert CrmAgent.name == 'crm_agent' # ── _plan ─────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_plan_pipeline_intent(): agent = _make_agent() plan = await agent._plan(_directive(intent='show pipeline summary')) assert plan['fetch_pipeline'] is True @pytest.mark.asyncio async def test_plan_leads_intent(): agent = _make_agent() plan = await agent._plan(_directive(intent='list my leads')) assert plan['fetch_leads'] is True @pytest.mark.asyncio async def test_plan_opportunities_intent(): agent = _make_agent() plan = await agent._plan(_directive(intent='show opportunities')) assert plan['fetch_opportunities'] is True @pytest.mark.asyncio async def test_plan_won_lost_intent(): agent = _make_agent() plan = await agent._plan(_directive(intent='show won and lost analysis')) assert plan['fetch_won_lost'] is True @pytest.mark.asyncio async def test_plan_propagates_user_id(): agent = _make_agent() plan = await agent._plan(_directive(context={'user_id': 7})) assert plan['user_id'] == 7 # ── _gather ───────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_gather_pipeline_fetched_by_default(): agent = _make_agent() ctx = {'plan': {'fetch_pipeline': True, 'fetch_leads': False, 'fetch_opportunities': False, 'fetch_won_lost': False, 'user_id': None}} data = await agent._gather(ctx) assert 'pipeline' in data agent._ct.get_pipeline_summary.assert_awaited_once() @pytest.mark.asyncio async def test_gather_leads_when_requested(): agent = _make_agent() agent._ct.get_leads = AsyncMock(return_value=[{'id': 1, 'name': 'Lead A'}]) ctx = {'plan': {'fetch_pipeline': False, 'fetch_leads': True, 'fetch_opportunities': False, 'fetch_won_lost': False, 'user_id': None}} data = await agent._gather(ctx) assert 'leads' in data @pytest.mark.asyncio async def test_gather_won_lost_when_requested(): agent = _make_agent() ctx = {'plan': {'fetch_pipeline': False, 'fetch_leads': False, 'fetch_opportunities': False, 'fetch_won_lost': True, 'user_id': None}} data = await agent._gather(ctx) assert 'won_lost' in data # ── _reason ───────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_reason_low_pipeline_escalates(): agent = _make_agent() agent._gathered_data = {'pipeline': {'weighted_pipeline': 500.0, 'total_opportunities': 2}} analysis = await agent._reason({}) assert len(analysis['escalations']) == 1 assert 'Low weighted pipeline' in analysis['escalations'][0] @pytest.mark.asyncio async def test_reason_healthy_pipeline_no_escalation(): agent = _make_agent() agent._gathered_data = {'pipeline': {'weighted_pipeline': 100000.0, 'total_opportunities': 20}} analysis = await agent._reason({}) assert analysis['escalations'] == [] # ── _report ───────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_report_includes_pipeline_stats(): agent = _make_agent() agent._gathered_data = { 'pipeline': {'total_opportunities': 15, 'weighted_pipeline': 75000.0} } agent._escalations_list = [] report = await agent._report({}) assert isinstance(report, AgentReport) assert '15' in report.summary @pytest.mark.asyncio async def test_report_includes_won_lost(): agent = _make_agent() agent._gathered_data = {'won_lost': {'won_count': 8, 'lost_count': 3}} agent._escalations_list = [] report = await agent._report({}) assert '8' in report.summary assert '3' in report.summary @pytest.mark.asyncio async def test_report_fallback_message(): agent = _make_agent() agent._gathered_data = {} agent._escalations_list = [] report = await agent._report({}) assert 'crm' in report.summary.lower() or 'complete' in report.summary.lower() # ── _dispatch_tool ─────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_dispatch_get_pipeline_summary(): agent = _make_agent() await agent._dispatch_tool('get_pipeline_summary', {}) agent._ct.get_pipeline_summary.assert_awaited_once() @pytest.mark.asyncio async def test_dispatch_update_lead_stage(): agent = _make_agent() await agent._dispatch_tool('update_lead_stage', {'lead_id': 1, 'stage_id': 3}) agent._ct.update_lead_stage.assert_awaited_once_with(lead_id=1, stage_id=3) @pytest.mark.asyncio async def test_dispatch_unknown_tool_raises(): agent = _make_agent() with pytest.raises(ValueError, match='Unknown tool'): await agent._dispatch_tool('nonexistent', {}) # ── handle_peer_request ────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_peer_pipeline_summary(): agent = _make_agent() result = await agent.handle_peer_request('pipeline_summary', {}, 'dir-1') assert 'total_opportunities' in result or 'weighted_pipeline' in result @pytest.mark.asyncio async def test_peer_opportunities(): agent = _make_agent() agent._ct.get_opportunities = AsyncMock(return_value=[{'id': 1}]) result = await agent.handle_peer_request('opportunities', {'user_id': 5}, 'dir-1') assert 'opportunities' in result @pytest.mark.asyncio async def test_peer_unknown_returns_error(): agent = _make_agent() result = await agent.handle_peer_request('bad_type', {}, 'dir-1') assert 'error' in result # ── sweep ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_sweep_low_pipeline_generates_finding(): agent = _make_agent() agent._ct.get_pipeline_summary = AsyncMock(return_value={'weighted_pipeline': 100.0}) result = await agent.sweep() assert isinstance(result, SweepReport) assert len(result.findings) == 1 assert result.findings[0]['type'] == 'low_pipeline' @pytest.mark.asyncio async def test_sweep_healthy_pipeline_no_findings(): agent = _make_agent() agent._ct.get_pipeline_summary = AsyncMock(return_value={'weighted_pipeline': 99999.0}) result = await agent.sweep() assert result.findings == [] @pytest.mark.asyncio async def test_sweep_handles_exception(): agent = _make_agent() agent._ct.get_pipeline_summary = AsyncMock(side_effect=Exception('network error')) result = await agent.sweep() assert isinstance(result, SweepReport) assert result.error is not None