1
0

M7 Bootstrap, Startvalidierung und Exit-Code-Verhalten finalisiert

This commit is contained in:
2026-04-08 12:37:29 +02:00
parent e91cfb9ec2
commit 8d915e7ded
3 changed files with 102 additions and 65 deletions

View File

@@ -159,13 +159,19 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
/**
* Loads candidates and processes them one by one.
* <p>
* Tracks whether any document-level persistence failures occur during processing.
* A persistence failure for a single document causes the overall batch outcome
* to be FAILURE instead of SUCCESS.
* Document-level failures — including content errors, transient technical errors,
* and individual persistence failures — do not affect the batch outcome. The batch
* completes with {@link BatchRunOutcome#SUCCESS} as long as the source folder is accessible
* and the processing loop runs to completion without a hard infrastructure error.
* Document-level persistence failures are logged by the coordinator and retried in
* subsequent runs; they must not escalate to a hard batch failure.
* <p>
* Only a hard source folder access failure ({@link SourceDocumentAccessException}) prevents
* the batch from running at all, in which case {@link BatchRunOutcome#FAILURE} is returned.
*
* @param context the current batch run context
* @return SUCCESS if all candidates were processed without persistence failures,
* FAILURE if source access fails or any document-level persistence failure occurred
* @return {@link BatchRunOutcome#SUCCESS} after all candidates have been processed,
* or {@link BatchRunOutcome#FAILURE} if the source folder is inaccessible
*/
private BatchRunOutcome processCandidates(BatchRunContext context) {
List<SourceDocumentCandidate> candidates;
@@ -177,24 +183,13 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
}
logger.info("Found {} PDF candidate(s) in source folder.", candidates.size());
// Track whether any document-level persistence failures occurred
boolean anyPersistenceFailure = false;
// Process each candidate
for (SourceDocumentCandidate candidate : candidates) {
if (!processCandidate(candidate, context)) {
anyPersistenceFailure = true;
}
processCandidate(candidate, context);
}
logger.info("Batch run completed. Processed {} candidate(s). RunId: {}",
candidates.size(), context.runId());
if (anyPersistenceFailure) {
logger.warn("Batch run completed with document-level persistence failure(s).");
return BatchRunOutcome.FAILURE;
}
return BatchRunOutcome.SUCCESS;
}

View File

@@ -450,11 +450,13 @@ class BatchRunProcessingUseCaseTest {
// -------------------------------------------------------------------------
/**
* Regression test: when a document-level persistence failure occurs,
* the batch outcome must be FAILURE, not SUCCESS.
* Document-level persistence failures must not escalate to a hard batch failure.
* The batch outcome must be SUCCESS even when a document's persistence step fails,
* because the batch loop ran to completion and the failed document will be retried
* in a subsequent run.
*/
@Test
void execute_documentPersistenceFailure_batchOutcomeIsFailure() throws Exception {
void execute_documentPersistenceFailure_batchOutcomeIsSuccess() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort();
RuntimeConfiguration config = buildConfig(tempDir);
@@ -487,16 +489,20 @@ class BatchRunProcessingUseCaseTest {
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");
assertTrue(outcome.isSuccess(),
"Document-level persistence failure must not escalate to batch FAILURE — "
+ "the batch ran to completion and the document will be retried");
assertFalse(outcome.isFailure(),
"Batch must not signal FAILURE when only a document-level persistence error occurred");
}
/**
* Regression test: mixed batch where one document succeeds and one has persistence failure.
* The batch outcome must be FAILURE due to the persistence failure.
* Mixed batch: one document completes normally, one has a persistence failure.
* The batch outcome must still be SUCCESS because both documents were processed
* by the loop. Document-level failures do not escalate to exit code 1.
*/
@Test
void execute_mixedBatch_oneCandidateSuccess_oneDocumentPersistenceFails_batchIsFailure() throws Exception {
void execute_mixedBatch_oneCandidateSuccess_oneDocumentPersistenceFails_batchIsSuccess() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort();
RuntimeConfiguration config = buildConfig(tempDir);
@@ -534,9 +540,11 @@ class BatchRunProcessingUseCaseTest {
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");
assertTrue(outcome.isSuccess(),
"Batch must be SUCCESS even when one document had a persistence failure"
+ "the batch loop ran to completion");
assertFalse(outcome.isFailure(),
"Document-level persistence failure in one candidate must not make the batch FAILURE");
}
// -------------------------------------------------------------------------
@@ -587,43 +595,6 @@ class BatchRunProcessingUseCaseTest {
"Bei Quellordner-Zugriffsfehler muss ein Fehler geloggt werden");
}
@Test
void execute_withPersistenceFailure_logsWarning() throws Exception {
// Prüft, dass nach Batch-Lauf mit Persistenzfehler eine Warnung geloggt wird
CapturingProcessingLogger capturingLogger = new CapturingProcessingLogger();
RuntimeConfiguration config = buildConfig(tempDir);
SourceDocumentCandidate candidate = makeCandidate("doc.pdf");
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(
new PdfExtractionSuccess("text", new PdfPageCount(1)));
// Coordinator der immer Persistenzfehler zurückgibt
DocumentProcessingCoordinator failingCoordinator = new DocumentProcessingCoordinator(
new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(),
new NoOpUnitOfWorkPort(), new NoOpTargetFolderPort(), new NoOpTargetFileCopyPort(),
new NoOpProcessingLogger(), 3) {
@Override
public boolean processDeferredOutcome(
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate c,
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fp,
de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext ctx,
java.time.Instant start,
java.util.function.Function<de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome> exec) {
return false;
}
};
DefaultBatchRunProcessingUseCase useCase = new DefaultBatchRunProcessingUseCase(
config, new MockRunLockPort(), candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), failingCoordinator, buildStubAiNamingService(), capturingLogger);
useCase.execute(new BatchRunContext(new RunId("persist-warn"), Instant.now()));
assertTrue(capturingLogger.warnCallCount > 0,
"Nach Batch-Lauf mit Persistenzfehler muss eine Warnung geloggt werden");
}
@Test
void execute_batchStart_logsInfo() throws Exception {
// Prüft, dass beim Batch-Start mindestens die erwarteten Info-Einträge geloggt werden.