refactor(ocr-training): route SenderModelService and OcrTrainingService through TranscriptionBlockQueryService

Both services injected TranscriptionBlockRepository directly to read block
counts. They now go through TranscriptionBlockQueryService (count() and
countManualKurrentBlocksByPerson() added as 1-line delegations) — chosen over
TranscriptionService to avoid the existing
SenderModelService → TrainingDataExportService → TranscriptionBlockQueryService
chain reaching back into TranscriptionService and creating a cycle.

Refs #417 (C6.2 violations #8 and #9).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-05 07:40:34 +02:00
parent 310bb5b2d5
commit f5151f3949
5 changed files with 38 additions and 34 deletions

View File

@@ -11,7 +11,6 @@ import org.raddatz.familienarchiv.model.SenderModel;
import org.raddatz.familienarchiv.model.TrainingStatus;
import org.raddatz.familienarchiv.model.TranscriptionBlock;
import org.raddatz.familienarchiv.repository.OcrTrainingRunRepository;
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
import org.raddatz.familienarchiv.service.PersonService;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
@@ -34,7 +33,7 @@ class OcrTrainingServiceTest {
SegmentationTrainingExportService segExportService;
OcrClient ocrClient;
OcrHealthClient healthClient;
TranscriptionBlockRepository blockRepository;
TranscriptionBlockQueryService transcriptionBlockQueryService;
TransactionTemplate txTemplate;
PersonService personService;
SenderModelService senderModelService;
@@ -47,7 +46,7 @@ class OcrTrainingServiceTest {
segExportService = mock(SegmentationTrainingExportService.class);
ocrClient = mock(OcrClient.class);
healthClient = mock(OcrHealthClient.class);
blockRepository = mock(TranscriptionBlockRepository.class);
transcriptionBlockQueryService = mock(TranscriptionBlockQueryService.class);
txTemplate = mock(TransactionTemplate.class);
personService = mock(PersonService.class);
senderModelService = mock(SenderModelService.class);
@@ -58,9 +57,9 @@ class OcrTrainingServiceTest {
return callback.doInTransaction(null);
});
service = new OcrTrainingService(runRepository, exportService, segExportService, ocrClient, healthClient, blockRepository, txTemplate, personService, senderModelService);
service = new OcrTrainingService(runRepository, exportService, segExportService, ocrClient, healthClient, transcriptionBlockQueryService, txTemplate, personService, senderModelService);
when(blockRepository.count()).thenReturn(0L);
when(transcriptionBlockQueryService.count()).thenReturn(0L);
when(runRepository.findTop20ByOrderByCreatedAtDesc()).thenReturn(List.of());
when(segExportService.querySegmentationBlocks()).thenReturn(List.of());
when(senderModelService.getAllSenderModels()).thenReturn(List.of());

View File

@@ -12,7 +12,6 @@ import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.model.Person;
import org.raddatz.familienarchiv.repository.OcrTrainingRunRepository;
import org.raddatz.familienarchiv.repository.SenderModelRepository;
import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
@@ -28,7 +27,7 @@ import static org.mockito.Mockito.*;
class SenderModelServiceTest {
SenderModelRepository senderModelRepository;
TranscriptionBlockRepository blockRepository;
TranscriptionBlockQueryService transcriptionBlockQueryService;
OcrTrainingRunRepository trainingRunRepository;
OcrClient ocrClient;
TransactionTemplate txTemplate;
@@ -42,7 +41,7 @@ class SenderModelServiceTest {
@BeforeEach
void setUp() {
senderModelRepository = mock(SenderModelRepository.class);
blockRepository = mock(TranscriptionBlockRepository.class);
transcriptionBlockQueryService = mock(TranscriptionBlockQueryService.class);
trainingRunRepository = mock(OcrTrainingRunRepository.class);
ocrClient = mock(OcrClient.class);
txTemplate = mock(TransactionTemplate.class);
@@ -57,7 +56,7 @@ class SenderModelServiceTest {
return callback.doInTransaction(null);
});
service = new SenderModelService(senderModelRepository, blockRepository,
service = new SenderModelService(senderModelRepository, transcriptionBlockQueryService,
trainingRunRepository, ocrClient, txTemplate, trainingDataExportService, personService);
ReflectionTestUtils.setField(service, "self", selfProxy);
ReflectionTestUtils.setField(service, "activationThreshold", 100);
@@ -82,7 +81,7 @@ class SenderModelServiceTest {
@Test
void runSenderTraining_queriesBlockCountForPerson() {
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(42L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(42L);
// triggerSenderTraining needs a RUNNING row — return empty to abort early
when(trainingRunRepository.findFirstByPersonIdAndStatus(personId, TrainingStatus.RUNNING))
.thenReturn(Optional.empty());
@@ -93,14 +92,14 @@ class SenderModelServiceTest {
// triggerSenderTraining will throw when no RUNNING row found
}
verify(blockRepository).countManualKurrentBlocksByPerson(personId);
verify(transcriptionBlockQueryService).countManualKurrentBlocksByPerson(personId);
}
// ─── Activation threshold ─────────────────────────────────────────────────
@Test
void checkAndTriggerTraining_doesNothing_belowActivationThreshold() {
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(99L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(99L);
when(senderModelRepository.findByPersonId(personId)).thenReturn(Optional.empty());
SenderModelService spy = spy(service);
@@ -111,7 +110,7 @@ class SenderModelServiceTest {
@Test
void checkAndTriggerTraining_triggersTraining_atActivationThreshold() {
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(100L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(100L);
when(senderModelRepository.findByPersonId(personId)).thenReturn(Optional.empty());
SenderModelService spy = spy(service);
@@ -129,7 +128,7 @@ class SenderModelServiceTest {
SenderModel existing = SenderModel.builder().personId(personId)
.correctedLinesAtTraining(100).build();
when(senderModelRepository.findByPersonId(personId)).thenReturn(Optional.of(existing));
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(149L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(149L);
SenderModelService spy = spy(service);
spy.checkAndTriggerTraining(personId);
@@ -142,7 +141,7 @@ class SenderModelServiceTest {
SenderModel existing = SenderModel.builder().personId(personId)
.correctedLinesAtTraining(100).build();
when(senderModelRepository.findByPersonId(personId)).thenReturn(Optional.of(existing));
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(150L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(150L);
SenderModelService spy = spy(service);
doReturn(false).when(spy).runOrQueueSenderTraining(personId, 150);
@@ -156,7 +155,7 @@ class SenderModelServiceTest {
@Test
void checkAndTriggerTraining_callsTrigger_whenRunNow() {
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(100L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(100L);
when(senderModelRepository.findByPersonId(personId)).thenReturn(Optional.empty());
SenderModelService spy = spy(service);
@@ -170,7 +169,7 @@ class SenderModelServiceTest {
@Test
void checkAndTriggerTraining_doesNotCallTrigger_whenQueued() {
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(100L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(100L);
when(senderModelRepository.findByPersonId(personId)).thenReturn(Optional.empty());
SenderModelService spy = spy(service);
@@ -200,7 +199,7 @@ class SenderModelServiceTest {
when(trainingRunRepository.findFirstByStatus(TrainingStatus.RUNNING)).thenReturn(
Optional.of(OcrTrainingRun.builder().id(UUID.randomUUID()).status(TrainingStatus.RUNNING)
.blockCount(5).documentCount(1).modelName("german_kurrent").build()));
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(120L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(120L);
when(trainingRunRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
boolean result = service.runOrQueueSenderTraining(personId, 120);
@@ -226,7 +225,7 @@ class SenderModelServiceTest {
// eliminating the race window between the check and a separate triggerSenderTraining call.
when(trainingRunRepository.existsByPersonIdAndStatus(personId, TrainingStatus.QUEUED)).thenReturn(false);
when(trainingRunRepository.findFirstByStatus(TrainingStatus.RUNNING)).thenReturn(Optional.empty());
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(120L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(120L);
when(trainingRunRepository.save(any())).thenAnswer(inv -> {
OcrTrainingRun r = inv.getArgument(0);
if (r.getId() == null) r.setId(UUID.randomUUID());
@@ -314,7 +313,7 @@ class SenderModelServiceTest {
@Test
void triggerManualSenderTraining_returnsRunningRun_whenIdle() {
when(personService.getById(personId)).thenReturn(Person.builder().id(personId).build());
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(0L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(0L);
when(trainingRunRepository.existsByPersonIdAndStatus(personId, TrainingStatus.QUEUED)).thenReturn(false);
when(trainingRunRepository.findFirstByStatus(TrainingStatus.RUNNING)).thenReturn(Optional.empty());
OcrTrainingRun runningRun = OcrTrainingRun.builder()
@@ -333,7 +332,7 @@ class SenderModelServiceTest {
@Test
void triggerManualSenderTraining_returnsQueuedRun_whenAnotherRunning() {
when(personService.getById(personId)).thenReturn(Person.builder().id(personId).build());
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(0L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(0L);
when(trainingRunRepository.existsByPersonIdAndStatus(personId, TrainingStatus.QUEUED)).thenReturn(false);
when(trainingRunRepository.findFirstByStatus(TrainingStatus.RUNNING)).thenReturn(
Optional.of(OcrTrainingRun.builder().id(UUID.randomUUID()).status(TrainingStatus.RUNNING)
@@ -363,7 +362,7 @@ class SenderModelServiceTest {
@Test
void triggerManualSenderTraining_throwsDomainException_whenRunRowMissingAfterCreate() {
when(personService.getById(personId)).thenReturn(Person.builder().id(personId).build());
when(blockRepository.countManualKurrentBlocksByPerson(personId)).thenReturn(0L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(personId)).thenReturn(0L);
when(trainingRunRepository.existsByPersonIdAndStatus(personId, TrainingStatus.QUEUED)).thenReturn(false);
when(trainingRunRepository.findFirstByStatus(TrainingStatus.RUNNING)).thenReturn(Optional.empty());
OcrTrainingRun runningRun = OcrTrainingRun.builder()
@@ -405,7 +404,7 @@ class SenderModelServiceTest {
.modelName("sender_" + nextPersonId).build();
when(trainingRunRepository.findFirstByStatusOrderByCreatedAtAsc(TrainingStatus.QUEUED))
.thenReturn(Optional.of(queued));
when(blockRepository.countManualKurrentBlocksByPerson(nextPersonId)).thenReturn(5L);
when(transcriptionBlockQueryService.countManualKurrentBlocksByPerson(nextPersonId)).thenReturn(5L);
SenderModelService spy = spy(service);
// Stub the recursive call to stop the chain after one promotion