From 805b2551b8cd8d726beb403400b747aa431d952f Mon Sep 17 00:00:00 2001 From: jmj Date: Fri, 5 Jun 2026 01:27:50 +0900 Subject: [PATCH] =?UTF-8?q?fix(ai):=20=ED=94=BC=EB=93=9C=EB=B0=B1=20endRea?= =?UTF-8?q?son=20=ED=8C=8C=EC=8B=B1=20=EC=8B=A4=ED=8C=A8=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=90=EC=88=98=20=EB=AF=B8=EC=83=9D=EC=84=B1=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core 가 질문 풀 소진 시 POOL_EXHAUSTED 사유로 세션을 종료하는데, AI 의 GenerateFeedbackRequest.end_reason 이 Literal["USER_REQUEST", "MAX_QUESTIONS_REACHED"] 라 envelope 파싱이 실패(→DLQ)해 피드백이 아예 생성되지 않았다(점수 안 뜸). end_reason 은 프롬프트 컨텍스트로만 쓰이므로 str|None 으로 풀어 향후 사유 추가에도 깨지지 않게 한다. - 회귀 테스트: POOL_EXHAUSTED 로도 피드백 생성되는지 검증. - Core SessionEndedEvent 주석에 POOL_EXHAUSTED 반영. 로그 근거: feedback.parse.failed (input_value='POOL_EXHAUSTED'), dlq.ai.generate.feedback 적체. Co-Authored-By: Claude Opus 4.8 --- ai/src/ai_server/model/messages/feedback.py | 4 +++- ai/tests/test_feedback_consumer.py | 23 ++++++++++++++++++- .../application/event/SessionEndedEvent.java | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/ai/src/ai_server/model/messages/feedback.py b/ai/src/ai_server/model/messages/feedback.py index 1cbfcd3b..9c73f01a 100644 --- a/ai/src/ai_server/model/messages/feedback.py +++ b/ai/src/ai_server/model/messages/feedback.py @@ -54,7 +54,9 @@ class GenerateFeedbackRequest(BaseModel): mode: InterviewMode job_category: Literal["FRONTEND", "BACKEND", "INFRA", "DBA"] total_question_count: int | None = None - end_reason: Literal["USER_REQUEST", "MAX_QUESTIONS_REACHED"] | None = None + # 종료 사유(USER_REQUEST/MAX_QUESTIONS_REACHED/POOL_EXHAUSTED 등). 프롬프트 컨텍스트로만 + # 쓰이므로 자유 문자열로 둬 Core 가 사유를 추가해도 피드백 파싱이 깨지지 않게 한다. + end_reason: str | None = None messages: list[FeedbackMessageItem] = Field(default_factory=list) context_document_ids: list[int] = Field(default_factory=list) voice_analysis_summary: VoiceAnalysisSummary | None = None diff --git a/ai/tests/test_feedback_consumer.py b/ai/tests/test_feedback_consumer.py index 1cc19e64..0fc1b86c 100644 --- a/ai/tests/test_feedback_consumer.py +++ b/ai/tests/test_feedback_consumer.py @@ -52,6 +52,7 @@ def _envelope( *, context_documents: list[int] | None = None, voice_analysis_summary: dict | None = None, + end_reason: str = "MAX_QUESTIONS_REACHED", ) -> bytes: env = { "messageId": "fb-1", @@ -65,7 +66,7 @@ def _envelope( "mode": "TECHNICAL", "jobCategory": "BACKEND", "totalQuestionCount": 2, - "endReason": "MAX_QUESTIONS_REACHED", + "endReason": end_reason, "messages": [ { "id": 100, @@ -131,6 +132,26 @@ async def test_consumer_generates_feedback_and_publishes_callback(): assert publisher.publish.await_args.kwargs["message_type"] == "callback.feedback" +@pytest.mark.asyncio +async def test_consumer_accepts_pool_exhausted_end_reason(): + # 회귀: Core 가 POOL_EXHAUSTED 로 종료해도 파싱 실패(DLQ) 없이 피드백 생성돼야 한다. + generator = _generator() + publisher = MagicMock() + publisher.publish = AsyncMock() + consumer = FeedbackConsumer( + generator=generator, + publisher=publisher, + idempotency=LruIdempotencyStore(max_size=10), + callback_routing_key="callback.feedback", + core_client=MagicMock(), + embedder=None, + ) + await consumer.handle(_StubMessage(_envelope(end_reason="POOL_EXHAUSTED"))) + + generator.generate.assert_awaited_once() + publisher.publish.assert_awaited_once() + + @pytest.mark.asyncio async def test_consumer_calls_rag_when_documents_and_embedder_present(): generator = _generator() diff --git a/backend/src/main/java/com/stackup/stackup/session/application/event/SessionEndedEvent.java b/backend/src/main/java/com/stackup/stackup/session/application/event/SessionEndedEvent.java index 55498bb0..4430c772 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/event/SessionEndedEvent.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/event/SessionEndedEvent.java @@ -1,7 +1,7 @@ package com.stackup.stackup.session.application.event; // 세션 종료(COMPLETED 전이) commit 후 발화. FeedbackRequester 가 수신. -// 사유: USER_REQUEST | MAX_QUESTIONS_REACHED +// 사유: USER_REQUEST | MAX_QUESTIONS_REACHED | POOL_EXHAUSTED public record SessionEndedEvent( Long userId, Long sessionId,