Add comprehensive unit tests for all agent service components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
233
tests/test_project_tools.py
Normal file
233
tests/test_project_tools.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""Unit tests for ProjectTools."""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from agent_service.tools.project_tools import ProjectTools
|
||||
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=99)
|
||||
return ProjectTools(odoo)
|
||||
|
||||
|
||||
# ── get_projects ──────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_projects_default():
|
||||
t = _make_tools()
|
||||
result = await t.get_projects()
|
||||
t._o.search_read.assert_awaited_once()
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('active', '=', True) in domain
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_projects_inactive():
|
||||
t = _make_tools()
|
||||
await t.get_projects(active=False)
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('active', '=', False) in domain
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_projects_returns_data():
|
||||
t = _make_tools()
|
||||
t._o.search_read = AsyncMock(return_value=[
|
||||
{'id': 1, 'name': 'Alpha', 'task_count': 10},
|
||||
{'id': 2, 'name': 'Beta', 'task_count': 5},
|
||||
])
|
||||
result = await t.get_projects()
|
||||
assert len(result) == 2
|
||||
|
||||
|
||||
# ── get_tasks ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tasks_default():
|
||||
t = _make_tools()
|
||||
result = await t.get_tasks()
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('active', '=', True) in domain
|
||||
assert isinstance(result, list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tasks_with_project():
|
||||
t = _make_tools()
|
||||
await t.get_tasks(project_id=3)
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('project_id', '=', 3) in domain
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tasks_with_stage():
|
||||
t = _make_tools()
|
||||
await t.get_tasks(stage_id=2)
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('stage_id', '=', 2) in domain
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tasks_with_user():
|
||||
t = _make_tools()
|
||||
await t.get_tasks(user_id=5)
|
||||
domain = t._o.search_read.call_args[0][1]
|
||||
assert ('user_ids', 'in', [5]) in domain
|
||||
|
||||
|
||||
# ── get_project_summary ───────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_summary_structure():
|
||||
t = _make_tools()
|
||||
result = await t.get_project_summary(project_id=1)
|
||||
assert 'project_id' in result
|
||||
assert result['project_id'] == 1
|
||||
assert 'total_tasks' in result
|
||||
assert 'blocked_tasks' in result
|
||||
assert 'overdue_tasks' in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_summary_counts_blocked():
|
||||
import datetime
|
||||
t = _make_tools()
|
||||
future = str(datetime.date.today().replace(year=2030))
|
||||
t._o.search_read = AsyncMock(return_value=[
|
||||
{'kanban_state': 'blocked', 'date_deadline': future, 'user_ids': []},
|
||||
{'kanban_state': 'normal', 'date_deadline': future, 'user_ids': []},
|
||||
{'kanban_state': 'blocked', 'date_deadline': future, 'user_ids': []},
|
||||
])
|
||||
result = await t.get_project_summary(project_id=1)
|
||||
assert result['total_tasks'] == 3
|
||||
assert result['blocked_tasks'] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_project_summary_counts_overdue():
|
||||
import datetime
|
||||
t = _make_tools()
|
||||
yesterday = str(datetime.date.today() - datetime.timedelta(days=1))
|
||||
t._o.search_read = AsyncMock(return_value=[
|
||||
{'kanban_state': 'normal', 'date_deadline': yesterday, 'user_ids': []},
|
||||
{'kanban_state': 'normal', 'date_deadline': None, 'user_ids': []},
|
||||
])
|
||||
result = await t.get_project_summary(project_id=1)
|
||||
assert result['overdue_tasks'] == 1
|
||||
|
||||
|
||||
# ── update_task_stage ─────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_task_stage_success():
|
||||
t = _make_tools()
|
||||
result = await t.update_task_stage(task_id=5, stage_id=3)
|
||||
assert result is True
|
||||
t._o.write.assert_awaited_once_with('project.task', [5], {'stage_id': 3})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_task_stage_failure():
|
||||
t = _make_tools()
|
||||
t._o.write = AsyncMock(return_value=WriteResult(
|
||||
success=False, model='', record_id=None, action='write'))
|
||||
result = await t.update_task_stage(task_id=5, stage_id=3)
|
||||
assert result is False
|
||||
|
||||
|
||||
# ── assign_task ───────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_assign_task_success():
|
||||
t = _make_tools()
|
||||
result = await t.assign_task(task_id=5, user_id=10)
|
||||
assert result is True
|
||||
t._o.write.assert_awaited_once_with('project.task', [5], {'user_ids': [(4, 10)]})
|
||||
|
||||
|
||||
# ── create_task ───────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_basic():
|
||||
t = _make_tools()
|
||||
result = await t.create_task(project_id=1, name='New Task')
|
||||
assert result == 99
|
||||
call_args = t._o.call.call_args[0]
|
||||
assert call_args[0] == 'project.task'
|
||||
assert call_args[1] == 'create'
|
||||
vals = call_args[2][0]
|
||||
assert vals['name'] == 'New Task'
|
||||
assert vals['project_id'] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_with_all_options():
|
||||
t = _make_tools()
|
||||
await t.create_task(
|
||||
project_id=1, name='Full Task',
|
||||
description='Description text', user_id=5, date_deadline='2026-06-01',
|
||||
)
|
||||
vals = t._o.call.call_args[0][2][0]
|
||||
assert vals['description'] == 'Description text'
|
||||
assert vals['user_ids'] == [(4, 5)]
|
||||
assert vals['date_deadline'] == '2026-06-01'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_no_optional_fields():
|
||||
t = _make_tools()
|
||||
await t.create_task(project_id=1, name='Simple Task')
|
||||
vals = t._o.call.call_args[0][2][0]
|
||||
assert 'description' not in vals
|
||||
assert 'user_ids' not in vals
|
||||
assert 'date_deadline' not in vals
|
||||
|
||||
|
||||
# ── log_timesheet ─────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_log_timesheet_basic():
|
||||
t = _make_tools()
|
||||
result = await t.log_timesheet(task_id=5, employee_id=3, hours=8.0)
|
||||
assert result == 99
|
||||
call_args = t._o.call.call_args[0]
|
||||
assert call_args[0] == 'account.analytic.line'
|
||||
vals = call_args[2][0]
|
||||
assert vals['task_id'] == 5
|
||||
assert vals['employee_id'] == 3
|
||||
assert vals['unit_amount'] == 8.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_log_timesheet_with_date():
|
||||
t = _make_tools()
|
||||
await t.log_timesheet(task_id=5, employee_id=3, hours=4.0, date='2026-01-15')
|
||||
vals = t._o.call.call_args[0][2][0]
|
||||
assert vals['date'] == '2026-01-15'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_log_timesheet_default_description():
|
||||
t = _make_tools()
|
||||
await t.log_timesheet(task_id=5, employee_id=3, hours=2.0)
|
||||
vals = t._o.call.call_args[0][2][0]
|
||||
assert 'AI' in vals['name'] or vals['name']
|
||||
|
||||
|
||||
# ── post_chatter_note ─────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_chatter_note():
|
||||
t = _make_tools()
|
||||
result = await t.post_chatter_note('project.task', 1, 'Task blocked by dep')
|
||||
assert result is True
|
||||
t._o.call.assert_awaited_once()
|
||||
call_args = t._o.call.call_args[0]
|
||||
assert call_args[3]['body'] == 'Task blocked by dep'
|
||||
Reference in New Issue
Block a user