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

219 lines
8.8 KiB
Python

"""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