""" Тесты для update_skill_mastery. Проверяют формулы расчёта mastery, retention, trend. """ import pytest from datetime import datetime, timedelta # ───────────────────────────────────────────── # ХЕЛПЕР: вставить ответ напрямую в БД # ───────────────────────────────────────────── async def insert_answer( conn, user_id: int, session_id: int, skill_tag: str, correct: bool, question_type: str = "recognition", wrong_because: str | None = None, days_ago: int = 0, ): """Вставляет один ответ в user_learning_answers.""" created_at = datetime.now() - timedelta(days=days_ago) await conn.execute(""" INSERT INTO user_learning_answers ( user_id, session_id, level_code, topic_id, question_id, selected_answer, correct, skill_tag, question_type, context_type, difficulty, time_taken_ms, hint_used, wrong_because, is_final_answer, created_at ) VALUES ( $1, $2, 'A2', 'test_topic', 'test_q', 'x', $3, $4, $5, 'lesson_practice', 1, 5000, FALSE, $6, TRUE, $7 ) """, user_id, session_id, correct, skill_tag, question_type, wrong_because, created_at) # ───────────────────────────────────────────── # ТЕСТЫ # ───────────────────────────────────────────── async def test_mastery_no_answers_returns_none(conn, test_user, clean_mastery): """Если у юзера нет ответов по skill_tag — функция возвращает None.""" from routers.learning import update_skill_mastery result = await update_skill_mastery(conn, test_user, "nonexistent_skill") assert result is None async def test_mastery_100_percent_accuracy(conn, test_user, test_session, clean_mastery): """Если все ответы правильные — accuracy = 100, mastery = высокая.""" from routers.learning import update_skill_mastery for _ in range(5): await insert_answer(conn, test_user, test_session, "test_skill", correct=True) result = await update_skill_mastery(conn, test_user, "test_skill") assert result is not None assert result["attempts"] == 5 assert result["accuracy"] == 100.0 assert result["mastery"] >= 90.0 async def test_mastery_0_percent_accuracy(conn, test_user, test_session, clean_mastery): """Если все ответы неправильные — accuracy = 0, mastery = низкая.""" from routers.learning import update_skill_mastery for _ in range(5): await insert_answer(conn, test_user, test_session, "test_skill", correct=False, wrong_because="wrong_case") result = await update_skill_mastery(conn, test_user, "test_skill") assert result is not None assert result["accuracy"] == 0.0 assert result["mastery"] <= 30.0 async def test_mastery_50_percent_accuracy(conn, test_user, test_session, clean_mastery): """50% accuracy → mastery в среднем диапазоне.""" from routers.learning import update_skill_mastery for i in range(6): await insert_answer(conn, test_user, test_session, "test_skill", correct=(i % 2 == 0)) result = await update_skill_mastery(conn, test_user, "test_skill") assert result is not None assert 40.0 <= result["accuracy"] <= 60.0 assert 40.0 <= result["mastery"] <= 80.0 async def test_mastery_retention_fresh(conn, test_user, test_session, clean_mastery): """Если последний ответ был сегодня — retention_status = 'fresh'.""" from routers.learning import update_skill_mastery await insert_answer(conn, test_user, test_session, "test_skill", correct=True, days_ago=0) await insert_answer(conn, test_user, test_session, "test_skill", correct=True, days_ago=0) result = await update_skill_mastery(conn, test_user, "test_skill") assert result["retention_status"] == "fresh" async def test_mastery_retention_needs_review(conn, test_user, test_session, clean_mastery): """Если последний ответ 10 дней назад — retention_status = 'needs_review'.""" from routers.learning import update_skill_mastery await insert_answer(conn, test_user, test_session, "test_skill", correct=True, days_ago=10) await insert_answer(conn, test_user, test_session, "test_skill", correct=True, days_ago=10) result = await update_skill_mastery(conn, test_user, "test_skill") assert result["retention_status"] == "needs_review" async def test_mastery_retention_overdue(conn, test_user, test_session, clean_mastery): """Если последний ответ 20 дней назад — retention_status = 'overdue'.""" from routers.learning import update_skill_mastery await insert_answer(conn, test_user, test_session, "test_skill", correct=True, days_ago=20) await insert_answer(conn, test_user, test_session, "test_skill", correct=True, days_ago=20) result = await update_skill_mastery(conn, test_user, "test_skill") assert result["retention_status"] == "overdue" async def test_mastery_trend_improving(conn, test_user, test_session, clean_mastery): """Если последние 3 ответа лучше, чем общая точность — trend = 'improving'.""" from routers.learning import update_skill_mastery # 7 старых неправильных, 3 свежих правильных for _ in range(7): await insert_answer(conn, test_user, test_session, "test_skill", correct=False, days_ago=5) for _ in range(3): await insert_answer(conn, test_user, test_session, "test_skill", correct=True, days_ago=0) result = await update_skill_mastery(conn, test_user, "test_skill") # 3 правильных из 10 = 30% accuracy, но recent_accuracy = 100% → improving assert result["trend"] == "improving" async def test_mastery_trend_declining(conn, test_user, test_session, clean_mastery): """Если последние 3 ответа хуже, чем общая точность — trend = 'declining'.""" from routers.learning import update_skill_mastery # 7 старых правильных, 3 свежих неправильных for _ in range(7): await insert_answer(conn, test_user, test_session, "test_skill", correct=True, days_ago=5) for _ in range(3): await insert_answer(conn, test_user, test_session, "test_skill", correct=False, days_ago=0) result = await update_skill_mastery(conn, test_user, "test_skill") assert result["trend"] == "declining" async def test_mastery_trend_insufficient_data(conn, test_user, test_session, clean_mastery): """Если меньше 3 ответов — trend = 'insufficient_data'.""" from routers.learning import update_skill_mastery await insert_answer(conn, test_user, test_session, "test_skill", correct=True) await insert_answer(conn, test_user, test_session, "test_skill", correct=True) result = await update_skill_mastery(conn, test_user, "test_skill") assert result["trend"] == "insufficient_data"