회기 무발화 0턴 분리, 자기예측 락 불변식 및 TDD 회귀 검증 완료
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s

This commit is contained in:
Yun Chan 2026-09-08 23:28:06 +09:00
parent a479db7a5a
commit a0311c5957
100 changed files with 4884 additions and 11210 deletions

View file

@ -6,7 +6,7 @@ import unittest
from unittest.mock import AsyncMock, patch
from .deps import Principal, Role
from . import session_read_model
from . import session_dashboard_projection
from .routes import sessions
from .services import state_machine
from .services.persona import P1, P2, P3
@ -88,14 +88,14 @@ def _exposure_sessions(persona_codes: list[str]) -> list[InProcSession]:
class TrainingExposureMetricTest(unittest.TestCase):
def test_zero_completed_sessions_is_insufficient(self) -> None:
metric = session_read_model.dashboard_training_exposure([])
metric = session_dashboard_projection.dashboard_training_exposure([])
self.assertEqual(metric.label, "판정 근거 부족")
self.assertEqual(metric.completed_sessions, 0)
self.assertIsNone(metric.dominant_share)
def test_one_completed_session_is_insufficient(self) -> None:
metric = session_read_model.dashboard_training_exposure(
metric = session_dashboard_projection.dashboard_training_exposure(
_exposure_sessions([P1.code])
)
@ -103,7 +103,7 @@ class TrainingExposureMetricTest(unittest.TestCase):
self.assertEqual(metric.dominant_share, 1.0)
def test_four_sessions_with_broad_exposure_is_balanced(self) -> None:
metric = session_read_model.dashboard_training_exposure(
metric = session_dashboard_projection.dashboard_training_exposure(
_exposure_sessions([P1.code, P1.code, P2.code, P3.code])
)
@ -111,7 +111,7 @@ class TrainingExposureMetricTest(unittest.TestCase):
self.assertEqual(metric.dominant_share, 0.5)
def test_dominant_share_at_seventy_five_percent_is_attention(self) -> None:
metric = session_read_model.dashboard_training_exposure(
metric = session_dashboard_projection.dashboard_training_exposure(
_exposure_sessions([P1.code, P1.code, P1.code, P2.code])
)
@ -120,7 +120,7 @@ class TrainingExposureMetricTest(unittest.TestCase):
self.assertEqual(metric.dominant_share, 0.75)
def test_tied_dominant_persona_is_deterministic_and_balanced(self) -> None:
metric = session_read_model.dashboard_training_exposure(
metric = session_dashboard_projection.dashboard_training_exposure(
_exposure_sessions([P2.code, P1.code, P2.code, P1.code])
)
@ -176,7 +176,7 @@ class LearnerDashboardTest(unittest.IsolatedAsyncioTestCase):
AsyncMock(return_value=(owned_sessions, True)),
) as list_recent_sessions,
patch.object(
sessions.session_persistence,
sessions.session_evaluation_repository,
"load_session_evaluation",
AsyncMock(side_effect=load_review),
),
@ -249,7 +249,7 @@ class LearnerDashboardTest(unittest.IsolatedAsyncioTestCase):
AsyncMock(return_value=([good_session, failed_session], True)),
),
patch.object(
sessions.session_persistence,
sessions.session_evaluation_repository,
"load_session_evaluation",
AsyncMock(return_value=({"status": "ready"}, True)),
),
@ -386,7 +386,7 @@ class LearnerDashboardTest(unittest.IsolatedAsyncioTestCase):
AsyncMock(return_value=(archive_records, True)),
) as list_session_archives,
patch.object(
sessions.session_persistence,
sessions.session_evaluation_repository,
"load_session_evaluation",
AsyncMock(side_effect=load_review),
),
@ -438,7 +438,7 @@ class LearnerDashboardTest(unittest.IsolatedAsyncioTestCase):
),
) as set_session_archived,
patch.object(
sessions.session_persistence,
sessions.session_evaluation_repository,
"load_session_evaluation",
AsyncMock(return_value=({"status": "ready"}, True)),
),
@ -486,7 +486,7 @@ class LearnerDashboardTest(unittest.IsolatedAsyncioTestCase):
AsyncMock(return_value=(None, True)),
) as set_session_archived,
patch.object(
sessions.session_persistence,
sessions.session_evaluation_repository,
"load_session_evaluation",
AsyncMock(return_value=(None, True)),
),
@ -510,6 +510,46 @@ class LearnerDashboardTest(unittest.IsolatedAsyncioTestCase):
self.assertFalse(response.session.archived)
self.assertIsNone(response.session.archived_at)
def test_zero_turn_ended_sessions_not_counted_in_completed_sessions(self) -> None:
"""REQ-010: Zero-turn sessions ended without counselor dialogue are not counted in completed_sessions."""
# 1 session with turns (completed)
normal_session = _session(
session_id="00000000-0000-0000-0000-00000000b991",
session_no=1,
persona_code=P1.code,
score="pos",
rapport=0.5,
technique="reflection",
created_at=1_000.0,
)
normal_session.ended = True
# 1 zero-turn session created and closed immediately
zero_turn_session = _session(
session_id="00000000-0000-0000-0000-00000000b992",
session_no=2,
persona_code=P1.code,
score="neutral",
rapport=0.0,
technique="none",
created_at=1_100.0,
)
zero_turn_session.turns = [] # No turns
zero_turn_session.ended = True
from app.session_dashboard_projection import dashboard_overview, dashboard_training_exposure
overview = dashboard_overview(
[normal_session, zero_turn_session],
visible_review_ready={normal_session.session_id: True, zero_turn_session.session_id: False},
archived_sessions=0,
)
self.assertEqual(overview.total_sessions, 2)
self.assertEqual(overview.completed_sessions, 1) # Only 1 completed, not 2!
exposure = dashboard_training_exposure([normal_session, zero_turn_session])
self.assertEqual(exposure.completed_sessions, 1)
if __name__ == "__main__":
unittest.main()