/
artyuhovs
/
AIFeedBackTrainingBot
Обзор
Документация
Войти
/
artyuhovs
/
AIFeedBackTrainingBot
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
tests/test_learner_workflow.py
439 строк
13 KB
Codex
Завершить архитектурное оздоровление и исправить итоги
15 июл 2026, 11:05
15 июл 2026, 11:05
c65f9a6
Код
Авторство
О чём код?
from __future__ import annotations from datetime import timedelta, timezone import pytest from app.services.learner_workflow import ( LearnerPhase, build_learner_workflow, learning_is_complete, ) from tests.time_support import frozen_clock, iso_at CLOCK = frozen_clock(zone=timezone(timedelta(hours=3))) CLOCK.advance(timedelta(hours=-3)) AT = iso_at(CLOCK) def moment(**delta: float) -> str: return iso_at(CLOCK, **delta) def assignment(**overrides: object) -> dict[str, object]: row: dict[str, object] = { "id": 10, "stream_publication_id": 100, "status": "submitted", "scheduled_at": moment(hours=-2), "deadline_at": moment(hours=-1), "review_deadline_at": moment(hours=1), "review_closed_at": None, "review_quality_deadline_at": moment(hours=3), "review_quality_closed_at": None, "result_notification_status": "pending", "correction_status": "not_started", "correction_opened_at": None, "correction_deadline_at": None, } row.update(overrides) return row def review_task(**overrides: object) -> dict[str, object]: row: dict[str, object] = { "id": 20, "stream_publication_id": 100, "status": "assigned", } row.update(overrides) return row def received_review(**overrides: object) -> dict[str, object]: row: dict[str, object] = {"id": 30, "quality_score": None, "quality_comment": None} row.update(overrides) if row["quality_score"] is not None and "quality_comment" not in overrides: row["quality_comment"] = "Содержательный ответ" return row def phase( row: dict[str, object], *, at: str = AT, outgoing: list[dict[str, object]] | None = None, incoming: list[dict[str, object]] | None = None, written: list[dict[str, object]] | None = None, ) -> LearnerPhase: workflow = build_learner_workflow( [row], outgoing or [], {int(row["id"]): incoming or []}, at, {int(row["stream_publication_id"]): written or []}, ) return workflow[0].phase def test_workflow_hides_upcoming_assignment_and_opens_it_at_publication_time() -> None: row = assignment( scheduled_at=moment(minutes=30), deadline_at=moment(hours=2), ) assert phase(row) == LearnerPhase.UPCOMING assert phase(row, at=moment(minutes=30)) == LearnerPhase.SUBMISSION def test_workflow_keeps_submission_current_until_exact_deadline() -> None: row = assignment(deadline_at=AT) assert phase(row, at=moment(seconds=-1)) == LearnerPhase.SUBMISSION assert phase(row, outgoing=[review_task()]) == LearnerPhase.PEER_REVIEW @pytest.mark.parametrize( ("offset", "expected"), [ ({"milliseconds": -1}, LearnerPhase.UPCOMING), ({}, LearnerPhase.SUBMISSION), ({"milliseconds": 1}, LearnerPhase.SUBMISSION), ], ) def test_publication_boundary_before_exact_after( offset: dict[str, float], expected: LearnerPhase, ) -> None: row = assignment(scheduled_at=AT, deadline_at=moment(hours=1)) assert phase(row, at=moment(**offset)) == expected @pytest.mark.parametrize( ("offset", "expected"), [ ({"milliseconds": -1}, LearnerPhase.SUBMISSION), ({}, LearnerPhase.PEER_REVIEW), ({"milliseconds": 1}, LearnerPhase.PEER_REVIEW), ], ) def test_submission_deadline_boundary_before_exact_after( offset: dict[str, float], expected: LearnerPhase, ) -> None: row = assignment(deadline_at=AT, review_deadline_at=moment(hours=1)) assert phase(row, at=moment(**offset), outgoing=[review_task()]) == expected @pytest.mark.parametrize( ("offset", "expected"), [ ({"milliseconds": -1}, LearnerPhase.PEER_REVIEW), ({}, LearnerPhase.WAITING_FOR_RESULTS), ({"milliseconds": 1}, LearnerPhase.WAITING_FOR_RESULTS), ], ) def test_peer_review_deadline_boundary_before_exact_after( offset: dict[str, float], expected: LearnerPhase, ) -> None: row = assignment(review_deadline_at=AT) assert phase(row, at=moment(**offset), outgoing=[review_task()]) == expected @pytest.mark.parametrize( ("offset", "expected"), [ ({"milliseconds": -1}, LearnerPhase.REVIEW_FEEDBACK), ({}, LearnerPhase.WAITING_FOR_IMPROVEMENT), ({"milliseconds": 1}, LearnerPhase.WAITING_FOR_IMPROVEMENT), ], ) def test_review_quality_deadline_boundary_before_exact_after( offset: dict[str, float], expected: LearnerPhase, ) -> None: row = assignment( review_deadline_at=moment(hours=-1), review_closed_at=moment(hours=-1), review_quality_deadline_at=AT, ) assert phase(row, at=moment(**offset), incoming=[received_review()]) == expected @pytest.mark.parametrize( ("offset", "expected"), [ ({"milliseconds": -1}, LearnerPhase.WAITING_FOR_IMPROVEMENT), ({}, LearnerPhase.IMPROVEMENT), ({"milliseconds": 1}, LearnerPhase.IMPROVEMENT), ], ) def test_correction_open_boundary_before_exact_after( offset: dict[str, float], expected: LearnerPhase, ) -> None: row = assignment( review_deadline_at=moment(hours=-2), review_closed_at=moment(hours=-2), review_quality_deadline_at=moment(hours=-1), review_quality_closed_at=moment(hours=-1), correction_opened_at=AT, correction_deadline_at=moment(hours=1), ) assert ( phase( row, at=moment(**offset), incoming=[received_review(quality_score=8.0)], ) == expected ) @pytest.mark.parametrize( ("offset", "expected"), [ ({"milliseconds": -1}, LearnerPhase.IMPROVEMENT), ({}, LearnerPhase.MISSED_IMPROVEMENT), ({"milliseconds": 1}, LearnerPhase.MISSED_IMPROVEMENT), ], ) def test_correction_deadline_boundary_before_exact_after( offset: dict[str, float], expected: LearnerPhase, ) -> None: row = assignment( review_deadline_at=moment(hours=-3), review_closed_at=moment(hours=-3), review_quality_deadline_at=moment(hours=-2), review_quality_closed_at=moment(hours=-2), correction_opened_at=moment(hours=-1), correction_deadline_at=AT, ) assert ( phase( row, at=moment(**offset), incoming=[received_review(quality_score=8.0)], ) == expected ) def test_workflow_keeps_completed_peer_reviews_editable_until_review_deadline() -> None: row = assignment() assert phase(row, outgoing=[review_task(status="completed")]) == LearnerPhase.PEER_REVIEW assert phase(row, outgoing=[]) == LearnerPhase.WAITING_FOR_PEER_REVIEW def test_workflow_waits_for_domain_close_at_exact_review_deadline() -> None: row = assignment(review_deadline_at=AT) assert phase(row, outgoing=[review_task(status="completed")]) == LearnerPhase.WAITING_FOR_RESULTS def test_workflow_exposes_received_review_ratings_independently_of_delivery() -> None: row = assignment( review_deadline_at=moment(minutes=-30), review_closed_at=moment(minutes=-29), review_quality_deadline_at=moment(hours=1, minutes=30), result_notification_status="failed", ) assert phase(row, incoming=[received_review()]) == LearnerPhase.REVIEW_FEEDBACK assert ( phase( row, incoming=[received_review(quality_score=8.0), received_review(id=31)], ) == LearnerPhase.REVIEW_FEEDBACK ) assert ( phase( row, incoming=[ received_review(quality_score=8.0), received_review(id=31, quality_score=9.0), ], ) == LearnerPhase.WAITING_FOR_REVIEW_FEEDBACK ) assert ( phase( row, at=moment(hours=1, minutes=30), incoming=[ received_review(quality_score=8.0), received_review(id=31, quality_score=9.0), ], ) == LearnerPhase.WAITING_FOR_IMPROVEMENT ) correction = assignment( review_deadline_at=moment(minutes=-30), review_closed_at=moment(minutes=-29), review_quality_deadline_at=moment(minutes=-15), review_quality_closed_at=moment(minutes=-15), correction_opened_at=moment(minutes=-10), correction_deadline_at=moment(days=1, minutes=-10), ) rated = [received_review(quality_score=8.0)] assert ( phase( correction, at=moment(minutes=-10, seconds=-1), incoming=rated, ) == LearnerPhase.WAITING_FOR_IMPROVEMENT ) assert phase(correction, incoming=rated) == LearnerPhase.IMPROVEMENT assert ( phase( correction, at=moment(days=1, minutes=-10), incoming=rated, ) == LearnerPhase.MISSED_IMPROVEMENT ) assert ( phase( {**correction, "correction_status": "submitted"}, at=moment(days=1, minutes=-10), incoming=rated, ) == LearnerPhase.COMPLETE ) def test_quality_score_without_a_meaningful_comment_remains_in_review_feedback() -> None: row = assignment( review_deadline_at=moment(minutes=-30), review_closed_at=moment(minutes=-29), review_quality_deadline_at=moment(hours=1, minutes=30), ) assert ( phase( row, incoming=[received_review(quality_score=8.0, quality_comment="")], ) == LearnerPhase.REVIEW_FEEDBACK ) assert ( phase( row, incoming=[received_review(quality_score=8.0, quality_comment=" ")], ) == LearnerPhase.REVIEW_FEEDBACK ) def test_workflow_has_explicit_terminal_state_when_no_reviews_were_received() -> None: row = assignment( review_deadline_at=moment(minutes=-30), review_closed_at=moment(minutes=-29), ) assert phase(row) == LearnerPhase.COMPLETE_WITHOUT_REVIEWS def test_non_submitter_does_not_wait_forever_for_results() -> None: row = assignment( status="late", review_deadline_at=moment(minutes=-30), review_closed_at=moment(minutes=-29), ) assert phase(row, outgoing=[review_task(status="completed")]) == LearnerPhase.MISSED_SUBMISSION def test_reviewer_only_feedback_is_current_until_it_is_seen() -> None: row = assignment( status="late", review_deadline_at=moment(minutes=-30), review_closed_at=moment(minutes=-29), review_quality_deadline_at=moment(minutes=-15), review_quality_closed_at=moment(minutes=-15), ) written_feedback = [ { "id": 40, "review_comment": "Моя рецензия", "quality_comment": "Ответ автора", } ] assert phase(row, written=written_feedback) == LearnerPhase.REVIEWER_FEEDBACK assert ( phase( {**row, "reviewer_feedback_seen_at": moment(minutes=-10)}, written=written_feedback, ) == LearnerPhase.MISSED_SUBMISSION ) def test_missed_original_and_correction_are_not_successful_completion() -> None: ordinary = assignment( status="not_started", review_deadline_at=None, deadline_at=moment(hours=-1), ) correction = assignment( review_deadline_at=moment(hours=-1, minutes=-30), review_closed_at=moment(hours=-1, minutes=-29), review_quality_deadline_at=moment(hours=-1, minutes=-25), review_quality_closed_at=moment(hours=-1, minutes=-25), correction_opened_at=moment(hours=-1, minutes=-20), correction_deadline_at=moment(minutes=-30), ) assert phase(ordinary) == LearnerPhase.MISSED_SUBMISSION assert phase(correction, incoming=[received_review(quality_score=8.0)]) == LearnerPhase.MISSED_IMPROVEMENT def test_program_is_complete_when_all_stages_are_terminal_including_misses() -> None: workflow = build_learner_workflow( [ assignment(id=10, stream_publication_id=100, review_deadline_at=None), assignment( id=11, stream_publication_id=101, status="late", review_deadline_at=None, ), ], [], {}, AT, ) assert [item.phase for item in workflow] == [ LearnerPhase.COMPLETE, LearnerPhase.MISSED_SUBMISSION, ] assert learning_is_complete(workflow) def test_overlapping_publications_keep_their_review_tasks_separate() -> None: first = assignment(id=10, stream_publication_id=100) second = assignment( id=11, stream_publication_id=101, scheduled_at=moment(minutes=-30), deadline_at=moment(minutes=30), review_deadline_at=moment(hours=2), ) workflow = build_learner_workflow( [first, second], [review_task(id=20, stream_publication_id=100)], {10: [], 11: []}, AT, ) assert [item.phase for item in workflow] == [LearnerPhase.PEER_REVIEW, LearnerPhase.SUBMISSION] assert [review["id"] for review in workflow[0].reviews_to_write] == [20] assert workflow[1].reviews_to_write == ()