Files
hyungi_document_server/tests/test_sr_schedule.py
hyungi e1da984e08 refactor(study): SR 산술 sr_schedule.py 공용추출 (B1 — 카드 SR 토대)
문제 SR과 카드 SR이 같은 간격 상수·산술을 참조하도록 순수함수 추출. 운영 동작 무변경.

- app/services/study/sr_schedule.py: REVIEW_INTERVAL_DAYS{1:3,2:7,3:14}/MASTERED=4/FIRST_DUE=1
  + advance(stage,outcome,now)→(new_stage,new_due) | None(skipped) + first_due(now).
  진입 게이트(due_at IS NOT NULL/최초 due/skipped 불변)는 호출부 잔류(finalize vs review-complete 정책 차이).
- session_finalize.py: 상수·advance 분기 → sr_schedule import + sr_advance() (re-export 유지).
- study_question_progress.py: DEFAULT_FIRST_DUE_DAYS → sr_schedule import.
- 회귀 테스트 7/7: 전진 1·3·7·14·졸업·리셋·skipped불변·상수 + 전 stage×outcome 구 로직 바이트 동등.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:11:38 +09:00

94 lines
3.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""sr_schedule 공용추출 회귀 테스트 (B1).
문제 SR 동작이 추출 전과 동일함을 보장 — advance() 가 구 session_finalize 분기 로직과
바이트 동등(전진 1·3·7·14 / 졸업 / 오답 리셋 / skipped 불변 / 상수값)인지 검증.
"""
from __future__ import annotations
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "app"))
from services.study import sr_schedule as sr # noqa: E402
NOW = datetime(2026, 6, 7, 12, 0, 0, tzinfo=timezone.utc)
def _old_logic(review_stage, outcome, now):
"""추출 전 session_finalize.py:188-201 의 산술을 그대로 재현 (동등성 기준)."""
if outcome == "correct":
new_stage = (review_stage or 0) + 1
if new_stage >= 4:
return new_stage, None
return new_stage, now + timedelta(days={1: 3, 2: 7, 3: 14}[new_stage])
elif outcome in ("wrong", "unsure"):
return 0, now + timedelta(days=1)
return None # skipped
def test_constants():
assert sr.REVIEW_INTERVAL_DAYS == {1: 3, 2: 7, 3: 14}
assert sr.REVIEW_STAGE_MASTERED == 4
assert sr.DEFAULT_FIRST_DUE_DAYS == 1
def test_advance_correct_progression():
assert sr.advance(None, "correct", NOW) == (1, NOW + timedelta(days=3))
assert sr.advance(0, "correct", NOW) == (1, NOW + timedelta(days=3))
assert sr.advance(1, "correct", NOW) == (2, NOW + timedelta(days=7))
assert sr.advance(2, "correct", NOW) == (3, NOW + timedelta(days=14))
def test_advance_graduation():
# stage 3 → correct → stage 4 = 졸업(due_at=None)
assert sr.advance(3, "correct", NOW) == (4, None)
assert sr.advance(4, "correct", NOW) == (5, None)
def test_advance_reset():
assert sr.advance(0, "wrong", NOW) == (0, NOW + timedelta(days=1))
assert sr.advance(2, "wrong", NOW) == (0, NOW + timedelta(days=1))
assert sr.advance(2, "unsure", NOW) == (0, NOW + timedelta(days=1))
def test_advance_skipped_no_change():
assert sr.advance(1, "skipped", NOW) is None
assert sr.advance(3, "skipped", NOW) is None
def test_first_due():
assert sr.first_due(NOW) == (0, NOW + timedelta(days=1))
def test_equivalence_with_old_logic():
# 전 stage × 전 outcome 조합에서 추출 함수 == 구 로직.
for stage in (None, 0, 1, 2, 3, 4):
for outcome in ("correct", "wrong", "unsure", "skipped"):
assert sr.advance(stage, outcome, NOW) == _old_logic(stage, outcome, NOW), \
f"mismatch stage={stage} outcome={outcome}"
def test_reexport_preserved():
# 기존 import 경로 (session_finalize / study_question_progress) 가 상수를 재-export.
from services.study import session_finalize as sf
assert sf.REVIEW_INTERVAL_DAYS == sr.REVIEW_INTERVAL_DAYS
assert sf.REVIEW_STAGE_MASTERED == sr.REVIEW_STAGE_MASTERED
assert sf.DEFAULT_FIRST_DUE_DAYS == sr.DEFAULT_FIRST_DUE_DAYS
_TESTS = [v for k, v in dict(globals()).items() if k.startswith("test_")]
if __name__ == "__main__":
# session_finalize import 는 무거운 의존(ai 등) 가능 — reexport 테스트만 조건부.
ran = 0
for t in _TESTS:
if t.__name__ == "test_reexport_preserved":
continue
t()
ran += 1
print(f"OK ({ran} pure tests; reexport는 pytest에서)")