Add comprehensive unit tests for all agent service components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
227
tests/test_sales_agent.py
Normal file
227
tests/test_sales_agent.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user