"""Unit tests for SalesAgent — plan, gather, reason, report, peer_bus, sweep.""" import datetime import pytest from unittest.mock import AsyncMock, MagicMock from agent_service.agents.sales_agent import SalesAgent, SALES_TOOLS from agent_service.agents.base_agent import AgentDirective, AgentReport, SweepReport def _directive(intent='', context=None): return AgentDirective( directive_id='sales-d1', user_id='1', intent=intent, context=context or {}, agent_name='sales_agent', ) def _make_agent(): agent = SalesAgent(odoo=MagicMock(), llm=MagicMock()) agent._st = MagicMock() agent._st.get_sales_summary = AsyncMock(return_value={ 'order_count': 12, 'total_revenue': 85000.0 }) agent._st.get_quotations = AsyncMock(return_value=[]) agent._st.get_sales_orders = AsyncMock(return_value=[]) agent._st.get_customer_orders = AsyncMock(return_value=[]) agent._st.confirm_quotation = AsyncMock(return_value=True) agent._st.update_order_note = AsyncMock(return_value=True) agent._st.flag_for_review = AsyncMock(return_value=True) agent._st.post_chatter_note = AsyncMock(return_value=True) return agent # ── Meta ──────────────────────────────────────────────────────────────────── def test_tool_count(): assert len(SALES_TOOLS) <= 8 def test_agent_name(): assert SalesAgent.name == 'sales_agent' # ── _plan ─────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_plan_summary_intent(): agent = _make_agent() plan = await agent._plan(_directive(intent='show sales summary')) assert plan['fetch_summary'] is True @pytest.mark.asyncio async def test_plan_revenue_intent(): agent = _make_agent() plan = await agent._plan(_directive(intent='what is our revenue this month')) assert plan['fetch_summary'] is True @pytest.mark.asyncio async def test_plan_quotation_intent(): agent = _make_agent() plan = await agent._plan(_directive(intent='list open quotations')) assert plan['fetch_quotations'] is True @pytest.mark.asyncio async def test_plan_order_intent(): agent = _make_agent() plan = await agent._plan(_directive(intent='show all orders')) assert plan['fetch_orders'] is True @pytest.mark.asyncio async def test_plan_propagates_partner_and_dates(): agent = _make_agent() plan = await agent._plan(_directive(context={'partner_id': 5, 'date_from': '2026-01-01'})) assert plan['partner_id'] == 5 assert plan['date_from'] == '2026-01-01' # ── _gather ───────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_gather_summary_by_default(): agent = _make_agent() ctx = {'plan': {'fetch_summary': True, 'fetch_quotations': False, 'fetch_orders': False, 'partner_id': None, 'date_from': None, 'date_to': None}} data = await agent._gather(ctx) assert 'summary' in data agent._st.get_sales_summary.assert_awaited_once() @pytest.mark.asyncio async def test_gather_quotations(): agent = _make_agent() agent._st.get_quotations = AsyncMock(return_value=[{'id': 1}]) ctx = {'plan': {'fetch_summary': False, 'fetch_quotations': True, 'fetch_orders': False, 'partner_id': None, 'date_from': None, 'date_to': None}} data = await agent._gather(ctx) assert 'quotations' in data @pytest.mark.asyncio async def test_gather_orders(): agent = _make_agent() agent._st.get_sales_orders = AsyncMock(return_value=[{'id': 1}]) ctx = {'plan': {'fetch_summary': False, 'fetch_quotations': False, 'fetch_orders': True, 'partner_id': None, 'date_from': None, 'date_to': None}} data = await agent._gather(ctx) assert 'orders' in data # ── _reason ───────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_reason_zero_revenue_escalates(): agent = _make_agent() agent._gathered_data = {'summary': {'order_count': 0, 'total_revenue': 0.0}} analysis = await agent._reason({}) assert len(analysis['escalations']) == 1 assert 'no confirmed sales' in analysis['escalations'][0].lower() @pytest.mark.asyncio async def test_reason_positive_revenue_no_escalation(): agent = _make_agent() agent._gathered_data = {'summary': {'order_count': 5, 'total_revenue': 10000.0}} analysis = await agent._reason({}) assert analysis['escalations'] == [] @pytest.mark.asyncio async def test_reason_no_summary_no_escalation(): agent = _make_agent() agent._gathered_data = {'quotations': []} analysis = await agent._reason({}) assert analysis['escalations'] == [] # ── _report ───────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_report_includes_revenue(): agent = _make_agent() agent._gathered_data = {'summary': {'order_count': 12, 'total_revenue': 85000.0}} agent._escalations_list = [] report = await agent._report({}) assert isinstance(report, AgentReport) assert '12' in report.summary assert '85000' 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 'complete' in report.summary.lower() or 'sales' in report.summary.lower() # ── _dispatch_tool ─────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_dispatch_get_sales_summary(): agent = _make_agent() await agent._dispatch_tool('get_sales_summary', {}) agent._st.get_sales_summary.assert_awaited_once() @pytest.mark.asyncio async def test_dispatch_confirm_quotation(): agent = _make_agent() await agent._dispatch_tool('confirm_quotation', {'order_id': 7}) agent._st.confirm_quotation.assert_awaited_once_with(order_id=7) @pytest.mark.asyncio async def test_dispatch_unknown_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_sales_summary(): agent = _make_agent() result = await agent.handle_peer_request('sales_summary', {}, 'dir-1') assert 'order_count' in result or 'total_revenue' in result @pytest.mark.asyncio async def test_peer_customer_orders(): agent = _make_agent() agent._st.get_customer_orders = AsyncMock(return_value=[{'id': 1}]) result = await agent.handle_peer_request('customer_orders', {'partner_id': 10}, 'dir-1') assert 'orders' in result assert len(result['orders']) == 1 @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_finds_expired_quotations(): agent = _make_agent() yesterday = str((datetime.date.today() - datetime.timedelta(days=1))) agent._st.get_quotations = AsyncMock(return_value=[ {'id': 1, 'partner_id': [5, 'ACME'], 'validity_date': yesterday} ]) result = await agent.sweep() assert isinstance(result, SweepReport) assert len(result.findings) == 1 assert result.findings[0]['type'] == 'expired_quotation' @pytest.mark.asyncio async def test_sweep_valid_quotations_no_findings(): agent = _make_agent() future = str((datetime.date.today() + datetime.timedelta(days=14))) agent._st.get_quotations = AsyncMock(return_value=[ {'id': 1, 'partner_id': [5, 'ACME'], 'validity_date': future} ]) result = await agent.sweep() assert result.findings == [] @pytest.mark.asyncio async def test_sweep_handles_exception(): agent = _make_agent() agent._st.get_quotations = AsyncMock(side_effect=Exception('network')) result = await agent.sweep() assert result.error is not None