From 1d5154aceb40c5483021356a4bfe8ff56e32f269 Mon Sep 17 00:00:00 2001 From: pswaao88 Date: Mon, 1 Jun 2026 20:15:43 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=20=EC=A7=88?= =?UTF-8?q?=EB=AC=B8=20=EC=83=9D=EC=84=B1=20=EC=88=98=201=EA=B0=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/application/SessionQuestionsRequester.java | 6 ++++-- .../session/application/dto/GenerateQuestionsPayload.java | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/stackup/stackup/session/application/SessionQuestionsRequester.java b/backend/src/main/java/com/stackup/stackup/session/application/SessionQuestionsRequester.java index 011b4731..4c30ae32 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/SessionQuestionsRequester.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/SessionQuestionsRequester.java @@ -38,6 +38,7 @@ public class SessionQuestionsRequester { private static final JsonMapper JSON = JsonMapper.builder().build(); private static final TypeReference> TECH_STACK_TYPE = new TypeReference<>() {}; private static final long MAX_MARKDOWN_BYTES = 200_000L; // envelope 비대화 방지 + private static final int INITIAL_QUESTION_COUNT = 1; private final RabbitMessagePublisher publisher; private final RabbitMqProperties properties; @@ -53,6 +54,7 @@ public void onSessionCreated(SessionCreatedEvent event) { event.mode(), event.jobCategory(), documents, + INITIAL_QUESTION_COUNT, event.maxQuestions() ); publisher.publishToAi( @@ -60,8 +62,8 @@ public void onSessionCreated(SessionCreatedEvent event) { payload, new MessageContext(event.userId(), event.sessionId(), null, null) ); - log.info("generate.questions published. sessionId={}, doc_count={}, max={}", - event.sessionId(), documents.size(), event.maxQuestions()); + log.info("generate.questions published. sessionId={}, doc_count={}, initial_count={}, max={}", + event.sessionId(), documents.size(), INITIAL_QUESTION_COUNT, event.maxQuestions()); } private List buildDocumentContexts(List documentIds) { diff --git a/backend/src/main/java/com/stackup/stackup/session/application/dto/GenerateQuestionsPayload.java b/backend/src/main/java/com/stackup/stackup/session/application/dto/GenerateQuestionsPayload.java index 22684a92..25092e54 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/dto/GenerateQuestionsPayload.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/dto/GenerateQuestionsPayload.java @@ -9,6 +9,9 @@ public record GenerateQuestionsPayload( SessionMode mode, JobCategory jobCategory, List documents, + // Number of questions AI should generate for this initial callback. + Integer initialQuestionCount, + // Overall session question limit; not the generation batch size. Integer maxQuestions ) { public record DocumentContext( From 16640361dd84c2916fbdfc5fdeeec83e86f54348 Mon Sep 17 00:00:00 2001 From: pswaao88 Date: Mon, 1 Jun 2026 20:16:56 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EC=A7=88=EB=AC=B8=20=EC=BD=9C?= =?UTF-8?q?=EB=B0=B1=EC=97=90=EC=84=9C=20=EC=B2=AB=20=EC=A7=88=EB=AC=B8?= =?UTF-8?q?=EB=A7=8C=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/QuestionsCallbackService.java | 24 ++++--- .../dto/QuestionsCallbackPayload.java | 2 +- .../QuestionsCallbackServiceTest.java | 12 +++- .../SessionQuestionsRequesterTest.java | 68 +++++++++++++++++++ 4 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 backend/src/test/java/com/stackup/stackup/session/application/SessionQuestionsRequesterTest.java diff --git a/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java b/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java index 08615fa6..e193f93f 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/QuestionsCallbackService.java @@ -21,10 +21,10 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -// callback.questions 처리. -// POOL: 첫 질문을 interview_messages 에 INSERT + SSE session.message push. -// (전체 풀은 first cut 에선 메모리상 처리 — 후속 PR 에서 별도 테이블 또는 컬럼) -// FOLLOWUP: parent_message_id 매핑하여 INSERT + SSE push (B5 진행 시 활성) +// callback.questions handling. +// POOL is kept as the legacy callback kind, but Core treats it as the single +// initial question result. Extra questions are ignored; Core does not manage a pool. +// FOLLOWUP inserts a follow-up question and pushes it through SSE. @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -65,7 +65,7 @@ public void apply(QuestionsCallbackEnvelope envelope) { } if (payload.isPool()) { - applyPool(session, payload); + applyInitialQuestion(session, payload); } else if (payload.isFollowup()) { applyFollowup(session, payload); } else { @@ -74,13 +74,17 @@ public void apply(QuestionsCallbackEnvelope envelope) { markProcessed(envelope.messageId()); } - private void applyPool(InterviewSession session, QuestionsCallbackPayload payload) { + private void applyInitialQuestion(InterviewSession session, QuestionsCallbackPayload payload) { List questions = payload.questions(); if (questions == null || questions.isEmpty()) { - log.warn("callback.questions POOL with no questions. sessionId={}", session.getId()); + log.warn("callback.questions initial result with no questions. sessionId={}", session.getId()); return; } GeneratedQuestion first = questions.get(0); + if (questions.size() > 1) { + log.info("callback.questions initial result included extra questions; ignoring extras. sessionId={}, extra={}", + session.getId(), questions.size() - 1); + } InterviewMessage message = messageRepository.save( InterviewMessage.interviewer(session, 1, first.question()) ); @@ -90,10 +94,10 @@ private void applyPool(InterviewSession session, QuestionsCallbackPayload payloa sseEventPublisher.publishToUser( session.getUser().getId(), SseEventType.SESSION_MESSAGE, - new SessionMessageNotice(session.getId(), message.getId(), "QUESTION_POOL_READY") + new SessionMessageNotice(session.getId(), message.getId(), "INITIAL_QUESTION_READY") ); - log.info("callback.questions POOL processed. sessionId={}, total={}", - session.getId(), questions.size()); + log.info("callback.questions initial question processed. sessionId={}, ignored_extras={}", + session.getId(), Math.max(questions.size() - 1, 0)); } private void applyFollowup(InterviewSession session, QuestionsCallbackPayload payload) { diff --git a/backend/src/main/java/com/stackup/stackup/session/application/dto/QuestionsCallbackPayload.java b/backend/src/main/java/com/stackup/stackup/session/application/dto/QuestionsCallbackPayload.java index 9041e286..e17a0076 100644 --- a/backend/src/main/java/com/stackup/stackup/session/application/dto/QuestionsCallbackPayload.java +++ b/backend/src/main/java/com/stackup/stackup/session/application/dto/QuestionsCallbackPayload.java @@ -7,7 +7,7 @@ public record QuestionsCallbackPayload( Long sessionId, String kind, // POOL | FOLLOWUP - List questions, // POOL 시 사용 + List questions, // POOL legacy kind: initial question result Long parentMessageId, // FOLLOWUP 시 사용 String followupQuestion, // FOLLOWUP 시 사용 AnswerEvaluation answerEvaluation // FOLLOWUP 시 옵션 diff --git a/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java b/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java index b3ddc779..f3e8cf06 100644 --- a/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java +++ b/backend/src/test/java/com/stackup/stackup/session/application/QuestionsCallbackServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -26,6 +27,7 @@ import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -42,7 +44,7 @@ class QuestionsCallbackServiceTest { @InjectMocks QuestionsCallbackService service; @Test - void apply_poolInsertsFirstQuestionAndPushesSse() { + void apply_poolCallbackStoresOnlyInitialQuestionAndPushesSse() { InterviewSession session = sessionFixture(11L, SessionStatus.READY); QuestionsCallbackEnvelope env = poolEnvelope(11L, List.of( new GeneratedQuestion("INTRO", "Introduce yourself"), @@ -58,8 +60,14 @@ void apply_poolInsertsFirstQuestionAndPushesSse() { service.apply(env); - verify(messageRepository).save(any(InterviewMessage.class)); + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(InterviewMessage.class); + verify(messageRepository).save(messageCaptor.capture()); + assertThat(messageCaptor.getValue().getContent()).isEqualTo("Introduce yourself"); + assertThat(messageCaptor.getValue().getSequenceNumber()).isEqualTo(1); verify(sseEventPublisher).publishToSession(eq(11L), eq(SseEventType.SESSION_MESSAGE), any()); + verify(sseEventPublisher).publishToUser(eq(1L), eq(SseEventType.SESSION_MESSAGE), + argThat(payload -> payload instanceof QuestionsCallbackService.SessionMessageNotice notice + && "INITIAL_QUESTION_READY".equals(notice.reason()))); verify(processedMessageRepository).save(any(ProcessedMessage.class)); assertThat(session.getTotalQuestionCount()).isEqualTo(1); } diff --git a/backend/src/test/java/com/stackup/stackup/session/application/SessionQuestionsRequesterTest.java b/backend/src/test/java/com/stackup/stackup/session/application/SessionQuestionsRequesterTest.java new file mode 100644 index 00000000..75f7f5b6 --- /dev/null +++ b/backend/src/test/java/com/stackup/stackup/session/application/SessionQuestionsRequesterTest.java @@ -0,0 +1,68 @@ +package com.stackup.stackup.session.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.stackup.stackup.common.config.properties.RabbitMqProperties; +import com.stackup.stackup.common.messaging.MessageContext; +import com.stackup.stackup.common.messaging.RabbitMessagePublisher; +import com.stackup.stackup.common.storage.ObjectStorageClient; +import com.stackup.stackup.document.domain.AnalyzedDocumentRepository; +import com.stackup.stackup.session.application.dto.GenerateQuestionsPayload; +import com.stackup.stackup.session.application.event.SessionCreatedEvent; +import com.stackup.stackup.session.domain.JobCategory; +import com.stackup.stackup.session.domain.SessionMode; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class SessionQuestionsRequesterTest { + + @Mock RabbitMessagePublisher publisher; + @Mock RabbitMqProperties properties; + @Mock AnalyzedDocumentRepository documentRepository; + @Mock ObjectStorageClient storage; + @InjectMocks SessionQuestionsRequester requester; + + @Test + void onSessionCreated_requestsOneInitialQuestionAndKeepsSessionLimit() { + when(properties.routingKeys()).thenReturn(mockRoutingKeys()); + + requester.onSessionCreated(new SessionCreatedEvent( + 1L, + 11L, + SessionMode.INTEGRATED, + JobCategory.BACKEND, + 5, + List.of() + )); + + ArgumentCaptor payloadCaptor = + ArgumentCaptor.forClass(GenerateQuestionsPayload.class); + verify(publisher).publishToAi(eq("generate.questions"), payloadCaptor.capture(), any(MessageContext.class)); + + GenerateQuestionsPayload payload = payloadCaptor.getValue(); + assertThat(payload.sessionId()).isEqualTo(11L); + assertThat(payload.mode()).isEqualTo(SessionMode.INTEGRATED); + assertThat(payload.jobCategory()).isEqualTo(JobCategory.BACKEND); + assertThat(payload.documents()).isEmpty(); + assertThat(payload.initialQuestionCount()).isEqualTo(1); + assertThat(payload.maxQuestions()).isEqualTo(5); + } + + private RabbitMqProperties.RoutingKeyProperties mockRoutingKeys() { + return new RabbitMqProperties.RoutingKeyProperties( + "analyze.resume", "analyze.repository", + "generate.questions", "generate.followup", "generate.feedback", "analyze.voice", + "callback.analysis", "callback.questions", "callback.feedback", "callback.voice", + "realtime.session.notify"); + } +}