M2 Vorläufige Freigabe nach Sonnet-Review
This commit is contained in:
@@ -7,6 +7,7 @@ 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.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;
|
||||
@@ -17,6 +18,7 @@ import de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -66,10 +68,13 @@ public class BootstrapRunner {
|
||||
|
||||
/**
|
||||
* Functional interface for creating a RunBatchProcessingUseCase.
|
||||
* <p>
|
||||
* Receives the already-loaded and validated {@link StartConfiguration} so the use case
|
||||
* does not need to re-read the configuration file.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface UseCaseFactory {
|
||||
RunBatchProcessingUseCase create(ConfigurationPort configPort, RunLockPort runLockPort);
|
||||
RunBatchProcessingUseCase create(StartConfiguration config, RunLockPort runLockPort);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +94,7 @@ public class BootstrapRunner {
|
||||
this.configPortFactory = PropertiesConfigurationPortAdapter::new;
|
||||
this.runLockPortFactory = FilesystemRunLockPortAdapter::new;
|
||||
this.validatorFactory = StartConfigurationValidator::new;
|
||||
this.useCaseFactory = M2BatchRunProcessingUseCase::new;
|
||||
this.useCaseFactory = (config, lock) -> new M2BatchRunProcessingUseCase(config, lock);
|
||||
this.commandFactory = SchedulerBatchCommand::new;
|
||||
}
|
||||
|
||||
@@ -136,8 +141,13 @@ public class BootstrapRunner {
|
||||
StartConfigurationValidator validator = validatorFactory.create();
|
||||
validator.validate(config);
|
||||
|
||||
// Step 4: Create the run lock port from the validated config (AP-006)
|
||||
RunLockPort runLockPort = runLockPortFactory.create(config.runtimeLockFile());
|
||||
// Step 4: Resolve lock file path – apply default if not configured (AP-006)
|
||||
Path lockFilePath = config.runtimeLockFile();
|
||||
if (lockFilePath == null || lockFilePath.toString().isBlank()) {
|
||||
lockFilePath = Paths.get("pdf-umbenenner.lock");
|
||||
LOG.info("runtime.lock.file not configured, using default lock path: {}", lockFilePath.toAbsolutePath());
|
||||
}
|
||||
RunLockPort runLockPort = runLockPortFactory.create(lockFilePath);
|
||||
|
||||
// Step 5: Create the batch run context (M2-AP-003)
|
||||
// Generate a unique run ID and initialize the run context
|
||||
@@ -145,8 +155,9 @@ public class BootstrapRunner {
|
||||
BatchRunContext runContext = new BatchRunContext(runId, Instant.now());
|
||||
LOG.info("Batch run started. RunId: {}", runId);
|
||||
|
||||
// Step 6: Create the use case with the configuration port and run lock (application layer)
|
||||
RunBatchProcessingUseCase useCase = useCaseFactory.create(configPort, runLockPort);
|
||||
// Step 6: Create the use case with the validated config and run lock (application layer)
|
||||
// Config is passed directly; the use case does not re-read the properties file.
|
||||
RunBatchProcessingUseCase useCase = useCaseFactory.create(config, runLockPort);
|
||||
|
||||
// Step 7: Create the CLI command adapter with the use case
|
||||
SchedulerBatchCommand command = commandFactory.create(useCase);
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -23,7 +25,8 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
* Unit tests for {@link BootstrapRunner}.
|
||||
* <p>
|
||||
* Tests cover the bootstrap orchestration behavior including success path,
|
||||
* invalid configuration handling, and unexpected failure handling.
|
||||
* invalid configuration handling, unexpected failure handling, and the
|
||||
* empty-lock-file-path default (M2-F2 fix).
|
||||
*/
|
||||
class BootstrapRunnerTest {
|
||||
|
||||
@@ -32,15 +35,13 @@ class BootstrapRunnerTest {
|
||||
|
||||
@Test
|
||||
void run_returnsZeroOnSuccess() throws Exception {
|
||||
// Create a mock configuration port that returns valid config
|
||||
ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
|
||||
|
||||
// Create mock factories that return working components
|
||||
BootstrapRunner runner = new BootstrapRunner(
|
||||
() -> mockConfigPort,
|
||||
lockFile -> new MockRunLockPort(),
|
||||
StartConfigurationValidator::new,
|
||||
(port, lock) -> new MockRunBatchProcessingUseCase(true),
|
||||
(config, lock) -> new MockRunBatchProcessingUseCase(true),
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
@@ -51,10 +52,8 @@ class BootstrapRunnerTest {
|
||||
|
||||
@Test
|
||||
void run_returnsOneOnInvalidConfiguration() throws Exception {
|
||||
// Create a mock configuration port that returns valid config
|
||||
ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
|
||||
|
||||
// Create a custom validator that always throws InvalidStartConfigurationException
|
||||
StartConfigurationValidator failingValidator = new StartConfigurationValidator() {
|
||||
@Override
|
||||
public void validate(StartConfiguration config) {
|
||||
@@ -66,7 +65,7 @@ class BootstrapRunnerTest {
|
||||
() -> mockConfigPort,
|
||||
lockFile -> new MockRunLockPort(),
|
||||
() -> failingValidator,
|
||||
(port, lock) -> new MockRunBatchProcessingUseCase(true),
|
||||
(config, lock) -> new MockRunBatchProcessingUseCase(true),
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
@@ -77,7 +76,6 @@ class BootstrapRunnerTest {
|
||||
|
||||
@Test
|
||||
void run_returnsOneOnConfigurationLoadingFailure() {
|
||||
// Create a mock configuration port that throws IllegalStateException
|
||||
ConfigurationPort failingConfigPort = () -> {
|
||||
throw new IllegalStateException("Simulated configuration loading failure");
|
||||
};
|
||||
@@ -86,7 +84,7 @@ class BootstrapRunnerTest {
|
||||
() -> failingConfigPort,
|
||||
lockFile -> new MockRunLockPort(),
|
||||
StartConfigurationValidator::new,
|
||||
(port, lock) -> new MockRunBatchProcessingUseCase(true),
|
||||
(config, lock) -> new MockRunBatchProcessingUseCase(true),
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
@@ -97,7 +95,6 @@ class BootstrapRunnerTest {
|
||||
|
||||
@Test
|
||||
void run_returnsOneOnUnexpectedException() {
|
||||
// Create a mock configuration port that throws a generic exception
|
||||
ConfigurationPort throwingConfigPort = () -> {
|
||||
throw new RuntimeException("Simulated unexpected failure");
|
||||
};
|
||||
@@ -106,7 +103,7 @@ class BootstrapRunnerTest {
|
||||
() -> throwingConfigPort,
|
||||
lockFile -> new MockRunLockPort(),
|
||||
StartConfigurationValidator::new,
|
||||
(port, lock) -> new MockRunBatchProcessingUseCase(true),
|
||||
(config, lock) -> new MockRunBatchProcessingUseCase(true),
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
@@ -117,17 +114,14 @@ class BootstrapRunnerTest {
|
||||
|
||||
@Test
|
||||
void run_returnsOneWhenBatchFails() throws Exception {
|
||||
// Create a mock configuration port that returns valid config
|
||||
ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
|
||||
|
||||
// Create a use case that returns failure outcome
|
||||
RunBatchProcessingUseCase failingUseCase = (context) -> BatchRunOutcome.FAILURE;
|
||||
|
||||
BootstrapRunner runner = new BootstrapRunner(
|
||||
() -> mockConfigPort,
|
||||
lockFile -> new MockRunLockPort(),
|
||||
StartConfigurationValidator::new,
|
||||
(port, lock) -> failingUseCase,
|
||||
(config, lock) -> failingUseCase,
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
@@ -139,16 +133,14 @@ class BootstrapRunnerTest {
|
||||
@Test
|
||||
void run_returnsOneWhenLockUnavailable() throws Exception {
|
||||
// AP-007: controlled early termination because another instance is already running
|
||||
// maps to exit code 1 (start protection counts as a hard startup failure)
|
||||
ConfigurationPort mockConfigPort = new MockConfigurationPort(tempDir, true);
|
||||
|
||||
RunBatchProcessingUseCase lockUnavailableUseCase = (context) -> BatchRunOutcome.LOCK_UNAVAILABLE;
|
||||
|
||||
BootstrapRunner runner = new BootstrapRunner(
|
||||
() -> mockConfigPort,
|
||||
lockFile -> new MockRunLockPort(),
|
||||
StartConfigurationValidator::new,
|
||||
(port, lock) -> lockUnavailableUseCase,
|
||||
(config, lock) -> lockUnavailableUseCase,
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
@@ -157,18 +149,70 @@ class BootstrapRunnerTest {
|
||||
assertEquals(1, exitCode, "Lock unavailable (another instance running) should return exit code 1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test for M2-F2: when runtime.lock.file is absent (empty path),
|
||||
* BootstrapRunner must apply a default lock path instead of passing Paths.get("")
|
||||
* to the lock adapter (which would always see the current directory as "existing"
|
||||
* and permanently block lock acquisition).
|
||||
*/
|
||||
@Test
|
||||
void run_usesDefaultLockPathWhenRuntimeLockFileNotConfigured() throws Exception {
|
||||
// Build a config that has an empty runtimeLockFile (simulates unconfigured property)
|
||||
Path sourceDir = Files.createDirectories(tempDir.resolve("source"));
|
||||
Path targetDir = Files.createDirectories(tempDir.resolve("target"));
|
||||
Path dbFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
|
||||
StartConfiguration configWithEmptyLock = new StartConfiguration(
|
||||
sourceDir,
|
||||
targetDir,
|
||||
dbFile,
|
||||
URI.create("https://api.example.com"),
|
||||
"gpt-4",
|
||||
30,
|
||||
3,
|
||||
100,
|
||||
50000,
|
||||
promptFile,
|
||||
Paths.get(""), // empty – simulates unconfigured runtime.lock.file
|
||||
tempDir.resolve("logs"),
|
||||
"INFO",
|
||||
"test-key"
|
||||
);
|
||||
|
||||
AtomicReference<Path> capturedLockPath = new AtomicReference<>();
|
||||
|
||||
// ConfigurationPortFactory returns a ConfigurationPort; ConfigurationPort returns StartConfiguration
|
||||
ConfigurationPort configPort = () -> configWithEmptyLock;
|
||||
BootstrapRunner runner = new BootstrapRunner(
|
||||
() -> configPort,
|
||||
lockFile -> {
|
||||
capturedLockPath.set(lockFile);
|
||||
return new MockRunLockPort();
|
||||
},
|
||||
StartConfigurationValidator::new,
|
||||
(config, lock) -> new MockRunBatchProcessingUseCase(true),
|
||||
useCase -> new SchedulerBatchCommand(useCase)
|
||||
);
|
||||
|
||||
int exitCode = runner.run();
|
||||
|
||||
assertEquals(0, exitCode, "Should succeed even when runtime.lock.file is not configured");
|
||||
assertNotNull(capturedLockPath.get(), "Lock path should have been set");
|
||||
assertFalse(capturedLockPath.get().toString().isBlank(),
|
||||
"Lock path passed to adapter must not be blank (default must be applied)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_withDefaultConstructor_usesRealImplementations() {
|
||||
// This test verifies that the default constructor creates a functional runner
|
||||
// We can't fully test it without actual config files, but we can verify instantiation
|
||||
BootstrapRunner runner = new BootstrapRunner();
|
||||
|
||||
assertNotNull(runner, "Default constructor should create a valid BootstrapRunner");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock ConfigurationPort that returns a valid StartConfiguration.
|
||||
*/
|
||||
// -------------------------------------------------------------------------
|
||||
// Mocks
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static class MockConfigurationPort implements ConfigurationPort {
|
||||
private final Path tempDir;
|
||||
private final boolean shouldSucceed;
|
||||
@@ -185,10 +229,16 @@ class BootstrapRunnerTest {
|
||||
}
|
||||
|
||||
try {
|
||||
Path sourceFolder = Files.createDirectory(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectory(tempDir.resolve("target"));
|
||||
Path sqliteFile = Files.createFile(tempDir.resolve("db.sqlite"));
|
||||
Path promptTemplateFile = Files.createFile(tempDir.resolve("prompt.txt"));
|
||||
Path sourceFolder = Files.createDirectories(tempDir.resolve("source"));
|
||||
Path targetFolder = Files.createDirectories(tempDir.resolve("target"));
|
||||
Path sqliteFile = tempDir.resolve("db.sqlite");
|
||||
if (!Files.exists(sqliteFile)) {
|
||||
Files.createFile(sqliteFile);
|
||||
}
|
||||
Path promptTemplateFile = tempDir.resolve("prompt.txt");
|
||||
if (!Files.exists(promptTemplateFile)) {
|
||||
Files.createFile(promptTemplateFile);
|
||||
}
|
||||
|
||||
return new StartConfiguration(
|
||||
sourceFolder,
|
||||
@@ -212,9 +262,6 @@ class BootstrapRunnerTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock RunBatchProcessingUseCase that returns a configurable result.
|
||||
*/
|
||||
private static class MockRunBatchProcessingUseCase implements RunBatchProcessingUseCase {
|
||||
private final boolean shouldSucceed;
|
||||
|
||||
@@ -228,18 +275,11 @@ class BootstrapRunnerTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock RunLockPort for testing.
|
||||
*/
|
||||
private static class MockRunLockPort implements RunLockPort {
|
||||
@Override
|
||||
public void acquire() {
|
||||
// Mock: no-op
|
||||
}
|
||||
public void acquire() { }
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// Mock: no-op
|
||||
}
|
||||
public void release() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user