feat(infra): add sweep coordinator, structured logging, test suite, and README
Sweep coordinator (Step 16): - SweepCoordinator runs all 8 agents in parallel with 60s per-agent / 300s total timeout - Aggregates findings, actions, errors into SweepCoordinatorResult - Registered in FastAPI lifespan; triggered via POST /sweep Structured logging (Step 18): - logging_utils/structured.py: JSONFormatter emitting ts/level/logger/msg + custom fields - log_directive_event() for structured directive lifecycle logging - push_to_loki() async Loki push (graceful no-op if LOKI_URL unset) - configure_logging() replaces root handler at startup Tests (Steps 17+19): - conftest.py: mock_odoo, mock_pool, mock_llm fixtures - test_tool_validator.py: 9 tests covering validation, coercion, hallucination stripping - test_llm_router.py: 6 tests covering local/cloud/hybrid modes and HIPAA enforcement - test_peer_bus.py: 6 tests covering registration, timeout, depth, circular detection - test_finance_agent.py: 10 tests covering all 6 steps + sweep + peer request - test_memory_manager.py: 3 tests covering context build + hard cap enforcement - test_dispatch_router.py: 3 tests covering dispatch, rate limit, health endpoint - test_odoo_client.py: 4 tests covering search_read, write result, unlink warning - test_e2e_dispatch.py: 2 E2E tests - full dispatch cycle + peer bus communication README (Step 20): architecture diagram, privacy modes, quick start, env vars, structure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
103
tests/test_e2e_dispatch.py
Normal file
103
tests/test_e2e_dispatch.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
End-to-end integration test: simulates a full dispatch cycle
|
||||
from HTTP request through MasterAgent to FinanceAgent response.
|
||||
|
||||
Uses in-process mocks for Odoo and LLM — no external services needed.
|
||||
"""
|
||||
import asyncio
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from agent_service.llm.llm_types import LLMResponse
|
||||
from agent_service.agents.peer_bus import PeerBus
|
||||
from agent_service.agents.finance_agent import FinanceAgent
|
||||
from agent_service.agents.master_agent import MasterAgent
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def llm_router():
|
||||
router = MagicMock()
|
||||
# First call: classify intent
|
||||
# Second+ calls: agent execution
|
||||
router.get_backend = AsyncMock(return_value='ollama')
|
||||
classify_response = LLMResponse(
|
||||
content='{"intent": "overdue_invoices", "agents": ["finance_agent"], "confidence": 0.95, "context": {}}',
|
||||
tool_calls=[], backend_used='ollama', model_used='llama3',
|
||||
tokens_in=50, tokens_out=80, latency_ms=200,
|
||||
)
|
||||
agent_response = LLMResponse(
|
||||
content='You have 2 overdue invoices totalling $125,000. Invoice #2 (BigCo, $120k) is 95 days overdue and has been flagged for review.',
|
||||
tool_calls=[], backend_used='ollama', model_used='llama3',
|
||||
tokens_in=200, tokens_out=150, latency_ms=800,
|
||||
)
|
||||
router.complete = AsyncMock(side_effect=[classify_response, agent_response])
|
||||
return router
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_finance_tools():
|
||||
ft = MagicMock()
|
||||
ft.get_overdue_invoices = AsyncMock(return_value=[
|
||||
{'id': 1, 'partner_name': 'ACME', '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.flag_for_review = AsyncMock(return_value=True)
|
||||
ft.send_payment_reminder = AsyncMock(return_value=True)
|
||||
ft.post_chatter_note = AsyncMock(return_value=True)
|
||||
ft.get_payment_history = AsyncMock(return_value=[])
|
||||
return ft
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_odoo():
|
||||
odoo = MagicMock()
|
||||
odoo.search_read = AsyncMock(return_value=[])
|
||||
odoo.write = AsyncMock()
|
||||
odoo.call = AsyncMock(return_value=True)
|
||||
odoo.ping = AsyncMock(return_value=True)
|
||||
return odoo
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_overdue_invoice_query(mock_odoo, llm_router, mock_finance_tools):
|
||||
peer_bus = PeerBus()
|
||||
finance_agent = FinanceAgent(odoo=mock_odoo, llm=llm_router, peer_bus=peer_bus)
|
||||
finance_agent._ft = mock_finance_tools
|
||||
peer_bus.register('finance_agent', finance_agent)
|
||||
|
||||
master = MasterAgent(
|
||||
odoo=mock_odoo,
|
||||
llm=llm_router,
|
||||
memory=None,
|
||||
peer_bus=peer_bus,
|
||||
registry=None,
|
||||
)
|
||||
|
||||
result = await asyncio.wait_for(
|
||||
master.handle_message(
|
||||
user_id='test_user_1',
|
||||
message='What are my overdue invoices?',
|
||||
context={},
|
||||
),
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.reply
|
||||
assert len(result.reply) > 10
|
||||
mock_finance_tools.get_overdue_invoices.assert_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_e2e_peer_bus_communication(mock_odoo, llm_router, mock_finance_tools):
|
||||
peer_bus = PeerBus()
|
||||
finance_agent = FinanceAgent(odoo=mock_odoo, llm=llm_router, peer_bus=peer_bus)
|
||||
finance_agent._ft = mock_finance_tools
|
||||
peer_bus.register('finance_agent', finance_agent)
|
||||
|
||||
from agent_service.agents.peer_bus import PeerResponse
|
||||
resp = await peer_bus.call('finance_agent', {'type': 'overdue_summary'})
|
||||
assert isinstance(resp, PeerResponse)
|
||||
assert resp.available is True
|
||||
assert 'overdue_count' in resp.data
|
||||
Reference in New Issue
Block a user