M2 Vorläufige Freigabe nach Sonnet-Review

This commit is contained in:
2026-04-01 07:24:45 +02:00
parent 9d66a446b3
commit 09ad365308
5 changed files with 243 additions and 223 deletions
@@ -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() { }
}
}
}