From bd2be347f6f18402688fb87c204928631dc403a8 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Mon, 4 May 2026 15:37:36 +0200 Subject: [PATCH] #78: E2E-Tests auf sofortiges FAILED_FINAL fuer NO_USABLE_TEXT angepasst Nach dem #78-Fix finalisiert NO_USABLE_TEXT (Foto-PDF) sofort zu FAILED_FINAL. Die drei betroffenen E2E-Testszenarien im Bootstrap-Modul erwarteten noch das alte FAILED_RETRYABLE-Verhalten: - deterministicContentError_twoRuns_reachesFailedFinal umgeschrieben: Run 1 erwartet jetzt sofort FAILED_FINAL, Run 2 erwartet SKIPPED_FINAL_FAILURE. - skipAfterFailedFinal_thirdRun_recordsSkip umbenannt zu _secondRun_: FAILED_FINAL ist nach einem Lauf erreicht, der Skip folgt im zweiten Lauf. - mixedBatch_oneSuccess_oneContentError_batchOutcomeIsSuccess korrigiert: Run 1 erwartet FAILED_FINAL (nicht FAILED_RETRYABLE), Run 2 erwartet SKIPPED_FINAL_FAILURE (nicht einen zweiten Inhaltsfehler). Co-Authored-By: Claude Sonnet 4.6 --- .../bootstrap/e2e/BatchRunEndToEndTest.java | 81 ++++++++++--------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/e2e/BatchRunEndToEndTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/e2e/BatchRunEndToEndTest.java index a58d4c7..ebeb7b5 100644 --- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/e2e/BatchRunEndToEndTest.java +++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/e2e/BatchRunEndToEndTest.java @@ -33,8 +33,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus; * the document's master record reaches {@code SUCCESS} and the target file is on disk * after the first run. *
  • Deterministic content error: blank PDFs (no extractable text) reach - * {@code FAILED_RETRYABLE} after the first run and {@code FAILED_FINAL} after the - * second run, exercising the one-retry rule for deterministic content errors.
  • + * {@code FAILED_FINAL} immediately after the first run, because {@code NO_USABLE_TEXT} + * is not retryable — an image-only scan without OCR text will not change on retry. *
  • Transient technical error: AI stub failures produce * {@code FAILED_RETRYABLE} (transient counter incremented) without a target file.
  • *
  • Transient error exhaustion: repeated AI stub failures across @@ -124,27 +124,29 @@ class BatchRunEndToEndTest { } // ========================================================================= - // Scenario 2: Deterministic content error → FAILED_RETRYABLE → FAILED_FINAL + // Scenario 2: Deterministic content error (NO_USABLE_TEXT) → immediate FAILED_FINAL // ========================================================================= /** - * Verifies the one-retry rule for deterministic content errors: + * Verifies that a blank PDF (no extractable text) reaches {@code FAILED_FINAL} immediately + * in a single run without any retry: *
      - *
    1. Run 1: blank PDF → pre-check fails (no extractable text) → - * {@code FAILED_RETRYABLE}, content error counter = 1.
    2. - *
    3. Run 2: same outcome again → {@code FAILED_FINAL}, content error counter = 2.
    4. + *
    5. Run 1: blank PDF → pre-check fails ({@code NO_USABLE_TEXT}) → + * {@code FAILED_FINAL} immediately, content error counter = 1.
    6. + *
    7. Run 2: document is already terminal → {@code SKIPPED_FINAL_FAILURE}.
    8. *
    - * No AI call is made in either run because the content pre-check prevents it. + * No AI call is made because the content pre-check prevents it. + * An image-only scan without OCR text will not change between runs, so no retry is useful. */ @Test - void deterministicContentError_twoRuns_reachesFailedFinal(@TempDir Path tempDir) + void deterministicContentError_noUsableText_immediatelyFailedFinal(@TempDir Path tempDir) throws Exception { try (E2ETestContext ctx = E2ETestContext.initialize(tempDir)) { ctx.createBlankPdf("blank.pdf"); Path pdfPath = ctx.sourceFolder().resolve("blank.pdf"); DocumentFingerprint fp = ctx.computeFingerprint(pdfPath); - // --- Run 1 --- + // --- Run 1: NO_USABLE_TEXT → FAILED_FINAL immediately --- ctx.runBatch(); assertThat(ctx.aiStub.invocationCount()) @@ -152,26 +154,29 @@ class BatchRunEndToEndTest { .isEqualTo(0); DocumentRecord record1 = ctx.findDocumentRecord(fp).orElseThrow(); - assertThat(record1.overallStatus()).isEqualTo(ProcessingStatus.FAILED_RETRYABLE); + assertThat(record1.overallStatus()) + .as("Blank PDF must finalise immediately without retry") + .isEqualTo(ProcessingStatus.FAILED_FINAL); assertThat(record1.failureCounters().contentErrorCount()).isEqualTo(1); assertThat(record1.failureCounters().transientErrorCount()).isEqualTo(0); List attempts1 = ctx.findAttempts(fp); assertThat(attempts1).hasSize(1); - assertThat(attempts1.get(0).status()).isEqualTo(ProcessingStatus.FAILED_RETRYABLE); - assertThat(attempts1.get(0).retryable()).isTrue(); + assertThat(attempts1.get(0).status()).isEqualTo(ProcessingStatus.FAILED_FINAL); + assertThat(attempts1.get(0).retryable()).isFalse(); - // --- Run 2 --- + // --- Run 2: terminal FAILED_FINAL → skip --- ctx.runBatch(); DocumentRecord record2 = ctx.findDocumentRecord(fp).orElseThrow(); assertThat(record2.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL); - assertThat(record2.failureCounters().contentErrorCount()).isEqualTo(2); + assertThat(record2.failureCounters().contentErrorCount()) + .as("Content error counter must not change after a skip") + .isEqualTo(1); List attempts2 = ctx.findAttempts(fp); assertThat(attempts2).hasSize(2); - assertThat(attempts2.get(1).status()).isEqualTo(ProcessingStatus.FAILED_FINAL); - assertThat(attempts2.get(1).retryable()).isFalse(); + assertThat(attempts2.get(1).status()).isEqualTo(ProcessingStatus.SKIPPED_FINAL_FAILURE); // No target file should exist assertThat(ctx.listTargetFiles()).isEmpty(); @@ -277,40 +282,39 @@ class BatchRunEndToEndTest { /** * Verifies the skip-after-final-failure invariant: - * after a document reaches {@code FAILED_FINAL} (via two blank-PDF runs), a third run + * after a document reaches {@code FAILED_FINAL} (in a single blank-PDF run), a second run * records a {@code SKIPPED_FINAL_FAILURE} attempt without changing the overall status * or failure counters. */ @Test - void skipAfterFailedFinal_thirdRun_recordsSkip(@TempDir Path tempDir) throws Exception { + void skipAfterFailedFinal_secondRun_recordsSkip(@TempDir Path tempDir) throws Exception { try (E2ETestContext ctx = E2ETestContext.initialize(tempDir)) { ctx.createBlankPdf("blank.pdf"); Path pdfPath = ctx.sourceFolder().resolve("blank.pdf"); DocumentFingerprint fp = ctx.computeFingerprint(pdfPath); - // Reach FAILED_FINAL via two blank-PDF runs - ctx.runBatch(); // → FAILED_RETRYABLE + // Reach FAILED_FINAL in a single blank-PDF run (NO_USABLE_TEXT finalises immediately) ctx.runBatch(); // → FAILED_FINAL DocumentRecord finalRecord = ctx.findDocumentRecord(fp).orElseThrow(); assertThat(finalRecord.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL); int contentErrorsBefore = finalRecord.failureCounters().contentErrorCount(); - // --- Run 3: should produce skip --- + // --- Run 2: should produce skip --- ctx.runBatch(); - DocumentRecord record3 = ctx.findDocumentRecord(fp).orElseThrow(); - assertThat(record3.overallStatus()) + DocumentRecord record2 = ctx.findDocumentRecord(fp).orElseThrow(); + assertThat(record2.overallStatus()) .as("Overall status must remain FAILED_FINAL after a skip") .isEqualTo(ProcessingStatus.FAILED_FINAL); - assertThat(record3.failureCounters().contentErrorCount()) + assertThat(record2.failureCounters().contentErrorCount()) .as("Failure counters must not change after a skip") .isEqualTo(contentErrorsBefore); List attempts = ctx.findAttempts(fp); - assertThat(attempts).hasSize(3); - assertThat(attempts.get(2).status()).isEqualTo(ProcessingStatus.SKIPPED_FINAL_FAILURE); - assertThat(attempts.get(2).retryable()).isFalse(); + assertThat(attempts).hasSize(2); + assertThat(attempts.get(1).status()).isEqualTo(ProcessingStatus.SKIPPED_FINAL_FAILURE); + assertThat(attempts.get(1).retryable()).isFalse(); } } @@ -579,10 +583,11 @@ class BatchRunEndToEndTest { *
      *
    1. Run 1: a searchable PDF reaches {@code SUCCESS} within the same run * (PROPOSAL_READY → SUCCESS); a blank PDF (no extractable text) reaches - * {@code FAILED_RETRYABLE}. {@link BatchRunOutcome#SUCCESS} is returned.
    2. + * {@code FAILED_FINAL} immediately (no retry for {@code NO_USABLE_TEXT}). + * {@link BatchRunOutcome#SUCCESS} is returned. *
    3. Run 2: the searchable PDF is skipped as {@code SKIPPED_ALREADY_PROCESSED}; - * the blank PDF reaches its second content error and is finalized to - * {@code FAILED_FINAL}. {@link BatchRunOutcome#SUCCESS} is returned.
    4. + * the blank PDF is skipped as {@code SKIPPED_FINAL_FAILURE} (already terminal). + * {@link BatchRunOutcome#SUCCESS} is returned. *
    * This confirms the exit-code contract: only hard bootstrap or infrastructure * failures produce a non-zero exit code; document-level errors do not. @@ -609,7 +614,8 @@ class BatchRunEndToEndTest { .as("Searchable PDF must reach SUCCESS within the same single run") .isEqualTo(ProcessingStatus.SUCCESS); assertThat(ctx.findDocumentRecord(fpBlank).orElseThrow().overallStatus()) - .isEqualTo(ProcessingStatus.FAILED_RETRYABLE); + .as("Blank PDF (NO_USABLE_TEXT) must finalise immediately to FAILED_FINAL") + .isEqualTo(ProcessingStatus.FAILED_FINAL); assertThat(ctx.findDocumentRecord(fpBlank).orElseThrow() .failureCounters().contentErrorCount()).isEqualTo(1); @@ -622,8 +628,7 @@ class BatchRunEndToEndTest { BatchRunOutcome run2 = ctx.runBatch(); assertThat(run2) - .as("Batch must complete with SUCCESS even when a document is finalised " - + "to FAILED_FINAL") + .as("Batch must complete with SUCCESS when all documents are skipped") .isEqualTo(BatchRunOutcome.SUCCESS); DocumentRecord goodRecord = ctx.findDocumentRecord(fpGood).orElseThrow(); @@ -632,8 +637,12 @@ class BatchRunEndToEndTest { .isEqualTo(ProcessingStatus.SUCCESS); DocumentRecord blankRecord = ctx.findDocumentRecord(fpBlank).orElseThrow(); - assertThat(blankRecord.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL); - assertThat(blankRecord.failureCounters().contentErrorCount()).isEqualTo(2); + assertThat(blankRecord.overallStatus()) + .as("Terminal FAILED_FINAL must remain unchanged after skip") + .isEqualTo(ProcessingStatus.FAILED_FINAL); + assertThat(blankRecord.failureCounters().contentErrorCount()) + .as("Content error counter must not change after SKIPPED_FINAL_FAILURE") + .isEqualTo(1); // Exactly one target file from the successfully processed document List targetFiles = ctx.listTargetFiles();