1
0

Nachbearbeitung: Dokumentbezogene Persistenzfehler korrekt im

Batch-Ergebnis berücksichtigt
This commit is contained in:
2026-04-05 21:45:49 +02:00
parent 8f1e41c1a6
commit 9fd6bc469d
3 changed files with 155 additions and 31 deletions

View File

@@ -424,6 +424,98 @@ class BatchRunProcessingUseCaseTest {
assertEquals(3, processor.processCallCount(), "processor should be called once per candidate");
}
// -------------------------------------------------------------------------
// Document-level persistence failure handling
// -------------------------------------------------------------------------
/**
* Regression test: when a document-level persistence failure occurs,
* the batch outcome must be FAILURE, not SUCCESS.
*/
@Test
void execute_documentPersistenceFailure_batchOutcomeIsFailure() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort();
RuntimeConfiguration config = buildConfig(tempDir);
SourceDocumentCandidate candidate = makeCandidate("document.pdf");
PdfExtractionSuccess success = new PdfExtractionSuccess("Invoice text", new PdfPageCount(1));
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(success);
// Use a coordinator that always fails persistence
DocumentProcessingCoordinator failingProcessor = new DocumentProcessingCoordinator(
new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(),
new NoOpUnitOfWorkPort(), new NoOpProcessingLogger()) {
@Override
public boolean processDeferredOutcome(
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate,
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint,
de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext context,
java.time.Instant attemptStart,
java.util.function.Function<de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome> pipelineExecutor) {
// Always report persistence failure
return false;
}
};
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), failingProcessor);
BatchRunContext context = new BatchRunContext(new RunId("persist-fail"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isFailure(), "Document persistence failure should yield FAILURE outcome");
assertFalse(outcome.isSuccess(), "Batch must not succeed when document persistence failed");
}
/**
* Regression test: mixed batch where one document succeeds and one has persistence failure.
* The batch outcome must be FAILURE due to the persistence failure.
*/
@Test
void execute_mixedBatch_oneCandidateSuccess_oneDocumentPersistenceFails_batchIsFailure() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort();
RuntimeConfiguration config = buildConfig(tempDir);
SourceDocumentCandidate goodCandidate = makeCandidate("good.pdf");
SourceDocumentCandidate failCandidate = makeCandidate("fails.pdf");
PdfExtractionSuccess success = new PdfExtractionSuccess("Invoice text", new PdfPageCount(1));
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(goodCandidate, failCandidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(success);
// Coordinator that succeeds for first document, fails persistence for second
DocumentProcessingCoordinator selectiveFailingProcessor = new DocumentProcessingCoordinator(
new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(),
new NoOpUnitOfWorkPort(), new NoOpProcessingLogger()) {
private int callCount = 0;
@Override
public boolean processDeferredOutcome(
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate,
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint,
de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext context,
java.time.Instant attemptStart,
java.util.function.Function<de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome> pipelineExecutor) {
callCount++;
// First document succeeds, second fails persistence
return callCount == 1;
}
};
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), selectiveFailingProcessor);
BatchRunContext context = new BatchRunContext(new RunId("mixed-persist-fail"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isFailure(),
"Batch must fail when any document has a persistence failure, even if others succeeded");
assertFalse(outcome.isSuccess(), "Cannot be SUCCESS when persistence failed for any document");
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
@@ -615,7 +707,7 @@ class BatchRunProcessingUseCaseTest {
}
@Override
public void process(
public boolean process(
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate,
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint,
de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome outcome,
@@ -623,11 +715,11 @@ class BatchRunProcessingUseCaseTest {
java.time.Instant attemptStart) {
processCallCount++;
// Delegate to super so the real logic runs (with no-op repos)
super.process(candidate, fingerprint, outcome, context, attemptStart);
return super.process(candidate, fingerprint, outcome, context, attemptStart);
}
@Override
public void processDeferredOutcome(
public boolean processDeferredOutcome(
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate,
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint,
de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext context,
@@ -635,7 +727,7 @@ class BatchRunProcessingUseCaseTest {
java.util.function.Function<de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome> pipelineExecutor) {
processCallCount++;
// Delegate to super so the real logic runs (with no-op repos)
super.processDeferredOutcome(candidate, fingerprint, context, attemptStart, pipelineExecutor);
return super.processDeferredOutcome(candidate, fingerprint, context, attemptStart, pipelineExecutor);
}
int processCallCount() { return processCallCount; }