"""평가 재시도 라우트의 실패 노출 테스트.""" from __future__ import annotations import importlib import unittest from types import SimpleNamespace from unittest.mock import AsyncMock, patch from fastapi import HTTPException from .deps import Principal, Role from .services import evaluator eval_routes = importlib.import_module("app.routes.eval") class ReevaluateSessionRouteTest(unittest.IsolatedAsyncioTestCase): async def test_reevaluate_session_saves_result_error_then_raises(self) -> None: session_id = "00000000-0000-0000-0000-00000000e222" sess = _session_stub() result = evaluator.SessionEvaluation( session_id=session_id, stage="정리", scope="session_end", turns_evaluated=1, error="no_structured_output", ) with ( patch.object(eval_routes, "_load_session_or_404", AsyncMock(return_value=sess)), patch.object(eval_routes.evaluator, "evaluate_session", AsyncMock(return_value=result)), patch.object( eval_routes.session_persistence, "save_session_evaluation", AsyncMock(return_value=True), ) as save_evaluation, ): with self.assertRaises(HTTPException) as raised: await eval_routes.reevaluate_session( session_id, eval_routes.ReevaluateRequest(scope="session_end"), _teacher_principal(), ) self.assertEqual(raised.exception.status_code, 502) self.assertEqual(raised.exception.detail, "no_structured_output") saved_write = save_evaluation.await_args.args[0] self.assertEqual(saved_write.status, "error") self.assertEqual(saved_write.error, "no_structured_output") async def test_reevaluate_session_raises_when_result_cannot_be_saved(self) -> None: session_id = "00000000-0000-0000-0000-00000000e222" sess = _session_stub() result = evaluator.SessionEvaluation( session_id=session_id, stage="정리", scope="session_end", turns_evaluated=1, ) with ( patch.object(eval_routes, "_load_session_or_404", AsyncMock(return_value=sess)), patch.object(eval_routes.evaluator, "evaluate_session", AsyncMock(return_value=result)), patch.object( eval_routes.session_persistence, "save_session_evaluation", AsyncMock(return_value=False), ), ): with self.assertRaises(HTTPException) as raised: await eval_routes.reevaluate_session( session_id, eval_routes.ReevaluateRequest(scope="session_end"), _teacher_principal(), ) self.assertEqual(raised.exception.status_code, 503) self.assertIn("could not be saved", raised.exception.detail) async def test_reevaluate_session_prefers_session_theory_mode(self) -> None: session_id = "00000000-0000-0000-0000-00000000e222" sess = _session_stub(theory_mode="cbt", theory_target=["humanistic"]) result = evaluator.SessionEvaluation( session_id=session_id, stage="정리", scope="session_end", turns_evaluated=1, ) with ( patch.object(eval_routes, "_load_session_or_404", AsyncMock(return_value=sess)), patch.object( eval_routes.evaluator, "evaluate_session", AsyncMock(return_value=result), ) as evaluate_session, patch.object( eval_routes.session_persistence, "save_session_evaluation", AsyncMock(return_value=True), ), ): await eval_routes.reevaluate_session( session_id, eval_routes.ReevaluateRequest(scope="session_end"), _teacher_principal(), ) self.assertEqual(evaluate_session.await_args.kwargs["theory_mode"], "cbt") async def test_reevaluate_turn_preserves_session_theory_mode(self) -> None: session_id = "00000000-0000-0000-0000-00000000e222" sess = _session_stub(theory_mode="cbt", theory_target=["humanistic"], with_turns=True) result = evaluator.TurnEvaluation(turn_seq=1, stage="정리", theory_mode="cbt") with ( patch.object(eval_routes, "_load_session_or_404", AsyncMock(return_value=sess)), patch.object( eval_routes.evaluator, "evaluate_turn", AsyncMock(return_value=result), ) as evaluate_turn, ): await eval_routes.reevaluate_turn( session_id, eval_routes.TurnReevaluateRequest(turn_seq=1), _teacher_principal(), ) ctx = evaluate_turn.await_args.args[0] self.assertEqual(ctx.theory_mode, "cbt") async def test_get_session_evaluation_exposes_status_error_and_durable_flag(self) -> None: session_id = "00000000-0000-0000-0000-00000000e222" sess = _session_stub() record = { "status": "error", "source": "engine", "scope": "session_end", "stage": "정리", "payload": {}, "error": "session evaluation timeout after 45s", "updated_at": "2026-07-01T00:00:00Z", } with ( patch.object(eval_routes, "_load_session_or_404", AsyncMock(return_value=sess)), patch.object( eval_routes.session_persistence, "load_session_evaluation", AsyncMock(return_value=(record, True)), ), ): response = await eval_routes.get_session_evaluation(session_id, _teacher_principal()) self.assertEqual(response.status, "error") self.assertEqual(response.error, "session evaluation timeout after 45s") self.assertTrue(response.durable) self.assertEqual(response.deep, {}) def _session_stub( *, theory_mode: str = "humanistic", theory_target: list[str] | None = None, with_turns: bool = False, ): selected_theory_mode = theory_mode selected_theory_target = theory_target or [] class SessionStub: session_id = "00000000-0000-0000-0000-00000000e222" case_id = "00000000-0000-0000-0000-00000000c333" learner_id = "00000000-0000-0000-0000-000000000111" theory_mode = selected_theory_mode persona = SimpleNamespace(theory_target=selected_theory_target) state = SimpleNamespace(stage=SimpleNamespace(value="정리")) turns = ( [ SimpleNamespace( turn_seq=1, speaker="counselor", text="상담자 발화", text_masked="상담자 발화", ), SimpleNamespace( turn_seq=2, speaker="client", text="내담자 응답", text_masked="내담자 응답", ), ] if with_turns else [] ) def masked_turns(self): return [{"speaker": "counselor", "text_masked": "상담자 발화"}] return SessionStub() def _teacher_principal() -> Principal: return Principal( user_id="00000000-0000-0000-0000-000000000222", role=Role.TEACHER, cohort_ids=[], ) if __name__ == "__main__": unittest.main()