DocDoeAI / tests /test_learning_engine.py
asnannp's picture
Deploy backend cd4237ff: support routes + rate limit + exam_date nullable + upload 413 fix
7c6ffa6
Raw
History Blame Contribute Delete
18.4 kB
"""Tests for Course Builder and Exam Coach Session services."""
from __future__ import annotations
from app.services.course_builder import (
build_course_plan,
course_plan_to_output,
_detect_course_type,
)
from app.services.exam_coach_session import (
build_exam_coach_session,
exam_coach_session_to_output,
_parse_daily_time,
_compute_exam_countdown,
)
from app.services.pyq_pattern_analyzer import PatternAnalysisResult, TopicPattern
def test_learning_engine_routes_registered(client):
course_response = client.post(
"/generate/course-plan",
json={"raw_request": "Teach me machine learning"},
)
assert course_response.status_code == 200, course_response.text
assert course_response.json()["lessons_total"] > 0
session_response = client.post(
"/generate/exam-coach-session",
json={
"subject": "Physics",
"topic": "Electric Charges",
"daily_time": "30 min",
},
)
assert session_response.status_code == 200, session_response.text
assert session_response.json()["study_blocks"]
def test_learning_engine_routes_require_auth(auth_client):
response = auth_client.post(
"/generate/course-plan",
json={"raw_request": "Teach me machine learning"},
)
assert response.status_code in {401, 403}
# === Course Builder Tests ===
def test_course_plan_disaster_management_semester():
result = build_course_plan(
raw_request="Teach me disaster management 4th semester degree",
semester="4",
degree="degree",
)
assert result.title != ""
assert result.lessons_total > 0
assert result.estimated_total_minutes > 0
assert len(result.modules) > 0
assert result.data_source_label in {"Starter seed", "Uploaded source"}
assert result.confidence > 0
def test_course_plan_machine_learning():
result = build_course_plan(
raw_request="Teach me machine learning",
)
output = course_plan_to_output(result)
assert output["title"] != ""
assert output["lessons_total"] > 0
assert len(output["modules"]) > 0
assert "Machine Learning" in output["subject_area"] or "Machine Learning" in output["title"]
assert len(output["prerequisites"]) > 0
assert len(output["practice_tasks"]) > 0
assert len(output["quizzes"]) > 0
def test_course_plan_with_source_context():
result = build_course_plan(
raw_request="Teach me Physics chapter 1",
source_context="Electric charges and Coulomb's law are fundamental concepts...",
subject="Physics",
topic="Electric Charges and Fields",
)
assert result.source_basis == "source_upload"
assert result.data_source_label == "Uploaded source"
def test_course_plan_source_ids_without_context_not_marked_uploaded():
result = build_course_plan(
raw_request="Teach me Physics chapter 1",
source_ids=["missing-or-not-ready-source"],
subject="Physics",
topic="Electric Charges and Fields",
)
assert result.source_basis == "catalog_seed"
assert result.data_source_label == "Starter seed"
assert any("no usable source text" in note.lower() for note in result.trust_notes)
def test_course_plan_no_source_uses_starter_seed():
result = build_course_plan(
raw_request="Teach me Chemistry mole concept",
subject="Chemistry",
topic="Mole Concept",
)
assert result.source_basis == "catalog_seed"
assert result.data_source_label == "Starter seed"
assert any("No source" in note for note in result.trust_notes)
def test_course_plan_degree_semester():
result = build_course_plan(
raw_request="Teach me disaster management for 4th semester B.Tech",
semester="4",
degree="B.Tech",
)
assert len(result.modules) >= 3
assert result.lessons_total >= 8
def test_course_plan_skill_course():
result = build_course_plan(
raw_request="Learn Python programming from scratch",
)
assert len(result.modules) >= 3
assert any("Python" in m.title for m in result.modules)
def test_course_plan_playlist_metadata():
result = build_course_plan(
raw_request="Turn this ML playlist into a course",
playlist_metadata={
"topics": [
"Linear Regression",
{"title": "Gradient Descent"},
"Model Evaluation",
],
},
)
assert result.learner_level == "Playlist"
assert result.lessons_total > 0
assert any("Linear Regression" in module.title for module in result.modules)
def test_course_plan_confidence_increases_with_info():
low = build_course_plan(raw_request="Teach something")
high = build_course_plan(
raw_request="Teach me +2 Physics chapter 1",
subject="Physics",
topic="Electric Charges",
class_level="+2",
)
assert high.confidence > low.confidence
def test_course_plan_output_json_serializable():
result = build_course_plan(
raw_request="Teach me Physics Ray Optics",
subject="Physics",
topic="Ray Optics",
)
output = course_plan_to_output(result)
assert isinstance(output, dict)
assert "modules" in output
assert "quizzes" in output
assert "practice_tasks" in output
assert isinstance(output["modules"], list)
def test_course_plan_school_physics_chapter():
result = build_course_plan(
raw_request="Teach me +2 Physics Ray Optics",
subject="Physics",
topic="Ray Optics",
)
assert result.subject_area == "Physics"
assert len(result.modules) > 0
assert result.lessons_total > 0
def test_course_plan_school_tuition_contract_contains_required_sections():
result = build_course_plan(
raw_request="Teach me lens maker formula",
class_level="Plus Two / Grade 12",
syllabus="Kerala HSE",
subject="Physics",
chapter="Ray Optics",
exam_date="2026-07-10",
daily_study_time="1 hour",
goal="board-exam",
)
output = course_plan_to_output(result)
assert output["student_context"]["class_level"] == "Plus Two / Grade 12"
assert output["student_context"]["syllabus"] == "Kerala HSE"
assert output["student_context"]["subject"] == "Physics"
assert output["student_context"]["chapter"] == "Ray Optics"
assert output["student_context"]["exam_date"] == "2026-07-10"
assert output["student_context"]["daily_study_time"] == "1 hour"
assert output["lesson_outline"]
assert output["prerequisite_check"]
assert output["concept_explanation"]["explanation"]
assert output["derivation_or_problem_steps"][0]["steps"]
assert output["practice_questions"]
assert output["pyq_style_questions"]
assert output["weak_topic_repairs"]
assert output["revision_plan"]
assert output["estimated_study_time"]
def test_course_plan_route_accepts_full_school_context(client):
response = client.post(
"/generate/course-plan",
json={
"raw_request": "Teach me Boolean logic",
"class_level": "Class 11",
"syllabus": "Kerala HSE",
"subject": "Computer Science",
"chapter": "Boolean Logic",
"exam_date": "2026-07-10",
"daily_study_time": "30 minutes",
"current_level": "basic",
"weak_topics": ["logic gates"],
"goal": "concept-clarity",
},
)
assert response.status_code == 200, response.text
data = response.json()
assert data["student_context"]["syllabus"] == "Kerala HSE"
assert data["student_context"]["chapter"] == "Boolean Logic"
assert data["student_context"]["daily_study_time"] == "30 minutes"
assert data["lesson_outline"]
assert data["practice_questions"]
assert data["revision_plan"]
assert "Student self-reported level: basic." in data["trust_notes"]
assert data["weak_topic_repairs"][0]["topic"] == "logic gates"
# === Detect Course Type Tests ===
def test_detect_course_type_degree():
assert _detect_course_type("4th semester degree") == "degree"
assert _detect_course_type("B.Tech computer science") == "degree"
assert _detect_course_type("MCA 2nd sem") == "degree"
def test_detect_course_type_skill():
assert _detect_course_type("Learn machine learning") == "skill"
assert _detect_course_type("Course on Python") == "skill"
def test_detect_course_type_topic():
assert _detect_course_type("Teach me Physics chapter 1") == "topic"
# === Exam Coach Session Tests ===
def test_exam_coach_session_plus2_physics_1hour():
result = build_exam_coach_session(
class_level="+2",
board="Kerala HSE",
subject="Physics",
chapter="Electric Charges and Fields",
daily_time="1 hour",
)
assert result.session_title != ""
assert result.time_budget == "1 hour"
assert len(result.study_blocks) > 0
assert result.today_mission != ""
assert result.concept_teaching_task != {}
assert result.answer_writing_task != {}
assert result.revision_task != {}
assert result.quick_test != {}
def test_exam_coach_session_1_5_hour_derivation():
result = build_exam_coach_session(
class_level="+2",
subject="Physics",
topic="Equations of Motion",
daily_time="1.5 hour",
)
assert result.time_budget == "1.5 hour"
assert len(result.study_blocks) == 3
assert result.derivation_or_numerical_task.get("task_type") == "derivation"
def test_exam_coach_session_no_pyq_no_fake():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="1 hour",
)
pyq_task = result.pyq_task
assert pyq_task["has_evidence"] is False
assert "No uploaded PYQ" in pyq_task["description"] or "No PYQ" in pyq_task["data_source_label"]
assert any("No PYQ" in note for note in result.trust_notes)
def test_exam_coach_session_plus2_motion_does_not_leak_unrelated_seed():
result = build_exam_coach_session(
class_level="+2",
subject="Physics",
topic="+2 Physics motion in a straight line",
daily_time="1 hour",
exam_date="in 20 days",
)
concept_output = result.concept_teaching_task["output"]
rendered = str(concept_output).lower()
assert concept_output["chapter_title"] == "+2 Physics motion in a straight line"
assert concept_output["data_source_label"] == "Starter seed"
assert "electric charges" not in rendered
assert "coulomb" not in rendered
assert "gauss" not in rendered
assert result.pyq_task["has_evidence"] is False
assert result.data_source_label == "Starter seed"
def test_exam_coach_session_chemistry_numerical_focus():
result = build_exam_coach_session(
subject="Chemistry",
topic="Chemistry mole concept numericals",
daily_time="1 hour",
)
task = result.derivation_or_numerical_task
concept_output = result.concept_teaching_task["output"]
assert task["task_type"] == "numerical"
assert "numerical" in task["description"].lower()
assert "Chemistry mole concept numericals" == concept_output["chapter_title"]
assert "given data" in concept_output["board_answer_keywords"]
assert result.data_source_label == "Starter seed"
def test_exam_coach_session_maths_proof_focus():
result = build_exam_coach_session(
subject="Maths",
topic="Maths trigonometry proof revision",
daily_time="1 hour",
)
task = result.derivation_or_numerical_task
concept_output = result.concept_teaching_task["output"]
assert task["task_type"] == "proof"
assert "proof" in task["title"].lower()
assert "given" in concept_output["board_answer_keywords"]
assert "to prove" in concept_output["board_answer_keywords"]
assert "reason" in concept_output["board_answer_keywords"]
def test_exam_coach_session_unknown_topic_uses_requested_starter_seed():
result = build_exam_coach_session(
subject="Physics",
topic="Quantum banana curves",
daily_time="30 min",
)
concept_output = result.concept_teaching_task["output"]
rendered = str(concept_output).lower()
assert concept_output["chapter_title"] == "Quantum banana curves"
assert concept_output["data_source_label"] == "Starter seed"
assert "electric charges" not in rendered
assert "ray optics" not in rendered
assert "coulomb" not in rendered
assert result.pyq_task["has_evidence"] is False
def test_exam_coach_session_with_pyq_evidence():
pyq_data = [
{"question_text": "State Coulomb's law", "marks": 3, "year": "2023"},
{"question_text": "Derive electric field", "marks": 5, "year": "2022"},
]
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="1 hour",
pyq_data=pyq_data,
)
pyq_task = result.pyq_task
assert pyq_task["has_evidence"] is True
assert "Uploaded PYQs" in pyq_task["data_source_label"]
def test_exam_coach_session_unknown_exam_date():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="1 hour",
exam_date=None,
)
assert result.exam_countdown == "unknown"
assert any("Exam date not provided" in note for note in result.trust_notes)
def test_exam_coach_session_exam_tomorrow():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="1 hour",
exam_date="tomorrow",
)
assert "tomorrow" in result.exam_countdown.lower()
def test_exam_coach_session_30_min():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="30 min",
)
assert result.time_budget == "30 min"
assert len(result.study_blocks) == 1
assert result.study_blocks[0].duration_minutes == 30
def test_exam_coach_session_weak_topics():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="1 hour",
weak_topics=["Electromagnetic Induction", "Current Electricity"],
)
revision = result.revision_task
assert "Electromagnetic Induction" in revision.get("revision_focus", [])
assert "Current Electricity" in revision.get("revision_focus", [])
def test_exam_coach_session_with_source():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="1 hour",
source_context="Electric charge is a fundamental property of matter...",
)
assert result.data_source_label == "Uploaded source"
assert not any("No source" in note for note in result.trust_notes)
def test_exam_coach_session_source_ids_without_context_not_marked_uploaded():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="1 hour",
source_ids=["missing-or-not-ready-source"],
)
assert result.data_source_label == "Starter seed"
assert any("no usable source text" in note.lower() for note in result.trust_notes)
def test_exam_coach_session_last_night_mode():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="last-night",
)
assert result.time_budget == "last-night"
assert "Last-night revision" in result.today_mission
assert len(result.study_blocks) == 3
def test_exam_coach_session_avoids_unsupported_high_yield_claims():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="30 min",
exam_date="tomorrow",
)
rendered = str(exam_coach_session_to_output(result)).lower()
assert "high-yield" not in rendered
assert "sure shot" not in rendered
assert "guaranteed" not in rendered
def test_exam_coach_session_output_json_serializable():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="1 hour",
)
output = exam_coach_session_to_output(result)
assert isinstance(output, dict)
assert "study_blocks" in output
assert "concept_teaching_task" in output
assert "quick_test" in output
def test_exam_coach_session_next_day_plan():
result = build_exam_coach_session(
subject="Physics",
topic="Electric Charges",
daily_time="1 hour",
weak_topics=["Magnetism"],
)
next_plan = result.next_day_plan
assert "Magnetism" in str(next_plan.get("suggested_topics", []))
# === Parse Daily Time Tests ===
def test_parse_daily_time_30_min():
label, minutes = _parse_daily_time("30 min")
assert minutes == 30
def test_parse_daily_time_1_hour():
label, minutes = _parse_daily_time("1 hour")
assert minutes == 60
def test_parse_daily_time_1_5_hour():
label, minutes = _parse_daily_time("1.5 hour")
assert minutes == 90
def test_parse_daily_time_last_night():
label, minutes = _parse_daily_time("last-night")
assert label == "last-night"
assert minutes == 90
def test_parse_daily_time_2_hours():
label, minutes = _parse_daily_time("2 hours")
assert minutes == 120
# === Compute Exam Countdown Tests ===
def test_compute_exam_countdown_days():
label, days = _compute_exam_countdown("3 days")
assert days == 3
assert "3" in label
def test_compute_exam_countdown_weeks():
label, days = _compute_exam_countdown("2 weeks")
assert days == 14
def test_compute_exam_countdown_unknown():
label, days = _compute_exam_countdown(None)
assert label == "unknown"
assert days is None
def test_compute_exam_countdown_tomorrow():
label, days = _compute_exam_countdown("tomorrow")
assert days == 1
# === Integration: Course + Exam Session ===
def test_course_plan_and_exam_session_same_subject():
course = build_course_plan(
raw_request="Teach me +2 Physics",
subject="Physics",
)
session = build_exam_coach_session(
subject="Physics",
daily_time="1 hour",
)
assert course.subject_area == session.concept_teaching_task.get("subject", "Physics")