diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidator.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidator.java index 1144609..6a2270c 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidator.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/config/StartConfigurationValidator.java @@ -70,6 +70,8 @@ public class StartConfigurationValidator { errors.add("- source.folder: path does not exist: " + sourceFolder); } else if (!Files.isDirectory(sourceFolder)) { errors.add("- source.folder: path is not a directory: " + sourceFolder); + } else if (!Files.isReadable(sourceFolder)) { + errors.add("- source.folder: directory is not readable: " + sourceFolder); } } diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java index 1ddbaf0..de910da 100644 --- a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunner.java @@ -6,15 +6,15 @@ import org.apache.logging.log4j.Logger; import de.gecheckt.pdf.umbenenner.adapter.inbound.cli.SchedulerBatchCommand; import de.gecheckt.pdf.umbenenner.adapter.outbound.configuration.PropertiesConfigurationPortAdapter; import de.gecheckt.pdf.umbenenner.adapter.outbound.lock.FilesystemRunLockPortAdapter; +import de.gecheckt.pdf.umbenenner.adapter.outbound.pdfextraction.PdfTextExtractionPortAdapter; +import de.gecheckt.pdf.umbenenner.adapter.outbound.sourcedocument.SourceDocumentCandidatesPortAdapter; import de.gecheckt.pdf.umbenenner.application.config.InvalidStartConfigurationException; import de.gecheckt.pdf.umbenenner.application.config.StartConfiguration; import de.gecheckt.pdf.umbenenner.application.config.StartConfigurationValidator; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome; import de.gecheckt.pdf.umbenenner.application.port.in.RunBatchProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort; -import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; -import de.gecheckt.pdf.umbenenner.application.port.out.SourceDocumentCandidatesPort; import de.gecheckt.pdf.umbenenner.application.usecase.M2BatchRunProcessingUseCase; import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext; import de.gecheckt.pdf.umbenenner.domain.model.RunId; @@ -27,12 +27,22 @@ import java.util.UUID; /** * Manual bootstrap runner that constructs the object graph and drives the startup flow. *
- * AP-003 Implementation: Creates all required components using plain Java constructor injection - * and executes the minimal no-op batch processing path. + * Responsibilities: + *
- * AP-005: CLI adapter and bootstrap wiring for M2 batch orchestration with run lock integration. - *
- * AP-006: Validates configuration before processing begins, returns exit code 1 on invalid config. + * Exit code semantics (M3): + *
* Receives the already-loaded and validated {@link StartConfiguration} and run lock port. - *
- * Note: The use case signature may accept additional ports for M3+ functionality, - * but bootstrap provides No-Op implementations for now (AP-005 scope). - * Full M3 adapter wiring will be completed in AP-007 (Bootstrap expansion). + * The factory is responsible for creating and wiring any additional outbound ports + * required by the use case (e.g., source document port, PDF extraction port). */ @FunctionalInterface public interface UseCaseFactory { @@ -93,16 +101,23 @@ public class BootstrapRunner { /** * Creates the BootstrapRunner with default factories for production use. *
- * AP-006: Uses FilesystemRunLockPortAdapter for file-based exclusive run locking. + * Wires the full M3 processing pipeline: + *
- * M2 batch execution does not scan the source folder, so this returns an empty list.
- * AP-007 will replace this with a real filesystem adapter.
- */
- private static class NoOpSourceCandidatesPort implements SourceDocumentCandidatesPort {
- @Override
- public java.util.List
- * M2 batch execution does not extract PDF content, so this port is never called.
- * AP-007 will replace this with a real PDFBox adapter.
- */
- private static class NoOpExtractionPort implements PdfTextExtractionPort {
- @Override
- public de.gecheckt.pdf.umbenenner.domain.model.PdfExtractionResult extractTextAndPageCount(
- de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate) {
- throw new UnsupportedOperationException("M2 scope: No-Op port, should not be called");
- }
- }
}
\ No newline at end of file
diff --git a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java
index a8d3afe..4c9f96a 100644
--- a/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java
+++ b/pdf-umbenenner-bootstrap/src/test/java/de/gecheckt/pdf/umbenenner/bootstrap/BootstrapRunnerTest.java
@@ -203,6 +203,30 @@ class BootstrapRunnerTest {
"Lock path passed to adapter must not be blank (default must be applied)");
}
+ /**
+ * M3 exit-code contract: a completed batch run returns exit code 0 even when individual
+ * documents ended with M3 content errors or technical document errors.
+ * Only hard infrastructure or startup failures must cause exit code 1.
+ */
+ @Test
+ void run_returnsZeroWhenBatchRunCompletesWithDocumentLevelFailures() throws Exception {
+ ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
+ // BatchRunOutcome.SUCCESS is what the M3 use case returns when the run completed,
+ // regardless of how many individual documents had content or technical errors.
+ RunBatchProcessingUseCase useCaseWithDocumentFailures = (context) -> BatchRunOutcome.SUCCESS;
+
+ BootstrapRunner runner = new BootstrapRunner(
+ () -> mockConfigPort,
+ lockFile -> new MockRunLockPort(),
+ StartConfigurationValidator::new,
+ (config, lock) -> useCaseWithDocumentFailures,
+ SchedulerBatchCommand::new
+ );
+
+ assertEquals(0, runner.run(),
+ "M3 contract: batch completion with document-level failures must return exit code 0");
+ }
+
@Test
void run_withDefaultConstructor_usesRealImplementations() {
BootstrapRunner runner = new BootstrapRunner();