Add comprehensive unit tests for all agent service components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
200
tests/test_sales_tools.py
Normal file
200
tests/test_sales_tools.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""Unit tests for SalesTools."""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from agent_service.tools.sales_tools import SalesTools
|
||||
from agent_service.tools.odoo_client import WriteResult
|
||||
|
||||
|
||||
def _make_tools():
|
||||
odoo = MagicMock()
|
||||
odoo.search_read = AsyncMock(return_value=[])
|
||||
odoo.write = AsyncMock(return_value=WriteResult(
|
||||
success=True, model='', record_id=None, action='write'))
|
||||
odoo.create = AsyncMock(return_value=WriteResult(
|
||||
success=True, model='', record_id=42, action='create'))
|
||||
odoo.call = AsyncMock(return_value=True)
|
||||
return SalesTools(odoo)
|
||||
|
||||
|
||||
# ── get_sales_orders ──────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sales_orders_default():
|
||||
t = _make_tools()
|
||||
result = await t.get_sales_orders()
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('state', '=', 'sale') in domain
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sales_orders_with_partner():
|
||||
t = _make_tools()
|
||||
await t.get_sales_orders(partner_id=5)
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('partner_id', '=', 5) in domain
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sales_orders_with_date_range():
|
||||
t = _make_tools()
|
||||
await t.get_sales_orders(date_from='2026-01-01', date_to='2026-01-31')
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('date_order', '>=', '2026-01-01') in domain
|
||||
assert ('date_order', '<=', '2026-01-31') in domain
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sales_orders_custom_state():
|
||||
t = _make_tools()
|
||||
await t.get_sales_orders(state='draft')
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('state', '=', 'draft') in domain
|
||||
|
||||
|
||||
# ── get_quotations ────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_quotations_default():
|
||||
t = _make_tools()
|
||||
result = await t.get_quotations()
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('state', 'in', ['draft', 'sent']) in domain
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_quotations_with_partner():
|
||||
t = _make_tools()
|
||||
await t.get_quotations(partner_id=7)
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('partner_id', '=', 7) in domain
|
||||
|
||||
|
||||
# ── get_sales_summary ─────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sales_summary_empty():
|
||||
t = _make_tools()
|
||||
result = await t.get_sales_summary()
|
||||
assert result['order_count'] == 0
|
||||
assert result['total_revenue'] == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sales_summary_aggregates():
|
||||
t = _make_tools()
|
||||
t._o.search_read = AsyncMock(return_value=[
|
||||
{'amount_total': 1000.0, 'user_id': [1, 'Alice']},
|
||||
{'amount_total': 2000.0, 'user_id': [1, 'Alice']},
|
||||
{'amount_total': 500.0, 'user_id': [2, 'Bob']},
|
||||
])
|
||||
result = await t.get_sales_summary()
|
||||
assert result['order_count'] == 3
|
||||
assert result['total_revenue'] == 3500.0
|
||||
assert 'by_sales_rep' in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sales_summary_by_rep():
|
||||
t = _make_tools()
|
||||
t._o.search_read = AsyncMock(return_value=[
|
||||
{'amount_total': 5000.0, 'user_id': [1, 'Alice']},
|
||||
{'amount_total': 3000.0, 'user_id': [2, 'Bob']},
|
||||
])
|
||||
result = await t.get_sales_summary()
|
||||
reps = result['by_sales_rep']
|
||||
assert len(reps) == 2
|
||||
assert reps[0]['total'] >= reps[1]['total'] # sorted descending
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sales_summary_with_dates():
|
||||
t = _make_tools()
|
||||
await t.get_sales_summary(date_from='2026-01-01', date_to='2026-01-31')
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('date_order', '>=', '2026-01-01') in domain
|
||||
|
||||
|
||||
# ── get_customer_orders ───────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_customer_orders():
|
||||
t = _make_tools()
|
||||
t._o.search_read = AsyncMock(return_value=[
|
||||
{'id': 1, 'name': 'SO/001', 'state': 'sale'}
|
||||
])
|
||||
result = await t.get_customer_orders(partner_id=5)
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('partner_id', '=', 5) in domain
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
# ── confirm_quotation ─────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirm_quotation_success():
|
||||
t = _make_tools()
|
||||
result = await t.confirm_quotation(order_id=3)
|
||||
assert result is True
|
||||
t._o.call.assert_awaited_once_with('sale.order', 'action_confirm', [[3]])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirm_quotation_handles_exception():
|
||||
t = _make_tools()
|
||||
t._o.call = AsyncMock(side_effect=Exception('access denied'))
|
||||
result = await t.confirm_quotation(order_id=3)
|
||||
assert result is False
|
||||
|
||||
|
||||
# ── update_order_note ─────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_order_note_success():
|
||||
t = _make_tools()
|
||||
result = await t.update_order_note(order_id=1, note='Special instructions')
|
||||
assert result is True
|
||||
t._o.write.assert_awaited_once_with('sale.order', [1], {'note': 'Special instructions'})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_order_note_failure():
|
||||
t = _make_tools()
|
||||
t._o.write = AsyncMock(return_value=WriteResult(
|
||||
success=False, model='', record_id=None, action='write'))
|
||||
result = await t.update_order_note(order_id=1, note='note')
|
||||
assert result is False
|
||||
|
||||
|
||||
# ── flag_for_review ───────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flag_for_review():
|
||||
t = _make_tools()
|
||||
result = await t.flag_for_review('sale.order', 1, 'Large discount applied')
|
||||
assert result is True
|
||||
call_args = t._o.call.call_args[0]
|
||||
assert 'message_post' in str(call_args)
|
||||
body = call_args[3]['body']
|
||||
assert '[AI FLAG - MEDIUM]' in body
|
||||
assert 'Large discount applied' in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flag_for_review_high_severity():
|
||||
t = _make_tools()
|
||||
await t.flag_for_review('sale.order', 1, 'reason', severity='high')
|
||||
body = t._o.call.call_args[0][3]['body']
|
||||
assert 'HIGH' in body
|
||||
|
||||
|
||||
# ── post_chatter_note ─────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_chatter_note():
|
||||
t = _make_tools()
|
||||
result = await t.post_chatter_note('sale.order', 1, 'Order confirmed by AI')
|
||||
assert result is True
|
||||
call_args = t._o.call.call_args[0]
|
||||
assert call_args[3]['body'] == 'Order confirmed by AI'
|
||||
Reference in New Issue
Block a user