"""Unit tests for ElearningAgent — tool count, plan, gather, reason, act, report, peer_bus, sweep.""" import pytest from unittest.mock import AsyncMock, MagicMock from agent_service.agents.elearning_agent import ElearningAgent, ELEARNING_TOOLS from agent_service.agents.base_agent import AgentReport, SweepReport def _make_agent(): agent = ElearningAgent(odoo=MagicMock(), llm=MagicMock()) agent._el = MagicMock() agent._el.get_courses = AsyncMock(return_value=[ {'id': 1, 'name': 'Python 101', 'completion_rate': 80.0}, {'id': 2, 'name': 'HR Basics', 'completion_rate': 20.0}, ]) agent._el.get_course_details = AsyncMock(return_value={ 'channel': {'name': 'Python 101'}, 'slide_count': 10, 'enrolled_users': [], 'slide_completion': [], }) agent._el.get_learning_summary = AsyncMock(return_value={ 'total_courses': 5, 'total_enrollments': 50, 'avg_completion': 65.0, 'low_completion_courses': [{'id': 2, 'name': 'HR Basics', 'completion_rate': 20.0}], }) agent._el.create_course = AsyncMock(return_value={'id': 10, 'name': 'New Course', 'success': True}) agent._el.update_course = AsyncMock(return_value={'success': True}) agent._el.add_section = AsyncMock(return_value={'id': 20, 'name': 'Section 1', 'success': True}) agent._el.create_slide = AsyncMock(return_value={'id': 30, 'name': 'Slide 1', 'slide_type': 'webpage', 'success': True}) agent._el.enroll_user = AsyncMock(return_value={'success': True}) agent._el.post_chatter_note = AsyncMock(return_value=True) agent._el.suggest_next_course = AsyncMock(return_value=[{'id': 3, 'name': 'Advanced Python'}]) return agent # ── Meta ──────────────────────────────────────────────────────────────────── def test_tool_count_exactly_8(): assert len(ELEARNING_TOOLS) == 8 def test_tool_names(): names = {t['name'] for t in ELEARNING_TOOLS} assert 'get_courses' in names assert 'get_course_details' in names assert 'create_course' in names assert 'update_course' in names assert 'add_section' in names assert 'create_slide' in names assert 'enroll_user' in names assert 'post_chatter_note' in names def test_removed_tools_not_present(): names = {t['name'] for t in ELEARNING_TOOLS} assert 'get_course_stats' not in names assert 'get_enrolled_users' not in names assert 'get_slide_completion' not in names assert 'get_learning_summary' not in names assert 'publish_course' not in names assert 'flag_low_completion' not in names assert 'suggest_next_course' not in names def test_update_course_has_website_published_param(): update = next(t for t in ELEARNING_TOOLS if t['name'] == 'update_course') assert 'website_published' in update['parameters'] def test_agent_name(): assert ElearningAgent.name == 'elearning_agent' # ── _plan ─────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_plan_create_intent(): agent = _make_agent() agent._directive = MagicMock() agent._directive.task = 'create a new course on Python' agent._directive.params = {} plan = await agent._plan() assert plan['intent'] == 'create' @pytest.mark.asyncio async def test_plan_build_intent(): agent = _make_agent() agent._directive = MagicMock() agent._directive.task = 'build me a course on compliance' agent._directive.params = {} plan = await agent._plan() assert plan['intent'] == 'create' @pytest.mark.asyncio async def test_plan_enroll_intent(): agent = _make_agent() agent._directive = MagicMock() agent._directive.task = 'enroll Alice in Python 101' agent._directive.params = {} plan = await agent._plan() assert plan['intent'] == 'enroll' @pytest.mark.asyncio async def test_plan_read_intent_default(): agent = _make_agent() agent._directive = MagicMock() agent._directive.task = 'show me the course list' agent._directive.params = {} plan = await agent._plan() assert plan['intent'] == 'read' # ── _gather ───────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_gather_reads_summary_when_no_channel(): agent = _make_agent() await agent._gather({'intent': 'read', 'channel_id': None}) agent._el.get_learning_summary.assert_awaited_once() @pytest.mark.asyncio async def test_gather_reads_course_details_when_channel_given(): agent = _make_agent() await agent._gather({'intent': 'read', 'channel_id': 1}) agent._el.get_course_details.assert_awaited_once_with(channel_id=1) # ── _reason ───────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_reason_read_returns_low_completion_list(): agent = _make_agent() agent._gathered = { 'intent': 'read', 'summary': { 'low_completion_courses': [{'id': 2, 'completion_rate': 20.0}] } } reasoning = await agent._reason() assert reasoning['intent'] == 'read' assert len(reasoning['low_completion']) == 1 # ── _act ───────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_act_read_intent_does_nothing(): agent = _make_agent() await agent._act({'intent': 'read', 'low_completion': []}) agent._el.post_chatter_note.assert_not_called() @pytest.mark.asyncio async def test_act_flags_low_completion_courses(): agent = _make_agent() await agent._act({ 'intent': 'read', 'low_completion': [{'id': 2, 'completion_rate': 15.0}], }) agent._el.post_chatter_note.assert_awaited_once() call_kwargs = agent._el.post_chatter_note.call_args.kwargs assert call_kwargs['model'] == 'slide.channel' assert call_kwargs['record_id'] == 2 assert '[AI FLAG]' in call_kwargs['note'] @pytest.mark.asyncio async def test_act_caps_at_3_flags(): agent = _make_agent() low_courses = [{'id': i, 'completion_rate': 10.0} for i in range(5)] await agent._act({'intent': 'read', 'low_completion': low_courses}) assert agent._el.post_chatter_note.await_count == 3 # ── _report ───────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_report_includes_summary_stats(): agent = _make_agent() agent._directive = MagicMock() agent._directive.directive_id = 'test-d1' agent._gathered = { 'summary': {'total_courses': 5, 'total_enrollments': 50, 'avg_completion': 65.0} } agent._actions_taken = [] report = await agent._report() assert isinstance(report, AgentReport) assert '5' in report.summary @pytest.mark.asyncio async def test_report_includes_course_details(): agent = _make_agent() agent._directive = MagicMock() agent._directive.directive_id = 'test-d1' agent._gathered = { 'course_details': {'channel': {'name': 'Python 101'}, 'slide_count': 10} } agent._actions_taken = [] report = await agent._report() assert 'Python 101' in report.summary assert '10' in report.summary @pytest.mark.asyncio async def test_report_fallback(): agent = _make_agent() agent._directive = MagicMock() agent._directive.directive_id = 'test-d1' agent._gathered = {} agent._actions_taken = [] report = await agent._report() assert 'complete' in report.summary.lower() or 'elearning' in report.summary.lower() # ── tool dispatchers ───────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_tool_get_courses(): agent = _make_agent() result = await agent._tool_get_courses() agent._el.get_courses.assert_awaited_once() assert len(result) == 2 @pytest.mark.asyncio async def test_tool_get_course_details(): agent = _make_agent() result = await agent._tool_get_course_details(channel_id=1) agent._el.get_course_details.assert_awaited_once_with(channel_id=1) assert 'slide_count' in result @pytest.mark.asyncio async def test_tool_create_course_records_action(): agent = _make_agent() result = await agent._tool_create_course(name='New Course') assert result['success'] is True assert any(a['action'] == 'create_course' for a in agent._actions_taken) @pytest.mark.asyncio async def test_tool_update_course_with_publish(): agent = _make_agent() await agent._tool_update_course(channel_id=1, website_published=True) agent._el.update_course.assert_awaited_once_with( channel_id=1, name=None, description=None, enroll_policy=None, website_published=True) @pytest.mark.asyncio async def test_tool_add_section_records_action(): agent = _make_agent() result = await agent._tool_add_section(channel_id=1, name='Section A', sequence=10) assert result['success'] is True assert any(a['action'] == 'add_section' for a in agent._actions_taken) @pytest.mark.asyncio async def test_tool_create_slide_records_action(): agent = _make_agent() result = await agent._tool_create_slide(channel_id=1, name='Slide 1') assert result['success'] is True assert any(a['action'] == 'create_slide' for a in agent._actions_taken) @pytest.mark.asyncio async def test_tool_enroll_user(): agent = _make_agent() result = await agent._tool_enroll_user(channel_id=1, partner_id=5) agent._el.enroll_user.assert_awaited_once_with(channel_id=1, partner_id=5) @pytest.mark.asyncio async def test_tool_post_chatter_note(): agent = _make_agent() result = await agent._tool_post_chatter_note( model='slide.channel', record_id=1, note='Test note') assert result['success'] is True # ── handle_peer_request ────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_peer_learning_summary(): agent = _make_agent() result = await agent.handle_peer_request('learning_summary', {}, 'dir-1') assert result['success'] is True assert 'total_courses' in result @pytest.mark.asyncio async def test_peer_suggest_courses(): agent = _make_agent() result = await agent.handle_peer_request('suggest_courses', {'partner_id': 3}, 'dir-1') assert result['success'] is True assert 'courses' in result @pytest.mark.asyncio async def test_peer_suggest_courses_missing_partner_id(): agent = _make_agent() result = await agent.handle_peer_request('suggest_courses', {}, 'dir-1') assert result['success'] is False @pytest.mark.asyncio async def test_peer_unknown_returns_error(): agent = _make_agent() result = await agent.handle_peer_request('bad_type', {}, 'dir-1') assert result['success'] is False # ── sweep ──────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_sweep_finds_low_completion_course(): agent = _make_agent() result = await agent.sweep() assert isinstance(result, SweepReport) assert any(f['issue'] == 'low_completion' for f in result.findings) @pytest.mark.asyncio async def test_sweep_no_findings_when_all_complete(): agent = _make_agent() agent._el.get_learning_summary = AsyncMock(return_value={ 'total_courses': 3, 'total_enrollments': 30, 'avg_completion': 90.0, 'low_completion_courses': [], }) result = await agent.sweep() assert result.findings == [] @pytest.mark.asyncio async def test_sweep_handles_exception(): agent = _make_agent() agent._el.get_learning_summary = AsyncMock(side_effect=Exception('timeout')) result = await agent.sweep() assert isinstance(result, SweepReport) assert result.findings[0]['issue'] == 'sweep_failed'