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>
56 lines
2.0 KiB
Python
56 lines
2.0 KiB
Python
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from agent_service.tools.odoo_client import OdooClient, WriteResult
|
|
|
|
|
|
@pytest.fixture
|
|
def odoo():
|
|
client = OdooClient(url='http://localhost:8069', db='test', api_key='testkey')
|
|
return client
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_read_returns_list(odoo):
|
|
with patch.object(odoo, '_call', new=AsyncMock(return_value=[
|
|
{'id': 1, 'name': 'Invoice 1'},
|
|
])):
|
|
result = await odoo.search_read('account.move', [('state', '=', 'posted')], ['name'])
|
|
assert isinstance(result, list)
|
|
assert result[0]['id'] == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_returns_write_result(odoo):
|
|
with patch.object(odoo, '_call', new=AsyncMock(return_value=True)), \
|
|
patch.object(odoo, 'search_read', new=AsyncMock(return_value=[{'id': 1, 'name': 'test'}])):
|
|
result = await odoo.write('account.move', [1], {'state': 'posted'})
|
|
assert isinstance(result, WriteResult)
|
|
assert result.success is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_result_has_before_after(odoo):
|
|
before = [{'id': 1, 'amount_residual': 1000.0}]
|
|
after = [{'id': 1, 'amount_residual': 0.0}]
|
|
call_count = 0
|
|
|
|
async def mock_search_read(*args, **kwargs):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
return before if call_count == 1 else after
|
|
|
|
with patch.object(odoo, '_call', new=AsyncMock(return_value=True)), \
|
|
patch.object(odoo, 'search_read', new=mock_search_read):
|
|
result = await odoo.write('account.move', [1], {'amount_residual': 0})
|
|
assert result.before == before[0]
|
|
assert result.after == after[0]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unlink_logs_warning(odoo, caplog):
|
|
import logging
|
|
with patch.object(odoo, '_call', new=AsyncMock(return_value=True)):
|
|
with caplog.at_level(logging.WARNING):
|
|
await odoo.unlink('account.move', [99])
|
|
assert any('unlink' in r.message.lower() or '99' in r.message for r in caplog.records)
|