회기 연속성과 멀티 케이스 계약을 영속화
This commit is contained in:
parent
be08c0b573
commit
72353ecd82
26 changed files with 2170 additions and 127 deletions
|
|
@ -249,15 +249,16 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
version=3,
|
||||
degraded=False,
|
||||
)
|
||||
case_context = sessions.session_persistence.CaseContext(
|
||||
case_id=case_id,
|
||||
last_session_no=1,
|
||||
)
|
||||
next_session_id = "00000000-0000-0000-0000-000000000702"
|
||||
|
||||
async def create_next_session(**kwargs: object) -> InProcSession:
|
||||
self.assertEqual(kwargs["case_id"], case_id)
|
||||
self.assertEqual(kwargs["session_no"], 2)
|
||||
self.assertEqual(kwargs["start_mode"], "continue")
|
||||
# The route now delegates durable case selection to the locked
|
||||
# persistence transaction. It only provides a provisional first
|
||||
# session number; the selected case counter makes this session #2.
|
||||
self.assertEqual(kwargs["session_no"], 1)
|
||||
state = await kwargs["locked_state_factory"](case_id, 2)
|
||||
created = InProcSession(
|
||||
session_id=next_session_id,
|
||||
case_id=case_id,
|
||||
|
|
@ -265,9 +266,9 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
persona_code=card.code,
|
||||
theory_mode=str(kwargs["theory_mode"]),
|
||||
persona=card,
|
||||
state=kwargs["state"],
|
||||
session_no=int(kwargs["session_no"]),
|
||||
prev_rapport_credit=float(kwargs["carry_rapport"]),
|
||||
state=state,
|
||||
session_no=2,
|
||||
prev_rapport_credit=state.rapport_credit,
|
||||
)
|
||||
durable_sessions[created.session_id] = created
|
||||
return created
|
||||
|
|
@ -283,11 +284,6 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
"get_catalog_persona",
|
||||
AsyncMock(return_value=catalog_persona),
|
||||
),
|
||||
patch.object(
|
||||
sessions.session_persistence,
|
||||
"get_case_context",
|
||||
AsyncMock(return_value=case_context),
|
||||
),
|
||||
patch.object(
|
||||
sessions,
|
||||
"_build_seed_recall",
|
||||
|
|
@ -301,7 +297,10 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
patch.object(sessions.asyncio, "create_task", close_background),
|
||||
):
|
||||
started = await sessions.start_session(
|
||||
sessions.SessionStartRequest(persona_code=card.code),
|
||||
sessions.SessionStartRequest(
|
||||
persona_code=card.code,
|
||||
case_id=case_id,
|
||||
),
|
||||
principal,
|
||||
)
|
||||
|
||||
|
|
@ -511,8 +510,12 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
return FakeTransaction()
|
||||
|
||||
async def fetchrow(self, query: str, *args: object) -> dict[str, object]:
|
||||
if "FROM app.case_profile" in query and "FOR UPDATE" in query:
|
||||
return {"case_id": args[0], "last_session_no": 1}
|
||||
if "UPDATE app.case_profile" in query:
|
||||
return {"last_session_no": 2}
|
||||
if "FROM app.sessions" in query and "ended_at IS NULL" in query:
|
||||
return None
|
||||
if "INSERT INTO app.sessions" in query:
|
||||
self.session_insert_args = args
|
||||
return {
|
||||
|
|
@ -532,6 +535,8 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
raise AssertionError(f"unexpected query: {query}")
|
||||
|
||||
async def execute(self, query: str, *args: object) -> str:
|
||||
if "pg_advisory_xact_lock" in query:
|
||||
return "SELECT 1"
|
||||
self.assert_jsonb_object(query, args)
|
||||
return "INSERT 0 1"
|
||||
|
||||
|
|
@ -1811,8 +1816,13 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
|
||||
async def fake_create_session(**kwargs):
|
||||
self.assertEqual(kwargs["case_id"], case_context.case_id)
|
||||
self.assertEqual(kwargs["session_no"], 2)
|
||||
self.assertGreater(kwargs["state"].rapport_credit, 0)
|
||||
self.assertEqual(kwargs["start_mode"], "continue")
|
||||
self.assertEqual(kwargs["session_no"], 1)
|
||||
state = await kwargs["locked_state_factory"](
|
||||
case_context.case_id,
|
||||
2,
|
||||
)
|
||||
self.assertGreater(state.rapport_credit, 0)
|
||||
return InProcSession(
|
||||
session_id="stable-case-session",
|
||||
case_id=kwargs["case_id"],
|
||||
|
|
@ -1820,9 +1830,9 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
persona_code=card.code,
|
||||
theory_mode=kwargs["theory_mode"],
|
||||
persona=card,
|
||||
state=kwargs["state"],
|
||||
session_no=kwargs["session_no"],
|
||||
prev_rapport_credit=kwargs["carry_rapport"],
|
||||
state=state,
|
||||
session_no=2,
|
||||
prev_rapport_credit=state.rapport_credit,
|
||||
)
|
||||
|
||||
def close_background(coro):
|
||||
|
|
@ -1833,11 +1843,6 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
patch.object(
|
||||
sessions, "get_catalog_persona", AsyncMock(return_value=catalog_persona)
|
||||
),
|
||||
patch.object(
|
||||
sessions.session_persistence,
|
||||
"get_case_context",
|
||||
AsyncMock(return_value=case_context),
|
||||
),
|
||||
patch.object(
|
||||
sessions,
|
||||
"_build_seed_recall",
|
||||
|
|
@ -1851,7 +1856,10 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
patch.object(sessions.asyncio, "create_task", close_background),
|
||||
):
|
||||
response = await sessions.start_session(
|
||||
sessions.SessionStartRequest(persona_code=card.code),
|
||||
sessions.SessionStartRequest(
|
||||
persona_code=card.code,
|
||||
case_id=case_context.case_id,
|
||||
),
|
||||
principal,
|
||||
)
|
||||
|
||||
|
|
@ -1911,16 +1919,23 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
return None
|
||||
|
||||
async def fake_create_session(**kwargs):
|
||||
test_case.assertEqual(kwargs["case_id"], case_context.case_id)
|
||||
test_case.assertEqual(kwargs["start_mode"], "continue")
|
||||
test_case.assertEqual(kwargs["session_no"], 1)
|
||||
state = await kwargs["locked_state_factory"](
|
||||
case_context.case_id,
|
||||
2,
|
||||
)
|
||||
return InProcSession(
|
||||
session_id="db-seed-recall-session",
|
||||
case_id=kwargs["case_id"],
|
||||
case_id=case_context.case_id,
|
||||
learner_id=principal.user_id,
|
||||
persona_code=card.code,
|
||||
theory_mode=kwargs["theory_mode"],
|
||||
persona=card,
|
||||
state=kwargs["state"],
|
||||
session_no=kwargs["session_no"],
|
||||
prev_rapport_credit=kwargs["carry_rapport"],
|
||||
state=state,
|
||||
session_no=2,
|
||||
prev_rapport_credit=state.rapport_credit,
|
||||
)
|
||||
|
||||
def close_background(coro):
|
||||
|
|
@ -1931,11 +1946,6 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
patch.object(
|
||||
sessions, "get_catalog_persona", AsyncMock(return_value=catalog_persona)
|
||||
),
|
||||
patch.object(
|
||||
sessions.session_persistence,
|
||||
"get_case_context",
|
||||
AsyncMock(return_value=case_context),
|
||||
),
|
||||
patch.object(sessions.db, "get_pool", return_value=object()),
|
||||
patch.object(
|
||||
sessions.db,
|
||||
|
|
@ -1950,7 +1960,10 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
patch.object(sessions.asyncio, "create_task", close_background),
|
||||
):
|
||||
response = await sessions.start_session(
|
||||
sessions.SessionStartRequest(persona_code=card.code),
|
||||
sessions.SessionStartRequest(
|
||||
persona_code=card.code,
|
||||
case_id=case_context.case_id,
|
||||
),
|
||||
principal,
|
||||
)
|
||||
started = store.get(response.session_id)
|
||||
|
|
@ -2743,10 +2756,43 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
)
|
||||
self.assertIn("AI 평가 재시도가 필요합니다", response.summary)
|
||||
self.assertNotIn("session evaluation timeout after 45s", response.summary)
|
||||
self.assertIsNone(response.evaluationFailure)
|
||||
self.assertEqual(response.rubric, [])
|
||||
self.assertEqual(response.goodMoments, [])
|
||||
self.assertEqual(response.growthPoints, [])
|
||||
|
||||
teacher_response = sessions.build_session_review(
|
||||
sessions.SessionReviewReadInput(
|
||||
session=sess,
|
||||
evaluation_record=error_record,
|
||||
evaluation_durable=True,
|
||||
include_teacher_review=True,
|
||||
)
|
||||
)
|
||||
self.assertIsNotNone(teacher_response.evaluationFailure)
|
||||
assert teacher_response.evaluationFailure is not None
|
||||
self.assertEqual(teacher_response.evaluationFailure.code, "timeout")
|
||||
self.assertTrue(teacher_response.evaluationFailure.retryable)
|
||||
self.assertNotIn("session evaluation timeout after 45s", teacher_response.model_dump_json())
|
||||
|
||||
oversized_input_record = {
|
||||
**error_record,
|
||||
"error": "engine_error: Agy CLI 프롬프트가 Windows 명령줄 안전 한도(24,000자)를 초과했습니다.",
|
||||
}
|
||||
oversized_response = sessions.build_session_review(
|
||||
sessions.SessionReviewReadInput(
|
||||
session=sess,
|
||||
evaluation_record=oversized_input_record,
|
||||
evaluation_durable=True,
|
||||
include_teacher_review=True,
|
||||
)
|
||||
)
|
||||
self.assertIsNotNone(oversized_response.evaluationFailure)
|
||||
assert oversized_response.evaluationFailure is not None
|
||||
self.assertEqual(oversized_response.evaluationFailure.code, "legacy_argv_limit")
|
||||
self.assertTrue(oversized_response.evaluationFailure.retryable)
|
||||
self.assertNotIn("24,000", oversized_response.model_dump_json())
|
||||
|
||||
def test_review_marks_stale_missing_session_evaluation_as_failed(self) -> None:
|
||||
principal = _principal()
|
||||
sess = _session(principal)
|
||||
|
|
@ -2780,7 +2826,10 @@ class SessionTurnPersistenceTest(unittest.IsolatedAsyncioTestCase):
|
|||
session=sess,
|
||||
evaluation_record=None,
|
||||
evaluation_durable=True,
|
||||
now_ts=(sess.ended_at or sess.created_at) + 41.0,
|
||||
# 평가 생성·감사 원장·결과 저장의 전체 예산을 넘긴 뒤에만 누락을 실패로 본다.
|
||||
now_ts=(sess.ended_at or sess.created_at)
|
||||
+ sessions.session_evaluation_stale_after_seconds()
|
||||
+ 1.0,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue