228 lines
9.0 KiB
Python
228 lines
9.0 KiB
Python
"""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
|