|
|
|
|
@ -501,6 +501,124 @@ class AiTrainingEvaluationServiceImplTest {
|
|
|
|
|
verify(service.qwenChatClient, never()).completeJson(any(), any());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void recentProcessingAssessmentIsNotReclaimed() {
|
|
|
|
|
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
|
|
|
|
|
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "PROCESSING");
|
|
|
|
|
evaluation.setUpdateTime(minutesAgo(1));
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1")).thenReturn(evaluation);
|
|
|
|
|
|
|
|
|
|
AiTrainingEvaluationView view = service.generateAssessment("new-product-survey", student());
|
|
|
|
|
|
|
|
|
|
assertEquals("PROCESSING", view.getAssessmentStatus());
|
|
|
|
|
verify(service.aiTrainingEvaluationMapper, never()).claimAssessment(any(), any(Date.class));
|
|
|
|
|
verify(service.qwenChatClient, never()).completeJson(any(), any());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void staleProcessingAssessmentIsReclaimedSoStudentIsNotStuckForever() {
|
|
|
|
|
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
|
|
|
|
|
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "PROCESSING");
|
|
|
|
|
evaluation.setUpdateTime(minutesAgo(11));
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1")).thenReturn(evaluation);
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1);
|
|
|
|
|
when(service.qwenChatClient.completeJson(any(), any())).thenReturn(assessmentJson());
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.completeAssessment(any(AiTrainingEvaluation.class))).thenReturn(1);
|
|
|
|
|
when(service.studentTrainingAnswerMapper.updateAiAssessmentScore(eq("answer-1"), eq(86), any(Date.class))).thenReturn(1);
|
|
|
|
|
|
|
|
|
|
AiTrainingEvaluationView view = service.generateAssessment("new-product-survey", student());
|
|
|
|
|
|
|
|
|
|
assertEquals(Integer.valueOf(86), view.getAssessmentScore());
|
|
|
|
|
verify(service.aiTrainingEvaluationMapper).claimAssessment(eq("evaluation-1"), any(Date.class));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void failureRecordingErrorFallsBackToShortRecordAndKeepsStudentRetryable() {
|
|
|
|
|
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
|
|
|
|
|
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
|
|
|
|
|
String invalidResponse = assessmentJson().replace("\"maxScore\":25", "\"maxScore\":24");
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
|
|
|
|
|
.thenReturn(null, evaluation);
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1);
|
|
|
|
|
when(service.qwenChatClient.completeJson(any(), any())).thenReturn(invalidResponse);
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.failAssessment(eq("evaluation-1"), any(), eq(invalidResponse), any(Date.class)))
|
|
|
|
|
.thenThrow(new org.apache.ibatis.exceptions.PersistenceException(longDatabaseMessage()));
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.failAssessment(eq("evaluation-1"), any(), isNull(), any(Date.class))).thenReturn(1);
|
|
|
|
|
|
|
|
|
|
ServiceException thrown = assertThrows(ServiceException.class,
|
|
|
|
|
() -> service.generateAssessment("new-product-survey", student()));
|
|
|
|
|
|
|
|
|
|
assertShortFriendlyMessage(thrown.getMessage());
|
|
|
|
|
verify(service.aiTrainingEvaluationMapper).failAssessment(eq("evaluation-1"), any(), isNull(), any(Date.class));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void failureRecordingBothAttemptsFailingStillReturnsShortError() {
|
|
|
|
|
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
|
|
|
|
|
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
|
|
|
|
|
.thenReturn(null, evaluation);
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1);
|
|
|
|
|
when(service.qwenChatClient.completeJson(any(), any())).thenThrow(new IllegalStateException("upstream"));
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.failAssessment(any(), any(), any(), any(Date.class)))
|
|
|
|
|
.thenThrow(new org.apache.ibatis.exceptions.PersistenceException(longDatabaseMessage()));
|
|
|
|
|
|
|
|
|
|
ServiceException thrown = assertThrows(ServiceException.class,
|
|
|
|
|
() -> service.generateAssessment("new-product-survey", student()));
|
|
|
|
|
|
|
|
|
|
assertShortFriendlyMessage(thrown.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void helpFailureRecordingErrorStillReturnsShortError() {
|
|
|
|
|
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
|
|
|
|
|
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
|
|
|
|
|
.thenReturn(null, evaluation);
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.claimHelp(eq("evaluation-1"), any(Date.class))).thenReturn(1);
|
|
|
|
|
when(service.qwenChatClient.completeJson(any(), any())).thenThrow(new IllegalStateException("upstream"));
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.failHelp(any(), any(), any(Date.class)))
|
|
|
|
|
.thenThrow(new org.apache.ibatis.exceptions.PersistenceException(longDatabaseMessage()));
|
|
|
|
|
|
|
|
|
|
ServiceException thrown = assertThrows(ServiceException.class,
|
|
|
|
|
() -> service.generateHelp("new-product-survey", student(), "如何完善调研证据?"));
|
|
|
|
|
|
|
|
|
|
assertShortFriendlyMessage(thrown.getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void storedFailureMessageNeverContainsInternalExceptionText() {
|
|
|
|
|
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
|
|
|
|
|
AiTrainingEvaluation evaluation = evaluation("NOT_STARTED", "NOT_STARTED");
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.findByStudentClassAndTask("stu-1", "class-1", "task-1"))
|
|
|
|
|
.thenReturn(null, evaluation);
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.claimAssessment(eq("evaluation-1"), any(Date.class))).thenReturn(1);
|
|
|
|
|
when(service.qwenChatClient.completeJson(any(), any())).thenThrow(new IllegalStateException(longDatabaseMessage()));
|
|
|
|
|
when(service.aiTrainingEvaluationMapper.failAssessment(any(), any(), any(), any(Date.class))).thenReturn(1);
|
|
|
|
|
|
|
|
|
|
assertThrows(ServiceException.class, () -> service.generateAssessment("new-product-survey", student()));
|
|
|
|
|
|
|
|
|
|
ArgumentCaptor<String> stored = ArgumentCaptor.forClass(String.class);
|
|
|
|
|
verify(service.aiTrainingEvaluationMapper).failAssessment(eq("evaluation-1"), stored.capture(), any(), any(Date.class));
|
|
|
|
|
assertShortFriendlyMessage(stored.getValue());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String longDatabaseMessage() {
|
|
|
|
|
return "\n### Error updating database. Cause: java.sql.SQLException: Incorrect string value"
|
|
|
|
|
+ "\n### SQL: UPDATE ai_training_evaluation SET assessment_raw_response = ? WHERE id = ?";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void assertShortFriendlyMessage(String message) {
|
|
|
|
|
assertNotNull(message);
|
|
|
|
|
assertTrue(message.length() <= 60, "message too long: " + message);
|
|
|
|
|
assertTrue(!message.contains("###") && !message.contains("SQL") && !message.contains("Exception"),
|
|
|
|
|
"internal detail leaked: " + message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Date minutesAgo(int minutes) {
|
|
|
|
|
return new Date(System.currentTimeMillis() - minutes * 60_000L);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
void rejectsNonStudentJwtUser() {
|
|
|
|
|
AiTrainingEvaluationServiceImpl service = serviceWithContext(answer("answer-1", "step one", null, null, null));
|
|
|
|
|
|