"""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'