import pytest from unittest.mock import AsyncMock, MagicMock, patch from agent_service.agents.finance_agent import FinanceAgent, FINANCE_TOOLS from agent_service.agents.base_agent import AgentDirective, AgentReport, SweepReport from agent_service.llm.llm_types import LLMResponse @pytest.fixture def mock_ft(): ft = MagicMock() ft.get_overdue_invoices = AsyncMock(return_value=[ {'id': 1, 'partner_name': 'ACME Corp', 'amount_residual': 5000.0, 'days_overdue': 45}, {'id': 2, 'partner_name': 'BigCo', 'amount_residual': 120000.0, 'days_overdue': 95}, ]) ft.get_financial_summary = AsyncMock(return_value={ 'total_invoiced': 200000.0, 'collection_rate': 75.0, }) ft.get_invoices = AsyncMock(return_value=[]) ft.send_payment_reminder = AsyncMock(return_value=True) ft.flag_for_review = AsyncMock(return_value=True) ft.post_chatter_note = AsyncMock(return_value=True) ft.get_payment_history = AsyncMock(return_value=[]) return ft @pytest.fixture def agent(mock_odoo, mock_llm): a = FinanceAgent(odoo=mock_odoo, llm=mock_llm) return a def test_finance_agent_has_8_tools(): assert len(FINANCE_TOOLS) == 8 def test_finance_agent_name(): assert FinanceAgent.name == 'finance_agent' def test_finance_agent_hipaa_domain(): from agent_service.llm.llm_router import HIPAA_LOCKED_AGENTS assert 'finance_agent' in HIPAA_LOCKED_AGENTS @pytest.mark.asyncio async def test_plan_overdue_intent(agent, mock_ft): agent._ft = mock_ft directive = AgentDirective( directive_id='d1', user_id='1', intent='show overdue invoices', context={}, agent_name='finance_agent', ) plan = await agent._plan(directive) assert plan['fetch_overdue'] is True @pytest.mark.asyncio async def test_plan_send_reminders(agent, mock_ft): agent._ft = mock_ft directive = AgentDirective( directive_id='d2', user_id='1', intent='send payment reminders to overdue customers', context={}, agent_name='finance_agent', ) plan = await agent._plan(directive) assert plan['send_reminders'] is True @pytest.mark.asyncio async def test_gather_fetches_overdue(agent, mock_ft): agent._ft = mock_ft agent._plan_result = {'fetch_overdue': True, 'fetch_summary': False, 'fetch_invoices': False, 'partner_id': None, 'period': 'this_month'} data = await agent._gather({}) assert 'overdue' in data mock_ft.get_overdue_invoices.assert_awaited_once() @pytest.mark.asyncio async def test_reason_flags_90_day_invoices(agent, mock_ft): agent._ft = mock_ft agent._gathered_data = { 'overdue': [ {'id': 2, 'partner_name': 'BigCo', 'amount_residual': 120000.0, 'days_overdue': 95}, ], } analysis = await agent._reason({}) assert len(analysis['flags']) == 1 assert analysis['flags'][0]['invoice_id'] == 2 @pytest.mark.asyncio async def test_reason_escalates_high_overdue(agent, mock_ft): agent._ft = mock_ft agent._gathered_data = { 'overdue': [{'id': 1, 'amount_residual': 60000.0, 'days_overdue': 5, 'partner_name': 'X'}], } analysis = await agent._reason({}) assert any('60000' in e or '60' in e for e in analysis['escalations']) @pytest.mark.asyncio async def test_report_includes_summary(agent, mock_ft): agent._ft = mock_ft agent._gathered_data = {'summary': {'total_invoiced': 100000.0, 'collection_rate': 80.0}} agent._actions_taken = [] agent._escalations_list = [] agent._recommendations = [] report = await agent._report({'analysis': {'overdue_count': 0}}) assert isinstance(report, AgentReport) assert '100000' in report.summary or '80' in report.summary @pytest.mark.asyncio async def test_sweep_returns_findings(agent, mock_ft): agent._ft = mock_ft mock_ft.get_overdue_invoices = AsyncMock(return_value=[ {'id': 3, 'partner_name': 'OldDebt', 'amount_residual': 2000.0, 'days_overdue': 65}, ]) result = await agent.sweep() assert isinstance(result, SweepReport) assert len(result.findings) == 1 @pytest.mark.asyncio async def test_handle_peer_request_overdue_summary(agent, mock_ft): agent._ft = mock_ft result = await agent.handle_peer_request({'type': 'overdue_summary'}) assert 'overdue_count' in result @pytest.mark.asyncio async def test_dispatch_tool_unknown_raises(agent, mock_ft): agent._ft = mock_ft with pytest.raises(ValueError, match='Unknown tool'): await agent._dispatch_tool('nonexistent', {})