99 lines
4.1 KiB
Python
99 lines
4.1 KiB
Python
"""
|
|
Тесты для get_next_lesson.
|
|
Проверяют приоритеты: overdue → weak → test → next.
|
|
"""
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# ХЕЛПЕРЫ
|
|
# ─────────────────────────────────────────────
|
|
|
|
async def set_user_level(conn, user_id: int, level: str):
|
|
await conn.execute("UPDATE users SET current_level = $1 WHERE telegram_id = $2", level, user_id)
|
|
|
|
|
|
async def insert_mastery(
|
|
conn,
|
|
user_id: int,
|
|
skill_tag: str,
|
|
mastery: float,
|
|
retention: str = "fresh",
|
|
attempts: int = 3,
|
|
days_since: int = 0,
|
|
):
|
|
"""Вставляет запись в user_skill_mastery."""
|
|
last_attempt = datetime.now() - timedelta(days=days_since)
|
|
await conn.execute("""
|
|
INSERT INTO user_skill_mastery (
|
|
user_id, skill_tag, mastery_score, retention_status,
|
|
attempts_count, last_attempt_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT (user_id, skill_tag) DO UPDATE SET
|
|
mastery_score = $3,
|
|
retention_status = $4,
|
|
attempts_count = $5,
|
|
last_attempt_at = $6
|
|
""", user_id, skill_tag, mastery, retention, attempts, last_attempt)
|
|
|
|
|
|
# ─────────────────────────────────────────────
|
|
# ТЕСТЫ
|
|
# ─────────────────────────────────────────────
|
|
|
|
async def test_orchestrator_prioritizes_overdue(conn, test_user, create_test_lesson, clean_mastery):
|
|
"""Если есть overdue навык — он выбирается первым."""
|
|
from routers.orchestrator import get_next_lesson
|
|
|
|
await set_user_level(conn, test_user, "A2")
|
|
await create_test_lesson("test_skill")
|
|
await insert_mastery(conn, test_user, "test_skill", mastery=50.0, retention="overdue", days_since=20)
|
|
|
|
result = await get_next_lesson(conn, test_user)
|
|
|
|
assert result["type"] == "review"
|
|
assert result["reason"] == "overdue"
|
|
|
|
|
|
async def test_orchestrator_prioritizes_weak(conn, test_user, create_test_lesson, clean_mastery):
|
|
"""Если нет overdue, но есть weak (mastery < 75) — выбирается он."""
|
|
from routers.orchestrator import get_next_lesson
|
|
|
|
await set_user_level(conn, test_user, "A2")
|
|
await create_test_lesson("test_skill")
|
|
await insert_mastery(conn, test_user, "test_skill", mastery=50.0, retention="fresh", days_since=1)
|
|
|
|
result = await get_next_lesson(conn, test_user)
|
|
|
|
assert result["type"] == "review"
|
|
assert result["reason"] == "weak_mastery"
|
|
|
|
|
|
async def test_orchestrator_prefers_overdue_over_weak(conn, test_user, create_test_lesson, clean_mastery):
|
|
"""Если есть и overdue, и weak — overdue важнее."""
|
|
from routers.orchestrator import get_next_lesson
|
|
|
|
await set_user_level(conn, test_user, "A2")
|
|
await create_test_lesson("weak_skill", order=1)
|
|
await create_test_lesson("overdue_skill", order=2)
|
|
|
|
await insert_mastery(conn, test_user, "weak_skill", mastery=40.0, retention="fresh", days_since=1)
|
|
await insert_mastery(conn, test_user, "overdue_skill", mastery=60.0, retention="overdue", days_since=20)
|
|
|
|
result = await get_next_lesson(conn, test_user)
|
|
|
|
assert result["reason"] == "overdue"
|
|
|
|
|
|
async def test_orchestrator_weak_requires_min_attempts(conn, test_user, create_test_lesson, clean_mastery):
|
|
"""Weak skill требует минимум 2 попытки — если меньше, он не выбирается."""
|
|
from routers.orchestrator import get_next_lesson
|
|
|
|
await set_user_level(conn, test_user, "A2")
|
|
await create_test_lesson("test_skill")
|
|
await insert_mastery(conn, test_user, "test_skill", mastery=50.0, retention="fresh", attempts=1)
|
|
|
|
result = await get_next_lesson(conn, test_user)
|
|
|
|
assert result["reason"] != "weak_mastery" |