세션 평가와 교수자 분석 보강

This commit is contained in:
Yun Chan 2026-07-01 12:10:52 +09:00
parent 5c4ac04e06
commit fe2796f05a
51 changed files with 4928 additions and 240 deletions

View file

@ -447,6 +447,49 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
legacy_history = await sessions.list_live_coach_history(sess.session_id, principal)
self.assertIsNone(legacy_history.events[0].stage)
async def test_live_coach_persistence_failure_is_not_swallowed(self) -> None:
principal = _principal()
sess = _session(principal)
suggestion = live_coach.LiveCoachSuggestion(
status="ready",
tone="pos",
focus="emotion",
title="감정 반영이 선명합니다",
message="학습자가 내담자의 감정을 먼저 되짚었습니다.",
)
save_error = sessions.HTTPException(
status_code=sessions.status.HTTP_503_SERVICE_UNAVAILABLE,
detail="live coach event save persistence unavailable; runtime fallback is disabled in prod",
)
with patch.object(
sessions,
"_retrieve_live_coach_grounding",
AsyncMock(return_value=[]),
), patch.object(
sessions.live_coach,
"generate_live_coaching",
AsyncMock(return_value=suggestion),
), patch.object(
session_persistence,
"save_live_coach_event",
AsyncMock(side_effect=save_error),
) as save_event:
with self.assertRaises(sessions.HTTPException) as caught:
await sessions.live_coach_turn(
sess.session_id,
sessions.LiveCoachRequest(
learner_text="그 마음이 컸겠네요.",
client_reply="네, 아무도 몰라주는 것 같았어요.",
turn_seq=1,
),
principal,
)
self.assertEqual(caught.exception.status_code, 503)
self.assertIn("live coach event save", str(caught.exception.detail))
save_event.assert_awaited_once()
async def test_live_coach_event_normalizes_legacy_stage_values(self) -> None:
suggestion = live_coach.LiveCoachSuggestion(
status="degraded",
@ -987,6 +1030,11 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
"category": "speech_activity",
"start_ms": 100,
},
{
"event_type": "silence",
"category": "timing",
"duration_ms": 1234,
},
{
"event_type": "background_noise",
"category": "audio_quality",
@ -1021,6 +1069,7 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
)
self.assertEqual(learner_turn.nonverbal[0].label, "침묵")
self.assertEqual(learner_turn.nonverbal[0].detail, "1.2초")
self.assertEqual([event.kind for event in learner_turn.nonverbal].count("silence"), 1)
self.assertEqual(learner_turn.nonverbal[1].detail, "분당 420자")
self.assertEqual(learner_turn.nonverbal[4].label, "음성 단서")
self.assertEqual(learner_turn.nonverbal[4].detail, "한숨 감지 · 신뢰도 82%")
@ -1191,6 +1240,66 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
self.assertIn("주호소", response.teacherReview.worksheetNote)
self.assertEqual(response.teacherReview.worksheetReviewedAt, "2026-06-27T10:05:00Z")
async def test_review_surfaces_session_evaluation_error_record(self) -> None:
principal = _principal()
sess = _session(principal)
sess.ended = True
sess.ended_at = sess.created_at + 180
sess.turns.extend(
[
TurnRecord(
turn_seq=1,
speaker="learner",
stage=sess.state.stage.value,
text="많이 지쳐 보였어요. 지금 제일 버거운 마음이 뭔가요?",
text_masked="많이 지쳐 보였어요. 지금 제일 버거운 마음이 뭔가요?",
created_at=sess.created_at + 1,
),
TurnRecord(
turn_seq=2,
speaker="client",
stage=sess.state.stage.value,
text="그냥 아무것도 하고 싶지 않아요.",
text_masked="그냥 아무것도 하고 싶지 않아요.",
created_at=sess.created_at + 2,
),
]
)
error_record = session_persistence.SessionEvaluationWrite.from_error(
session_id=sess.session_id,
learner_id=principal.user_id,
scope="session_end",
stage="정리",
error="session evaluation timeout after 45s",
).cache_record()
with (
patch.object(
sessions.session_persistence,
"load_session",
AsyncMock(return_value=sess),
),
patch.object(
sessions.session_persistence,
"load_case_worksheet",
AsyncMock(return_value=(None, False)),
),
patch.object(
sessions.session_persistence,
"load_session_evaluation",
AsyncMock(return_value=(error_record, True)),
),
):
response = await sessions.get_session_review(sess.session_id, principal)
self.assertFalse(response.reviewReady)
self.assertTrue(response.degraded)
self.assertEqual(response.supervisorState, "평가 실패")
self.assertIn("session evaluation timeout after 45s", response.summary)
self.assertEqual(response.rubric, [])
self.assertEqual(response.goodMoments, [])
self.assertEqual(response.growthPoints, [])
async def test_learner_can_save_case_formulation_worksheet(self) -> None:
principal = _principal()
sess = _session(principal)