Add comprehensive unit tests for all agent service components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
238
tests/test_project_agent.py
Normal file
238
tests/test_project_agent.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""Unit tests for ProjectAgent — plan, gather, reason, report, peer_bus, sweep."""
|
||||
import datetime
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from agent_service.agents.project_agent import ProjectAgent, PROJECT_TOOLS
|
||||
from agent_service.agents.base_agent import AgentDirective, AgentReport, SweepReport
|
||||
|
||||
|
||||
def _directive(intent='', context=None):
|
||||
return AgentDirective(
|
||||
directive_id='proj-d1', user_id='1', intent=intent,
|
||||
context=context or {}, agent_name='project_agent',
|
||||
)
|
||||
|
||||
|
||||
def _make_agent():
|
||||
agent = ProjectAgent(odoo=MagicMock(), llm=MagicMock())
|
||||
agent._pt = MagicMock()
|
||||
agent._pt.get_projects = AsyncMock(return_value=[
|
||||
{'id': 1, 'name': 'Alpha'}, {'id': 2, 'name': 'Beta'}
|
||||
])
|
||||
agent._pt.get_tasks = AsyncMock(return_value=[])
|
||||
agent._pt.get_project_summary = AsyncMock(return_value={'task_count': 5})
|
||||
agent._pt.update_task_stage = AsyncMock(return_value=True)
|
||||
agent._pt.assign_task = AsyncMock(return_value=True)
|
||||
agent._pt.create_task = AsyncMock(return_value=99)
|
||||
agent._pt.log_timesheet = AsyncMock(return_value=True)
|
||||
agent._pt.post_chatter_note = AsyncMock(return_value=True)
|
||||
return agent
|
||||
|
||||
|
||||
# ── Meta ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_tool_count():
|
||||
assert len(PROJECT_TOOLS) <= 8
|
||||
|
||||
def test_agent_name():
|
||||
assert ProjectAgent.name == 'project_agent'
|
||||
|
||||
|
||||
# ── _plan ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plan_project_intent():
|
||||
agent = _make_agent()
|
||||
plan = await agent._plan(_directive(intent='show all projects overview'))
|
||||
assert plan['fetch_projects'] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plan_task_intent():
|
||||
agent = _make_agent()
|
||||
plan = await agent._plan(_directive(intent='list my tasks'))
|
||||
assert plan['fetch_tasks'] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plan_propagates_project_id():
|
||||
agent = _make_agent()
|
||||
plan = await agent._plan(_directive(context={'project_id': 3}))
|
||||
assert plan['project_id'] == 3
|
||||
|
||||
|
||||
# ── _gather ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gather_projects_by_default():
|
||||
agent = _make_agent()
|
||||
ctx = {'plan': {'fetch_projects': True, 'fetch_tasks': False,
|
||||
'project_id': None, 'user_id': None}}
|
||||
data = await agent._gather(ctx)
|
||||
assert 'projects' in data
|
||||
assert len(data['projects']) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gather_tasks_when_requested():
|
||||
agent = _make_agent()
|
||||
agent._pt.get_tasks = AsyncMock(return_value=[{'id': 10, 'kanban_state': 'normal'}])
|
||||
ctx = {'plan': {'fetch_projects': False, 'fetch_tasks': True,
|
||||
'project_id': None, 'user_id': None}}
|
||||
data = await agent._gather(ctx)
|
||||
assert 'tasks' in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gather_tasks_when_project_id_set():
|
||||
agent = _make_agent()
|
||||
ctx = {'plan': {'fetch_projects': False, 'fetch_tasks': False,
|
||||
'project_id': 1, 'user_id': None}}
|
||||
data = await agent._gather(ctx)
|
||||
assert 'tasks' in data
|
||||
|
||||
|
||||
# ── _reason ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reason_flags_blocked_tasks():
|
||||
agent = _make_agent()
|
||||
agent._gathered_data = {
|
||||
'tasks': [
|
||||
{'id': i, 'kanban_state': 'blocked', 'name': f'Task {i}'}
|
||||
for i in range(6)
|
||||
]
|
||||
}
|
||||
analysis = await agent._reason({})
|
||||
assert len(analysis['blocked_tasks']) == 6
|
||||
assert len(analysis['escalations']) == 1
|
||||
assert '6' in analysis['escalations'][0]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reason_few_blocked_no_escalation():
|
||||
agent = _make_agent()
|
||||
agent._gathered_data = {
|
||||
'tasks': [{'id': 1, 'kanban_state': 'blocked', 'name': 'T1'}]
|
||||
}
|
||||
analysis = await agent._reason({})
|
||||
assert len(analysis['blocked_tasks']) == 1
|
||||
assert analysis['escalations'] == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reason_no_tasks_no_escalation():
|
||||
agent = _make_agent()
|
||||
agent._gathered_data = {'projects': [{'id': 1}]}
|
||||
analysis = await agent._reason({})
|
||||
assert analysis['escalations'] == []
|
||||
|
||||
|
||||
# ── _report ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_report_project_count():
|
||||
agent = _make_agent()
|
||||
agent._gathered_data = {'projects': [{'id': 1}, {'id': 2}, {'id': 3}]}
|
||||
agent._escalations_list = []
|
||||
report = await agent._report({})
|
||||
assert isinstance(report, AgentReport)
|
||||
assert '3' in report.summary
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_report_task_and_blocked_count():
|
||||
agent = _make_agent()
|
||||
agent._gathered_data = {
|
||||
'tasks': [{'id': i, 'kanban_state': 'blocked' if i < 2 else 'normal'}
|
||||
for i in range(5)]
|
||||
}
|
||||
agent._escalations_list = []
|
||||
report = await agent._report({'analysis': {'blocked_tasks': [0, 1]}})
|
||||
assert '5' in report.summary
|
||||
assert '2' 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 'complete' in report.summary.lower() or 'project' in report.summary.lower()
|
||||
|
||||
|
||||
# ── _dispatch_tool ───────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_get_projects():
|
||||
agent = _make_agent()
|
||||
await agent._dispatch_tool('get_projects', {})
|
||||
agent._pt.get_projects.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_create_task():
|
||||
agent = _make_agent()
|
||||
await agent._dispatch_tool('create_task', {'project_id': 1, 'name': 'New Task'})
|
||||
agent._pt.create_task.assert_awaited_once_with(project_id=1, name='New Task')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_unknown_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_project_list():
|
||||
agent = _make_agent()
|
||||
result = await agent.handle_peer_request('project_list', {}, 'dir-1')
|
||||
assert 'projects' in result
|
||||
assert len(result['projects']) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_peer_task_count():
|
||||
agent = _make_agent()
|
||||
agent._pt.get_tasks = AsyncMock(return_value=[{'id': 1}, {'id': 2}])
|
||||
result = await agent.handle_peer_request('task_count', {'project_id': 1}, 'dir-1')
|
||||
assert result['count'] == 2
|
||||
|
||||
@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_finds_blocked_tasks():
|
||||
agent = _make_agent()
|
||||
agent._pt.get_tasks = AsyncMock(return_value=[
|
||||
{'id': 1, 'name': 'Stuck', 'kanban_state': 'blocked', 'date_deadline': None}
|
||||
])
|
||||
result = await agent.sweep()
|
||||
assert isinstance(result, SweepReport)
|
||||
assert any(f['type'] == 'blocked_task' for f in result.findings)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sweep_finds_overdue_tasks():
|
||||
agent = _make_agent()
|
||||
yesterday = str((datetime.date.today() - datetime.timedelta(days=1)))
|
||||
agent._pt.get_tasks = AsyncMock(return_value=[
|
||||
{'id': 2, 'name': 'Late', 'kanban_state': 'normal', 'date_deadline': yesterday}
|
||||
])
|
||||
result = await agent.sweep()
|
||||
assert any(f['type'] == 'overdue_task' for f in result.findings)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sweep_healthy_no_findings():
|
||||
agent = _make_agent()
|
||||
future = str((datetime.date.today() + datetime.timedelta(days=7)))
|
||||
agent._pt.get_tasks = AsyncMock(return_value=[
|
||||
{'id': 1, 'name': 'On track', 'kanban_state': 'normal', 'date_deadline': future}
|
||||
])
|
||||
result = await agent.sweep()
|
||||
assert result.findings == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sweep_handles_exception():
|
||||
agent = _make_agent()
|
||||
agent._pt.get_tasks = AsyncMock(side_effect=Exception('timeout'))
|
||||
result = await agent.sweep()
|
||||
assert result.error is not None
|
||||
Reference in New Issue
Block a user