Add comprehensive unit tests for all agent service components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 04:00:45 +00:00
parent 6c22a9a128
commit 20a69313d7
19 changed files with 3734 additions and 66 deletions

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