M4 AP-008 Testabdeckung für Fingerprint, Persistenz und Skip-Logik
vervollständigen
This commit is contained in:
1
.claude/.gitignore
vendored
Normal file
1
.claude/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/settings.local.json
|
||||||
@@ -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 ':*)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.DocumentPersistenceException;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
|
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.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.DocumentTerminalSuccess;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown;
|
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown;
|
||||||
import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
|
import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
|
||||||
@@ -203,4 +204,199 @@ class SqliteDocumentRecordRepositoryAdapterTest {
|
|||||||
.isInstanceOf(DocumentPersistenceException.class)
|
.isInstanceOf(DocumentPersistenceException.class)
|
||||||
.hasMessageContaining("Expected to update 1 row but affected 0 rows");
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user