feat(trace): bind feedback to runs
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package com.superbiz.agent.controller;
|
||||
|
||||
import com.superbiz.agent.dto.FeedbackRequest;
|
||||
import com.superbiz.agent.dto.FeedbackResponse;
|
||||
import com.superbiz.agent.service.FeedbackService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class FeedbackControllerTest {
|
||||
|
||||
@Test
|
||||
void submitFeedbackPassesRunIdToService() {
|
||||
FeedbackController controller = new FeedbackController();
|
||||
FeedbackService feedbackService = mock(FeedbackService.class);
|
||||
ReflectionTestUtils.setField(controller, "feedbackService", feedbackService);
|
||||
|
||||
FeedbackRequest request = new FeedbackRequest();
|
||||
request.setSessionId("session-1");
|
||||
request.setRunId("run-1");
|
||||
request.setFeedback("useful");
|
||||
|
||||
when(feedbackService.submitFeedback("session-1", "run-1", "useful"))
|
||||
.thenReturn(FeedbackResponse.builder()
|
||||
.success(true)
|
||||
.runId("run-1")
|
||||
.fallbackToLatestRun(false)
|
||||
.build());
|
||||
|
||||
ResponseEntity<FeedbackResponse> response = controller.submitFeedback(request);
|
||||
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertTrue(response.getBody().isSuccess());
|
||||
assertEquals("run-1", response.getBody().getRunId());
|
||||
verify(feedbackService).submitFeedback("session-1", "run-1", "useful");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.superbiz.agent.service;
|
||||
|
||||
import com.superbiz.agent.domain.entity.CaseLibrary;
|
||||
import com.superbiz.agent.domain.entity.DiagnosisRun;
|
||||
import com.superbiz.agent.domain.entity.DiagnosisSession;
|
||||
import com.superbiz.agent.domain.enums.SourceType;
|
||||
import com.superbiz.agent.repository.CaseLibraryRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class CaseLibraryServiceTest {
|
||||
|
||||
private final CaseLibraryRepository caseLibraryRepository = mock(CaseLibraryRepository.class);
|
||||
private final CaseLibraryService service = new CaseLibraryService();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(service, "caseLibraryRepository", caseLibraryRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromRunStoresRunIdAsDiagnosisIdAndUsesRunContent() {
|
||||
DiagnosisRun run = DiagnosisRun.builder()
|
||||
.sessionId("session-1")
|
||||
.runId("run-1")
|
||||
.query("payment timeout")
|
||||
.answer("redis timeout caused payment latency")
|
||||
.build();
|
||||
when(caseLibraryRepository.findByDiagnosisId("run-1")).thenReturn(Optional.empty());
|
||||
when(caseLibraryRepository.save(any(CaseLibrary.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
CaseLibrary saved = service.createFromRun(run);
|
||||
|
||||
assertEquals("run-1", saved.getDiagnosisId());
|
||||
assertEquals("payment timeout", saved.getTitle());
|
||||
assertEquals("redis timeout caused payment latency", saved.getRootCause());
|
||||
assertEquals("redis timeout caused payment latency", saved.getSolution());
|
||||
assertEquals(SourceType.AUTO, saved.getSourceType());
|
||||
assertNotNull(saved.getCaseId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromRunReusesExistingCaseForIdempotency() {
|
||||
DiagnosisRun run = DiagnosisRun.builder()
|
||||
.runId("run-existing")
|
||||
.query("query")
|
||||
.answer("answer")
|
||||
.build();
|
||||
CaseLibrary existing = CaseLibrary.builder()
|
||||
.caseId("case-existing")
|
||||
.diagnosisId("run-existing")
|
||||
.build();
|
||||
when(caseLibraryRepository.findByDiagnosisId("run-existing")).thenReturn(Optional.of(existing));
|
||||
|
||||
CaseLibrary saved = service.createFromRun(run);
|
||||
|
||||
assertEquals("case-existing", saved.getCaseId());
|
||||
verify(caseLibraryRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromSessionPreservesLegacySessionIdSemantics() {
|
||||
DiagnosisSession session = DiagnosisSession.builder()
|
||||
.sessionId("legacy-session")
|
||||
.query("legacy query")
|
||||
.answer("legacy answer")
|
||||
.build();
|
||||
when(caseLibraryRepository.findByDiagnosisId("legacy-session")).thenReturn(Optional.empty());
|
||||
when(caseLibraryRepository.save(any(CaseLibrary.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
service.createFromSession(session);
|
||||
|
||||
ArgumentCaptor<CaseLibrary> captor = ArgumentCaptor.forClass(CaseLibrary.class);
|
||||
verify(caseLibraryRepository).save(captor.capture());
|
||||
assertEquals("legacy-session", captor.getValue().getDiagnosisId());
|
||||
assertEquals("legacy answer", captor.getValue().getRootCause());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.superbiz.agent.service;
|
||||
|
||||
import com.superbiz.agent.domain.entity.CaseLibrary;
|
||||
import com.superbiz.agent.domain.entity.DiagnosisRun;
|
||||
import com.superbiz.agent.domain.entity.DiagnosisSession;
|
||||
import com.superbiz.agent.dto.FeedbackResponse;
|
||||
import com.superbiz.agent.repository.DiagnosisRunRepository;
|
||||
import com.superbiz.agent.repository.DiagnosisSessionRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class FeedbackServiceTest {
|
||||
|
||||
private final DiagnosisSessionRepository diagnosisSessionRepository = mock(DiagnosisSessionRepository.class);
|
||||
private final DiagnosisRunRepository diagnosisRunRepository = mock(DiagnosisRunRepository.class);
|
||||
private final CaseLibraryService caseLibraryService = mock(CaseLibraryService.class);
|
||||
private final FeedbackService service = new FeedbackService();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(service, "diagnosisSessionRepository", diagnosisSessionRepository);
|
||||
ReflectionTestUtils.setField(service, "diagnosisRunRepository", diagnosisRunRepository);
|
||||
ReflectionTestUtils.setField(service, "caseLibraryService", caseLibraryService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitFeedbackWithRunIdUpdatesSpecifiedRun() {
|
||||
DiagnosisRun run = run("session-1", "run-1");
|
||||
when(diagnosisRunRepository.findBySessionIdAndRunId("session-1", "run-1"))
|
||||
.thenReturn(Optional.of(run));
|
||||
|
||||
FeedbackResponse response = service.submitFeedback("session-1", "run-1", "not_useful");
|
||||
|
||||
assertTrue(response.isSuccess());
|
||||
assertEquals("run-1", response.getRunId());
|
||||
assertFalse(response.isFallbackToLatestRun());
|
||||
assertEquals("not_useful", run.getFeedback());
|
||||
verify(diagnosisRunRepository).save(run);
|
||||
verifyNoInteractions(caseLibraryService);
|
||||
verifyNoInteractions(diagnosisSessionRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitFeedbackWithoutRunIdFallsBackToLatestRunObservably() {
|
||||
DiagnosisRun latest = run("session-1", "run-latest");
|
||||
when(diagnosisRunRepository.findFirstBySessionIdOrderByCreatedAtDescIdDesc("session-1"))
|
||||
.thenReturn(Optional.of(latest));
|
||||
when(diagnosisRunRepository.findBySessionIdAndRunId("session-1", "run-latest"))
|
||||
.thenReturn(Optional.of(latest));
|
||||
|
||||
FeedbackResponse response = service.submitFeedback("session-1", null, "not_useful");
|
||||
|
||||
assertTrue(response.isSuccess());
|
||||
assertEquals("run-latest", response.getRunId());
|
||||
assertTrue(response.isFallbackToLatestRun());
|
||||
assertEquals("not_useful", latest.getFeedback());
|
||||
verify(diagnosisRunRepository).save(latest);
|
||||
verify(diagnosisSessionRepository, never()).findBySessionId(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitFeedbackRejectsRunFromAnotherSession() {
|
||||
when(diagnosisRunRepository.findBySessionIdAndRunId("session-1", "run-other"))
|
||||
.thenReturn(Optional.empty());
|
||||
when(diagnosisRunRepository.findByRunId("run-other"))
|
||||
.thenReturn(Optional.of(run("session-2", "run-other")));
|
||||
|
||||
FeedbackResponse response = service.submitFeedback("session-1", "run-other", "useful");
|
||||
|
||||
assertFalse(response.isSuccess());
|
||||
assertEquals("run-other", response.getRunId());
|
||||
verify(diagnosisRunRepository, never()).save(any());
|
||||
verifyNoInteractions(caseLibraryService);
|
||||
verifyNoInteractions(diagnosisSessionRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void usefulFeedbackCreatesCaseFromRun() {
|
||||
DiagnosisRun run = run("session-1", "run-useful");
|
||||
CaseLibrary caseLibrary = CaseLibrary.builder().caseId("case-1").build();
|
||||
when(diagnosisRunRepository.findBySessionIdAndRunId("session-1", "run-useful"))
|
||||
.thenReturn(Optional.of(run));
|
||||
when(caseLibraryService.createFromRun(run)).thenReturn(caseLibrary);
|
||||
|
||||
FeedbackResponse response = service.submitFeedback("session-1", "run-useful", "useful");
|
||||
|
||||
assertTrue(response.isSuccess());
|
||||
assertEquals("case-1", response.getCaseId());
|
||||
assertEquals("run-useful", response.getRunId());
|
||||
verify(caseLibraryService).createFromRun(run);
|
||||
verify(diagnosisRunRepository).save(run);
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacySessionFallbackPreservesOldDataCompatibility() {
|
||||
DiagnosisSession session = DiagnosisSession.builder()
|
||||
.sessionId("legacy-session")
|
||||
.query("legacy query")
|
||||
.answer("legacy answer")
|
||||
.build();
|
||||
CaseLibrary caseLibrary = CaseLibrary.builder().caseId("legacy-case").build();
|
||||
when(diagnosisRunRepository.findFirstBySessionIdOrderByCreatedAtDescIdDesc("legacy-session"))
|
||||
.thenReturn(Optional.empty());
|
||||
when(diagnosisSessionRepository.findBySessionId("legacy-session")).thenReturn(Optional.of(session));
|
||||
when(caseLibraryService.createFromSession(session)).thenReturn(caseLibrary);
|
||||
|
||||
FeedbackResponse response = service.submitFeedback("legacy-session", null, "useful");
|
||||
|
||||
assertTrue(response.isSuccess());
|
||||
assertNull(response.getRunId());
|
||||
assertFalse(response.isFallbackToLatestRun());
|
||||
assertEquals("legacy-case", response.getCaseId());
|
||||
assertEquals("useful", session.getFeedback());
|
||||
verify(diagnosisSessionRepository).save(session);
|
||||
verify(caseLibraryService).createFromSession(session);
|
||||
}
|
||||
|
||||
private DiagnosisRun run(String sessionId, String runId) {
|
||||
return DiagnosisRun.builder()
|
||||
.sessionId(sessionId)
|
||||
.runId(runId)
|
||||
.query("query for " + runId)
|
||||
.answer("answer for " + runId)
|
||||
.status("SUCCESS")
|
||||
.agentFlow("CHAT")
|
||||
.createdAt(LocalDateTime.of(2026, 7, 10, 10, 0))
|
||||
.updatedAt(LocalDateTime.of(2026, 7, 10, 10, 1))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user