Files
odoo-ai/tests/test_sales_agent.py
2026-05-20 04:00:45 +00:00

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