fix(ocr): replace IllegalStateException with DomainException.internal in triggerManualSenderTraining
Ensures the unexpected-state path produces a structured JSON error response instead of an unmapped 500 RuntimeException. Adds OCR_TRAINING_CONFLICT ErrorCode and mirrors it in the frontend errors.ts. Adds coverage tests for getAllSenderModels() and runSenderTraining(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,8 @@ public enum ErrorCode {
|
||||
OCR_PROCESSING_FAILED,
|
||||
/** A training run is already in progress. 409 */
|
||||
TRAINING_ALREADY_RUNNING,
|
||||
/** Internal inconsistency: expected training run row was not found after creation. 500 */
|
||||
OCR_TRAINING_CONFLICT,
|
||||
|
||||
// --- Tags ---
|
||||
/** A tag with the given ID does not exist. 404 */
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.raddatz.familienarchiv.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||
import org.raddatz.familienarchiv.model.OcrTrainingRun;
|
||||
import org.raddatz.familienarchiv.model.SenderModel;
|
||||
import org.raddatz.familienarchiv.model.TrainingStatus;
|
||||
@@ -64,8 +66,9 @@ public class SenderModelService {
|
||||
boolean runNow = runOrQueueSenderTraining(personId, (int) correctedLines);
|
||||
TrainingStatus targetStatus = runNow ? TrainingStatus.RUNNING : TrainingStatus.QUEUED;
|
||||
OcrTrainingRun run = trainingRunRepository.findFirstByPersonIdAndStatus(personId, targetStatus)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"Expected " + targetStatus + " row for person " + personId));
|
||||
.orElseThrow(() -> DomainException.internal(
|
||||
ErrorCode.OCR_TRAINING_CONFLICT,
|
||||
"Expected " + targetStatus + " run for person " + personId));
|
||||
if (runNow) {
|
||||
self.runSenderTraining(personId);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,38 @@ class SenderModelServiceTest {
|
||||
ReflectionTestUtils.setField(service, "retrainDelta", 50);
|
||||
}
|
||||
|
||||
// ─── getAllSenderModels ───────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void getAllSenderModels_returnsAllModelsFromRepository() {
|
||||
SenderModel model = SenderModel.builder()
|
||||
.id(UUID.randomUUID()).personId(personId).correctedLinesAtTraining(100).build();
|
||||
when(senderModelRepository.findAll()).thenReturn(java.util.List.of(model));
|
||||
|
||||
java.util.List<SenderModel> result = service.getAllSenderModels();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getPersonId()).isEqualTo(personId);
|
||||
}
|
||||
|
||||
// ─── runSenderTraining ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void runSenderTraining_queriesBlockCountForPerson() {
|
||||
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(42L);
|
||||
// triggerSenderTraining needs a RUNNING row — return empty to abort early
|
||||
when(trainingRunRepository.findFirstByPersonIdAndStatus(personId, TrainingStatus.RUNNING))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
try {
|
||||
service.runSenderTraining(personId);
|
||||
} catch (Exception ignored) {
|
||||
// triggerSenderTraining will throw when no RUNNING row found
|
||||
}
|
||||
|
||||
verify(blockRepository).countManualKurrentBlocksByPerson(personId);
|
||||
}
|
||||
|
||||
// ─── Activation threshold ─────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@@ -318,6 +350,25 @@ class SenderModelServiceTest {
|
||||
.isInstanceOf(DomainException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void triggerManualSenderTraining_throwsDomainException_whenRunRowMissingAfterCreate() {
|
||||
when(personService.getById(personId)).thenReturn(Person.builder().id(personId).build());
|
||||
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(0L);
|
||||
when(trainingRunRepository.existsByPersonIdAndStatus(personId, TrainingStatus.QUEUED)).thenReturn(false);
|
||||
when(trainingRunRepository.findFirstByStatus(TrainingStatus.RUNNING)).thenReturn(Optional.empty());
|
||||
OcrTrainingRun runningRun = OcrTrainingRun.builder()
|
||||
.id(UUID.randomUUID()).status(TrainingStatus.RUNNING)
|
||||
.personId(personId).blockCount(0).documentCount(0).modelName("sender_" + personId).build();
|
||||
when(trainingRunRepository.save(any())).thenReturn(runningRun);
|
||||
// Simulate the run row not being found after creation (defensive path)
|
||||
when(trainingRunRepository.findFirstByPersonIdAndStatus(personId, TrainingStatus.RUNNING))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
org.assertj.core.api.Assertions.assertThatThrownBy(
|
||||
() -> service.triggerManualSenderTraining(personId))
|
||||
.isInstanceOf(DomainException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void triggerSenderTraining_promotesNextQueued_afterCompletion() throws Exception {
|
||||
UUID nextPersonId = UUID.randomUUID();
|
||||
|
||||
@@ -27,6 +27,7 @@ export type ErrorCode =
|
||||
| 'OCR_DOCUMENT_NOT_UPLOADED'
|
||||
| 'OCR_PROCESSING_FAILED'
|
||||
| 'TRAINING_ALREADY_RUNNING'
|
||||
| 'OCR_TRAINING_CONFLICT'
|
||||
| 'INVALID_TAG_COLOR'
|
||||
| 'TAG_CYCLE_DETECTED'
|
||||
| 'TAG_NOT_FOUND'
|
||||
@@ -105,6 +106,8 @@ export function getErrorMessage(code: ErrorCode | string | undefined): string {
|
||||
return m.error_ocr_processing_failed();
|
||||
case 'TRAINING_ALREADY_RUNNING':
|
||||
return m.error_training_already_running();
|
||||
case 'OCR_TRAINING_CONFLICT':
|
||||
return m.error_internal_error();
|
||||
case 'INVALID_TAG_COLOR':
|
||||
return m.error_invalid_tag_color();
|
||||
case 'TAG_CYCLE_DETECTED':
|
||||
|
||||
Reference in New Issue
Block a user