From 326e739e455a2a99ef7791bf9efffb0457bc7804 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Fri, 3 Apr 2026 14:18:31 +0200 Subject: [PATCH] =?UTF-8?q?M4=20AP-008=20Testabdeckung=20f=C3=BCr=20Finger?= =?UTF-8?q?print,=20Persistenz=20und=20Skip-Logik=20vervollst=C3=A4ndigen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/.gitignore | 1 + .claude/settings.local.json | 25 --- ...teDocumentRecordRepositoryAdapterTest.java | 196 ++++++++++++++++++ 3 files changed, 197 insertions(+), 25 deletions(-) create mode 100644 .claude/.gitignore delete mode 100644 .claude/settings.local.json diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 0000000..53ced0f --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1 @@ +/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 0b47bfb..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(xargs grep:*)", - "Bash(xargs wc:*)", - "Bash(mvn clean:*)", - "Bash(mvn verify:*)", - "Bash(mvn test:*)", - "Bash(find D:/Dev/Projects/pdf-umbenenner-parent -not -path */target/* -type d)", - "Bash(mvn -pl pdf-umbenenner-adapter-out clean compile)", - "Bash(mvn dependency:tree -pl pdf-umbenenner-adapter-out)", - "Bash(mvn -pl pdf-umbenenner-domain clean compile)", - "Bash(mvn help:describe -Dplugin=org.apache.pdfbox:pdfbox -Ddetail=false)", - "Bash(cd /d D:/Dev/Projects/pdf-umbenenner-parent)", - "Bash(mvn -v)", - "Bash(grep -E \"\\\\.java$\")", - "Bash(grep \"\\\\.java$\")", - "Bash(mvn -q clean compile -DskipTests)", - "Bash(mvn -q test)", - "Bash(mvn -q clean test)", - "Bash(git add:*)", - "Bash(git commit -m ':*)" - ] - } -} diff --git a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteDocumentRecordRepositoryAdapterTest.java b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteDocumentRecordRepositoryAdapterTest.java index 582bad0..be265a8 100644 --- a/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteDocumentRecordRepositoryAdapterTest.java +++ b/pdf-umbenenner-adapter-out/src/test/java/de/gecheckt/pdf/umbenenner/adapter/out/sqlite/SqliteDocumentRecordRepositoryAdapterTest.java @@ -15,6 +15,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.DocumentKnownProcessable; import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException; import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord; import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult; +import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure; import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess; import de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown; import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters; @@ -203,4 +204,199 @@ class SqliteDocumentRecordRepositoryAdapterTest { .isInstanceOf(DocumentPersistenceException.class) .hasMessageContaining("Expected to update 1 row but affected 0 rows"); } + + @Test + void findByFingerprint_shouldReturnDocumentTerminalFinalFailure_whenStatusIsFailedFinal() { + // Given + DocumentFingerprint fingerprint = new DocumentFingerprint( + "5555555555555555555555555555555555555555555555555555555555555555"); + Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS); + // Create initially as PROCESSING + DocumentRecord initialRecord = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/path/to/document.pdf"), + "document.pdf", + ProcessingStatus.PROCESSING, + FailureCounters.zero(), + null, + null, + now.minusSeconds(120), + now.minusSeconds(120) + ); + repository.create(initialRecord); + + // Update to FAILED_FINAL (second content error: count=2) + Instant failureInstant = now.minusSeconds(60); + DocumentRecord failedFinalRecord = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/path/to/document.pdf"), + "document.pdf", + ProcessingStatus.FAILED_FINAL, + new FailureCounters(2, 0), + failureInstant, + null, + now.minusSeconds(120), + failureInstant + ); + repository.update(failedFinalRecord); + + // When + DocumentRecordLookupResult result = repository.findByFingerprint(fingerprint); + + // Then + assertThat(result).isInstanceOf(DocumentTerminalFinalFailure.class); + DocumentTerminalFinalFailure terminalFailure = (DocumentTerminalFinalFailure) result; + DocumentRecord foundRecord = terminalFailure.record(); + assertThat(foundRecord.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL); + assertThat(foundRecord.failureCounters().contentErrorCount()).isEqualTo(2); + assertThat(foundRecord.failureCounters().transientErrorCount()).isEqualTo(0); + assertThat(foundRecord.lastFailureInstant()).isEqualTo(failureInstant); + assertThat(foundRecord.lastSuccessInstant()).isNull(); + } + + @Test + void update_shouldPersistNonZeroFailureCountersAndFailureInstant() { + // Given: create a new document as PROCESSING + DocumentFingerprint fingerprint = new DocumentFingerprint( + "6666666666666666666666666666666666666666666666666666666666666666"); + Instant createdAt = Instant.now().minusSeconds(120).truncatedTo(ChronoUnit.MICROS); + DocumentRecord initialRecord = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/invoice.pdf"), + "invoice.pdf", + ProcessingStatus.PROCESSING, + FailureCounters.zero(), + null, + null, + createdAt, + createdAt + ); + repository.create(initialRecord); + + // Update to FAILED_RETRYABLE with content_error_count=1, transient_error_count=0 + Instant failureInstant = Instant.now().truncatedTo(ChronoUnit.MICROS); + DocumentRecord failedRetryableRecord = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/invoice.pdf"), + "invoice.pdf", + ProcessingStatus.FAILED_RETRYABLE, + new FailureCounters(1, 0), + failureInstant, + null, + createdAt, + failureInstant + ); + + // When + repository.update(failedRetryableRecord); + DocumentRecordLookupResult result = repository.findByFingerprint(fingerprint); + + // Then: lookup returns DocumentKnownProcessable (FAILED_RETRYABLE is not terminal) + assertThat(result).isInstanceOf(DocumentKnownProcessable.class); + DocumentKnownProcessable known = (DocumentKnownProcessable) result; + DocumentRecord foundRecord = known.record(); + assertThat(foundRecord.overallStatus()).isEqualTo(ProcessingStatus.FAILED_RETRYABLE); + assertThat(foundRecord.failureCounters().contentErrorCount()).isEqualTo(1); + assertThat(foundRecord.failureCounters().transientErrorCount()).isEqualTo(0); + assertThat(foundRecord.lastFailureInstant()).isEqualTo(failureInstant); + assertThat(foundRecord.lastSuccessInstant()).isNull(); + assertThat(foundRecord.createdAt()).isEqualTo(createdAt); + assertThat(foundRecord.updatedAt()).isEqualTo(failureInstant); + } + + @Test + void update_shouldTransitionFromFailedRetryableToFailedFinalWithIncrementedCounter() { + // Given: create as FAILED_RETRYABLE with content_error_count=1 (first content error already recorded) + DocumentFingerprint fingerprint = new DocumentFingerprint( + "7777777777777777777777777777777777777777777777777777777777777777"); + Instant createdAt = Instant.now().minusSeconds(300).truncatedTo(ChronoUnit.MICROS); + Instant firstFailureAt = Instant.now().minusSeconds(120).truncatedTo(ChronoUnit.MICROS); + + DocumentRecord initialRecord = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/report.pdf"), + "report.pdf", + ProcessingStatus.FAILED_RETRYABLE, + new FailureCounters(1, 0), + firstFailureAt, + null, + createdAt, + firstFailureAt + ); + repository.create(initialRecord); + + // Second content error: update to FAILED_FINAL with content_error_count=2 + Instant secondFailureAt = Instant.now().truncatedTo(ChronoUnit.MICROS); + DocumentRecord failedFinalRecord = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/report.pdf"), + "report.pdf", + ProcessingStatus.FAILED_FINAL, + new FailureCounters(2, 0), + secondFailureAt, + null, + createdAt, + secondFailureAt + ); + + // When + repository.update(failedFinalRecord); + DocumentRecordLookupResult result = repository.findByFingerprint(fingerprint); + + // Then: terminal final failure + assertThat(result).isInstanceOf(DocumentTerminalFinalFailure.class); + DocumentTerminalFinalFailure terminalFailure = (DocumentTerminalFinalFailure) result; + DocumentRecord foundRecord = terminalFailure.record(); + assertThat(foundRecord.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL); + assertThat(foundRecord.failureCounters().contentErrorCount()).isEqualTo(2); + assertThat(foundRecord.failureCounters().transientErrorCount()).isEqualTo(0); + assertThat(foundRecord.lastFailureInstant()).isEqualTo(secondFailureAt); + assertThat(foundRecord.lastSuccessInstant()).isNull(); + } + + @Test + void update_shouldPersistTransientErrorCounter() { + // Given: create as PROCESSING + DocumentFingerprint fingerprint = new DocumentFingerprint( + "8888888888888888888888888888888888888888888888888888888888888888"); + Instant createdAt = Instant.now().minusSeconds(60).truncatedTo(ChronoUnit.MICROS); + DocumentRecord initialRecord = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/scan.pdf"), + "scan.pdf", + ProcessingStatus.PROCESSING, + FailureCounters.zero(), + null, + null, + createdAt, + createdAt + ); + repository.create(initialRecord); + + // Update to FAILED_RETRYABLE with transient_error_count=3 (technical errors) + Instant failureInstant = Instant.now().truncatedTo(ChronoUnit.MICROS); + DocumentRecord transientFailureRecord = new DocumentRecord( + fingerprint, + new SourceDocumentLocator("/source/scan.pdf"), + "scan.pdf", + ProcessingStatus.FAILED_RETRYABLE, + new FailureCounters(0, 3), + failureInstant, + null, + createdAt, + failureInstant + ); + + // When + repository.update(transientFailureRecord); + DocumentRecordLookupResult result = repository.findByFingerprint(fingerprint); + + // Then + assertThat(result).isInstanceOf(DocumentKnownProcessable.class); + DocumentKnownProcessable known = (DocumentKnownProcessable) result; + DocumentRecord foundRecord = known.record(); + assertThat(foundRecord.failureCounters().contentErrorCount()).isEqualTo(0); + assertThat(foundRecord.failureCounters().transientErrorCount()).isEqualTo(3); + assertThat(foundRecord.lastFailureInstant()).isEqualTo(failureInstant); + } } \ No newline at end of file