Add comprehensive unit tests for all agent service components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
317
tests/test_elearning_agent.py
Normal file
317
tests/test_elearning_agent.py
Normal file
@@ -0,0 +1,317 @@
|
||||
"""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'
|
||||
Reference in New Issue
Block a user