Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,10 @@ docker compose up -d
정상 질문이면 그대로(답하면 됨) / "(생성 중)" placeholder 면 `failFollowup` + 다음 일반질문 /
자기소개 답변인데 풀이 0건이면 `SelfIntroAnsweredEvent` 재발행(넘기면 POOL_EXHAUSTED 로
세션이 끝나버린다) / 그 외 답변이면 다음 일반질문.
- **재개는 terminal 가드의 전제를 깬다.** 종료 세션에 늦게 온 콜백은 `isTerminal()` 로 드롭되지만,
재개된 세션은 IN_PROGRESS 라 그대로 통과한다. 그래서 `applyFollowup` 이 **이미 FAILED 인
placeholder 를 되살리지 않도록** 막는다 — 복구가 실패 확정 + 다음 질문까지 마친 뒤 늦은 콜백이
그 자리를 채우면 살아있는 질문이 두 개가 된다. POOL 은 `countBySessionId > 0` 로 이미 멱등.
- **Spring AI 미사용** — LLM·임베딩 호출은 모두 AI 서버 위임. Core는 RabbitMQ 발행만 담당.
- **Redis 미사용** — 휘발성 데이터는 DB short-lived 레코드 또는 인메모리로.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.stackup.stackup.session.domain.InterviewSession;
import com.stackup.stackup.session.domain.InterviewSessionRepository;
import com.stackup.stackup.session.domain.MessageRole;
import com.stackup.stackup.session.domain.MessageStatus;
import com.stackup.stackup.session.domain.SessionQuestionPool;
import com.stackup.stackup.session.domain.SessionQuestionPoolRepository;
import com.stackup.stackup.session.domain.SessionStatus;
Expand Down Expand Up @@ -248,6 +249,16 @@ private void applyFollowup(InterviewSession session, QuestionsCallbackPayload pa
? null
: messageRepository.findById(payload.followupMessageId()).orElse(null);

// 이미 실패로 확정된 placeholder 면 그 턴은 지나갔다 — 이어하기 복구
// (SessionResumeService.recoverTurn)가 실패 처리하고 다음 일반질문으로 넘긴 뒤,
// 늦게 도착한 콜백이 그 자리를 되살리면 살아있는 질문이 두 개가 된다.
// 종료 세션은 terminal 가드가 막지만, 재개된 세션은 IN_PROGRESS 라 여기까지 온다.
if (placeholder != null && placeholder.getStatus() == MessageStatus.FAILED) {
log.info("callback.questions FOLLOWUP dropped — placeholder already failed (turn moved on). "
+ "sessionId={}, msg={}", session.getId(), placeholder.getId());
return;
}

// 모르겠음 → 이 주제 그만, 다음 일반질문. placeholder 는 삭제(seq 연속성 유지).
if ("DONT_KNOW".equalsIgnoreCase(intent)) {
recordAnswerEvaluation(payload);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,39 @@ void apply_followupNormal_updatesPlaceholderInPlaceWithoutCounting() {
assertThat(cap.getValue()).isSameAs(placeholder);
}

// 이어하기 복구가 placeholder 를 실패로 확정하고 다음 일반질문으로 넘긴 뒤, 늦게 도착한
// 콜백이 그 자리를 되살리면 살아있는 질문이 두 개가 된다. 종료 세션은 terminal 가드가
// 막지만 재개된 세션은 IN_PROGRESS 라 여기까지 온다.
@Test
void apply_followupOnFailedPlaceholder_isDropped() {
InterviewSession session = sessionFixture(25L, SessionStatus.IN_PROGRESS);
InterviewMessage placeholder = InterviewMessage.followupPlaceholder(
session, 3, parentMessageFixture(session));
ReflectionTestUtils.setField(placeholder, "id", 305L);
placeholder.failFollowup(); // 재개 복구가 이미 실패로 확정한 상태

QuestionsCallbackPayload payload = new QuestionsCallbackPayload(
25L, "FOLLOWUP", null, 200L, null, "뒤늦게 도착한 꼬리질문?",
null, "NORMAL", 305L
);
QuestionsCallbackEnvelope env = new QuestionsCallbackEnvelope(
"m-late-followup", "callback.questions", "1", "t", null, "ai", payload, null);

when(processedMessageRepository.existsById("m-late-followup")).thenReturn(false);
when(sessionRepository.findById(25L)).thenReturn(Optional.of(session));
when(messageRepository.findById(200L)).thenReturn(Optional.of(parentMessageFixture(session)));
when(messageRepository.findById(305L)).thenReturn(Optional.of(placeholder));

service.apply(env);

// 실패 상태·문구가 그대로여야 한다(되살아나면 안 된다).
assertThat(placeholder.getStatus())
.isEqualTo(com.stackup.stackup.session.domain.MessageStatus.FAILED);
assertThat(placeholder.getContent())
.isEqualTo(InterviewMessage.FOLLOWUP_GENERATION_FAILED_TEXT);
verify(messageRepository, never()).save(any(InterviewMessage.class));
}

@Test
void apply_followupClarification_updatesPlaceholderWithoutCounting() {
InterviewSession session = sessionFixture(21L, SessionStatus.IN_PROGRESS);
Expand Down
Loading