M10 bis AP-003

This commit is contained in:
2026-04-20 13:07:19 +02:00
parent 20b847d821
commit 01414fc732
16 changed files with 1139 additions and 184 deletions
@@ -25,6 +25,10 @@ import org.junit.jupiter.api.io.TempDir;
import de.gecheckt.pdf.umbenenner.adapter.in.cli.SchedulerBatchCommand;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiAdapter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.out.configuration.LegacyConfigurationMigrator;
import de.gecheckt.pdf.umbenenner.adapter.out.configuration.PropertiesConfigurationPortAdapter;
import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.StartConfigurationValidator;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import de.gecheckt.pdf.umbenenner.application.config.provider.MultiProviderConfiguration;
@@ -184,11 +188,13 @@ class BootstrapRunnerConfigPathSemanticsTest {
void runGui_withNonExistentConfigPath_startsGuiAndReturnsZeroOnNormalShutdown() {
String nonExistentPath = tempDir.resolve("missing.properties").toString();
AtomicReference<Optional<String>> receivedNotice = new AtomicReference<>();
AtomicReference<GuiConfigurationEditorState> receivedState = new AtomicReference<>();
BootstrapRunner runner = runnerWithGuiFactory(() -> new GuiAdapter() {
@Override
public void start(Optional<String> startupNotice) {
receivedNotice.set(startupNotice);
public void start(GuiStartupContext startupContext) {
receivedNotice.set(startupContext.startupNotice());
receivedState.set(startupContext.initialState());
// normal shutdown: returns without throwing
}
});
@@ -199,6 +205,8 @@ class BootstrapRunnerConfigPathSemanticsTest {
"GUI with non-existent --config path must still return exit code 0 on normal shutdown");
assertTrue(receivedNotice.get().isPresent(),
"A startup notice must be forwarded to the GUI adapter when the config file is missing");
assertFalse(receivedState.get().hasLoadedFileSnapshot(),
"GUI must start without a loaded configuration when the supplied file is missing");
}
@Test
@@ -223,7 +231,7 @@ class BootstrapRunnerConfigPathSemanticsTest {
BootstrapRunner runner = runnerWithGuiFactory(() -> new GuiAdapter() {
@Override
public void start(Optional<String> startupNotice) {
public void start(GuiStartupContext startupContext) {
// normal termination
}
});
@@ -250,8 +258,8 @@ class BootstrapRunnerConfigPathSemanticsTest {
BootstrapRunner runner = runnerWithGuiFactory(() -> new GuiAdapter() {
@Override
public void start(Optional<String> startupNotice) {
receivedNotice.set(startupNotice);
public void start(GuiStartupContext startupContext) {
receivedNotice.set(startupContext.startupNotice());
}
});
@@ -266,11 +274,13 @@ class BootstrapRunnerConfigPathSemanticsTest {
void runGui_withExistingConfigPath_startsGuiWithEmptyNotice(@TempDir Path workDir) throws Exception {
Path existingConfigFile = Files.createFile(workDir.resolve("real.properties"));
AtomicReference<Optional<String>> receivedNotice = new AtomicReference<>();
AtomicReference<GuiConfigurationEditorState> receivedState = new AtomicReference<>();
BootstrapRunner runner = runnerWithGuiFactory(() -> new GuiAdapter() {
@Override
public void start(Optional<String> startupNotice) {
receivedNotice.set(startupNotice);
public void start(GuiStartupContext startupContext) {
receivedNotice.set(startupContext.startupNotice());
receivedState.set(startupContext.initialState());
}
});
@@ -281,6 +291,76 @@ class BootstrapRunnerConfigPathSemanticsTest {
"GUI with existing --config path must return exit code 0 on normal shutdown");
assertFalse(receivedNotice.get().isPresent(),
"No startup notice must be forwarded when the config file exists");
assertTrue(receivedState.get().hasLoadedFileSnapshot(),
"GUI must receive a loaded editor state when the supplied file exists");
assertEquals(existingConfigFile.toString(), receivedState.get().configurationPathText(),
"The loaded editor state must point to the supplied configuration file");
}
@Test
void runGui_withLegacyConfigPath_loadsMigratedStateAndKeepsMigrationNote(@TempDir Path workDir) throws Exception {
Path legacyConfigFile = workDir.resolve("legacy.properties");
Files.createDirectories(workDir.resolve("source"));
Files.createDirectories(workDir.resolve("target"));
Files.createDirectories(workDir.resolve("logs"));
Files.createFile(workDir.resolve("db.sqlite"));
Files.createFile(workDir.resolve("prompt.txt"));
Files.createFile(workDir.resolve("lock.pid"));
Files.writeString(legacyConfigFile, """
source.folder=%s
target.folder=%s
sqlite.file=%s
api.baseUrl=https://api.openai.com/v1
api.model=gpt-4o-mini
api.timeoutSeconds=30
api.key=test-api-key
max.retries.transient=3
max.pages=10
max.text.characters=5000
prompt.template.file=%s
runtime.lock.file=%s
log.directory=%s
log.level=INFO
log.ai.sensitive=false
""".formatted(
workDir.resolve("source"),
workDir.resolve("target"),
workDir.resolve("db.sqlite"),
workDir.resolve("prompt.txt"),
workDir.resolve("lock.pid"),
workDir.resolve("logs")));
AtomicReference<Optional<String>> receivedNotice = new AtomicReference<>();
AtomicReference<GuiConfigurationEditorState> receivedState = new AtomicReference<>();
BootstrapRunner runner = new BootstrapRunner(
path -> new LegacyConfigurationMigrator().migrateIfLegacy(path),
PropertiesConfigurationPortAdapter::new,
lockFile -> new MockRunLockPort(),
StartConfigurationValidator::new,
jdbcUrl -> new MockSchemaInitializationPort(),
(config, lock) -> new MockRunBatchProcessingUseCase(true),
SchedulerBatchCommand::new,
() -> new GuiAdapter() {
@Override
public void start(GuiStartupContext startupContext) {
receivedNotice.set(startupContext.startupNotice());
receivedState.set(startupContext.initialState());
}
}
);
int exitCode = runner.run(new StartupArguments(StartupMode.GUI, Optional.of(legacyConfigFile.toString())));
assertEquals(0, exitCode, "GUI with legacy config path must still return exit code 0 on normal shutdown");
assertFalse(receivedNotice.get().isPresent(),
"No startup notice is needed when a legacy config can be migrated and loaded");
assertTrue(receivedState.get().hasLoadedFileSnapshot(),
"The migrated editor state must carry the loaded file snapshot");
assertTrue(receivedState.get().hasPendingMigrationMessage(),
"The migrated editor state must retain the pending migration message");
assertEquals(legacyConfigFile.toString(), receivedState.get().configurationPathText(),
"The migrated editor state must report the full loaded path");
}
@Test
@@ -289,8 +369,8 @@ class BootstrapRunnerConfigPathSemanticsTest {
BootstrapRunner runner = runnerWithGuiFactory(() -> new GuiAdapter() {
@Override
public void start(Optional<String> startupNotice) {
receivedNotice.set(startupNotice);
public void start(GuiStartupContext startupContext) {
receivedNotice.set(startupContext.startupNotice());
}
});
@@ -14,6 +14,8 @@ import org.junit.jupiter.api.io.TempDir;
import de.gecheckt.pdf.umbenenner.adapter.in.cli.SchedulerBatchCommand;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiAdapter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.StartConfigurationValidator;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import de.gecheckt.pdf.umbenenner.application.config.provider.MultiProviderConfiguration;
@@ -41,7 +43,7 @@ class BootstrapRunnerStartupDispatchTest {
void run_withGuiMode_returnsZeroWhenGuiStartSucceeds() {
BootstrapRunner runner = runnerWithGuiFactory(() -> new GuiAdapter() {
@Override
public void start(Optional<String> startupNotice) {
public void start(GuiStartupContext startupContext) {
// normal termination: returns without throwing
}
});
@@ -55,7 +57,7 @@ class BootstrapRunnerStartupDispatchTest {
void run_withGuiMode_returnsOneWhenGuiStartThrowsException() {
BootstrapRunner runner = runnerWithGuiFactory(() -> new GuiAdapter() {
@Override
public void start(Optional<String> startupNotice) {
public void start(GuiStartupContext startupContext) {
throw new RuntimeException("simulated GUI startup failure");
}
});