initial commit (backend)
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Тесты для 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"
|
||||
Reference in New Issue
Block a user