From 479d1765360299facca2bbf4cd978cc076784536 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Mon, 4 May 2026 17:02:51 +0200 Subject: [PATCH] =?UTF-8?q?#89=20#90:=20Log-Verzeichnis-Pr=C3=BCfpunkt=20+?= =?UTF-8?q?=20betrieb.md=20MSI-Pfadwarnungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #90: Neuer technischer Prüfpunkt LOG_DIRECTORY_USABLE (12. Checkpoint): - Zeigt konfigurierten log.directory-Wert und aufgelösten absoluten Pfad - Prüft ob Verzeichnis beschreibbar/anlegbar ist (WARNING, kein ERROR) - Liest tatsächlichen Log-Datei-Pfad via Log4j2 LoggerContext → RollingFileAppender - LogDiagnosticsPort als neuer Outbound-Port (application-Modul) - Log4jLogDiagnosticsAdapter als Implementierung im bootstrap-Modul - TechnicalTestRequest erhält logDirectory-Feld - GuiTechnicalTestCoordinator erhält logDirectoryProvider-Supplier #89: docs/betrieb.md – MSI-Betrieb um Pfadwarnungen erweitert: - Warnung: relative Pfade lösen sich in schreibgeschütztes C:\Program Files\ auf - Warnung: Backslashes in .properties werden als Java-Escape-Sequenzen interpretiert - Betroffene Parameter mit Empfehlung zu absoluten Forward-Slash-Pfaden - Beschreibung des neuen Log-Verzeichnis-Prüfpunkts Co-Authored-By: Claude Sonnet 4.6 --- docs/betrieb.md | 50 +++++++- .../gui/GuiConfigurationEditorWorkspace.java | 1 + .../adapter/in/gui/GuiStartupContext.java | 3 +- .../in/gui/GuiTechnicalTestCoordinator.java | 10 +- .../adapter/in/gui/GuiAdapterSmokeTest.java | 3 +- .../in/gui/GuiEditorFieldBindingTest.java | 3 +- .../in/gui/GuiEditorIntegrationTest.java | 9 +- .../in/gui/GuiEditorRegressionSmokeTest.java | 15 ++- .../in/gui/GuiEditorValidationSmokeTest.java | 6 +- .../in/gui/GuiMessageAreaSmokeTest.java | 12 +- .../in/gui/GuiModelCatalogSmokeTest.java | 3 +- .../GuiTechnicalTestCoordinatorSmokeTest.java | 20 ++-- .../gui/GuiUnsavedChangesGuardSmokeTest.java | 6 +- .../in/gui/GuiValidateActionSmokeTest.java | 6 +- .../technicaltest/CheckpointId.java | 10 +- .../technicaltest/LogDiagnosticsPort.java | 28 +++++ .../TechnicalTestOrchestrator.java | 109 +++++++++++++++--- .../technicaltest/TechnicalTestRequest.java | 14 ++- .../technicaltest/CheckpointIdTest.java | 7 +- .../TechnicalTestOrchestratorTest.java | 61 ++++++---- .../TechnicalTestRequestTest.java | 16 ++- .../umbenenner/bootstrap/BootstrapRunner.java | 4 +- .../adapter/Log4jLogDiagnosticsAdapter.java | 68 +++++++++++ 23 files changed, 376 insertions(+), 88 deletions(-) create mode 100644 pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/LogDiagnosticsPort.java create mode 100644 pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jLogDiagnosticsAdapter.java diff --git a/docs/betrieb.md b/docs/betrieb.md index 1f5f375..9e6a718 100644 --- a/docs/betrieb.md +++ b/docs/betrieb.md @@ -550,13 +550,51 @@ Installationsverzeichnis ab. **Der Betreiber muss diese Beispieldatei manuell na Windows-SmartScreen-Warnung, die durch „Weitere Informationen → Trotzdem ausführen" bestätigt werden muss. Code-Signing ist für spätere Ausbaustufen vorgesehen. -**Empfehlung für Prompt- und Konfigurationspfade im MSI-Betrieb:** +**Empfehlung für Pfade im MSI-Betrieb:** -Für den MSI-Betrieb (Startmenü, Task Scheduler) wird **empfohlen**, -`prompt.template.file` und `sqlite.file` als **absolute Pfade** zu konfigurieren -oder auf `%APPDATA%`- bzw. `%ProgramData%`-Verzeichnisse zu zeigen. -Relative Pfade beziehen sich auf das Arbeitsverzeichnis, das je nach Startart variiert -(siehe Abschnitt „Prompt-Konfiguration"). +Für den MSI-Betrieb (Startmenü, Task Scheduler) müssen alle Dateipfade als **absolute Pfade** +konfiguriert werden. Relative Pfade werden relativ zum Installationsverzeichnis +`C:\Program Files\PDF KI Renamer\` aufgelöst, das **schreibgeschützt** ist. Dadurch +schlagen Schreibversuche (Logs, SQLite-Datenbank, Lock-Datei) ohne Fehlermeldung fehl. + +> **Warnung – Relative Pfade im MSI-Betrieb nicht verwenden:** +> Pfade wie `./logs`, `./work/local/logs` oder `logs/` werden im MSI-Betrieb relativ +> zum Installationsverzeichnis aufgelöst. Das Installationsverzeichnis ist für normale +> Benutzerkonten schreibgeschützt. Log4j2 scheitert dann still, ohne eine sichtbare +> Fehlermeldung zu erzeugen. + +> **Warnung – Backslashes in `.properties`-Dateien:** +> In Java-`.properties`-Dateien werden Backslashes (`\`) als Escape-Zeichen interpretiert. +> Windows-Pfade wie `C:\Users\Funny\Logs` müssen entweder mit Forward-Slashes +> (`C:/Users/Funny/Logs`) oder mit doppelten Backslashes (`C:\\Users\\Funny\\Logs`) +> angegeben werden. Einfache Backslashes werden stillschweigend falsch interpretiert. + +Betroffene Parameter: + +| Parameter | Empfehlung | +|---|---| +| `log.directory` | Absoluter Pfad, z. B. `C:/ProgramData/PDF KI Renamer/logs` | +| `runtime.lock.file` | Absoluter Pfad, z. B. `C:/ProgramData/PDF KI Renamer/pdf-umbenenner.lock` | +| `prompt.template.file` | Absoluter Pfad, z. B. `C:/ProgramData/PDF KI Renamer/config/prompts/template.txt` | +| `sqlite.file` | Absoluter Pfad, z. B. `C:/ProgramData/PDF KI Renamer/config/pdf-umbenenner.db` | + +Das empfohlene Konfigurationsverzeichnis für alle schreibbaren Daten im MSI-Betrieb ist +`C:\ProgramData\PDF KI Renamer\`, da dieses Verzeichnis standardmäßig für alle +Benutzerkonten schreibbar ist und bei der Deinstallation erhalten bleibt. + +**Diagnose: Log-Datei-Prüfpunkt in den technischen Tests** + +Die technischen Tests (Schaltfläche „Technische Tests ausführen" im Konfigurationseditor) +enthalten einen dedizierten Prüfpunkt **„Log-Verzeichnis beschreibbar"**, der anzeigt: + +- den konfigurierten `log.directory`-Wert (roh und als aufgelöster absoluter Pfad), +- ob das Verzeichnis vorhanden und beschreibbar ist, +- den tatsächlichen Log-Dateipfad aus der laufenden Log4j2-Konfiguration. + +Ein nicht beschreibbares Log-Verzeichnis wird als **Warnung** angezeigt, nicht als Fehler +(die Anwendung kann ohne Datei-Logging laufen). Der Prüfpunkt hilft, den typischen +MSI-Betriebsfehler – relatives `log.directory` auf schreibgeschütztem Installationspfad – +frühzeitig zu erkennen. ### MSI-Release-Checkliste diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java index ac62940..d01a9d4 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiConfigurationEditorWorkspace.java @@ -495,6 +495,7 @@ public final class GuiConfigurationEditorWorkspace { () -> editorState.loadedFileSnapshot() .map(snapshot -> snapshot.filePath().toString()) .orElse(""), + () -> editorState.values().logDirectory(), pendingMessages, report -> { technicalTestsButton.setDisable(false); diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java index 5bd7e7f..85c6dfa 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiStartupContext.java @@ -344,7 +344,8 @@ public record GuiStartupContext( TechnicalTestOrchestrator noOpOrchestrator = new TechnicalTestOrchestrator( new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(), noOpPathCheckPort, - noOpTestService); + noOpTestService, + () -> java.util.Optional.empty()); ResourceCreationPort noOpResourceCreationPort = new ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTechnicalTestCoordinator.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTechnicalTestCoordinator.java index 15a9e99..de88dc6 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTechnicalTestCoordinator.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTechnicalTestCoordinator.java @@ -64,6 +64,7 @@ public final class GuiTechnicalTestCoordinator { private final TechnicalTestOrchestrator orchestrator; private final Supplier inputProvider; private final Supplier configFilePathProvider; + private final Supplier logDirectoryProvider; private final List pendingMessages; private final Consumer postResultCallback; @@ -89,6 +90,9 @@ public final class GuiTechnicalTestCoordinator { * @param configFilePathProvider Lieferant des aktuell geladenen Konfigurationsdateipfads als String; * gibt eine leere Zeichenkette zurück wenn keine Datei geladen ist; * darf nicht {@code null} sein + * @param logDirectoryProvider Lieferant des konfigurierten {@code log.directory}-Rohwerts; + * gibt eine leere Zeichenkette zurück wenn kein Wert konfiguriert ist; + * darf nicht {@code null} sein * @param pendingMessages geteilte veränderliche Nachrichtenliste; darf nicht {@code null} sein * @param postResultCallback Callback nach erfolgreicher Ergebnisanwendung; darf nicht {@code null} sein * @throws NullPointerException wenn einer der Parameter {@code null} ist @@ -96,11 +100,13 @@ public final class GuiTechnicalTestCoordinator { public GuiTechnicalTestCoordinator(TechnicalTestOrchestrator orchestrator, Supplier inputProvider, Supplier configFilePathProvider, + Supplier logDirectoryProvider, List pendingMessages, Consumer postResultCallback) { this.orchestrator = Objects.requireNonNull(orchestrator, "orchestrator must not be null"); this.inputProvider = Objects.requireNonNull(inputProvider, "inputProvider must not be null"); this.configFilePathProvider = Objects.requireNonNull(configFilePathProvider, "configFilePathProvider must not be null"); + this.logDirectoryProvider = Objects.requireNonNull(logDirectoryProvider, "logDirectoryProvider must not be null"); this.pendingMessages = Objects.requireNonNull(pendingMessages, "pendingMessages must not be null"); this.postResultCallback = Objects.requireNonNull(postResultCallback, "postResultCallback must not be null"); this.testThreadFactory = task -> { @@ -134,7 +140,8 @@ public final class GuiTechnicalTestCoordinator { pendingMessages.clear(); EditorValidationInput input = inputProvider.get(); String configFilePath = configFilePathProvider.get(); - TechnicalTestRequest request = new TechnicalTestRequest(input, configFilePath); + String logDirectory = logDirectoryProvider.get(); + TechnicalTestRequest request = new TechnicalTestRequest(input, configFilePath, logDirectory); LOG.info("GUI-Gesamttest: Technische Tests ausführen gestartet."); @@ -234,6 +241,7 @@ public final class GuiTechnicalTestCoordinator { case SOURCE_FOLDER_PRESENT -> "Quellordner vorhanden und lesbar"; case TARGET_FOLDER_USABLE -> "Zielordner vorhanden oder anlegbar sowie schreibbar"; case SQLITE_PATH_USABLE -> "SQLite-Pfad technisch nutzbar"; + case LOG_DIRECTORY_USABLE -> "Log-Verzeichnis beschreibbar"; }; } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiAdapterSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiAdapterSmokeTest.java index 6239f66..9009444 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiAdapterSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiAdapterSmokeTest.java @@ -419,7 +419,8 @@ class GuiAdapterSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorFieldBindingTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorFieldBindingTest.java index 99bf004..297d9ef 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorFieldBindingTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorFieldBindingTest.java @@ -345,7 +345,8 @@ class GuiEditorFieldBindingTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorIntegrationTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorIntegrationTest.java index b262e45..02eeaf8 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorIntegrationTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorIntegrationTest.java @@ -137,7 +137,8 @@ class GuiEditorIntegrationTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -287,7 +288,8 @@ class GuiEditorIntegrationTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -371,7 +373,8 @@ class GuiEditorIntegrationTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorRegressionSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorRegressionSmokeTest.java index c1dca13..6ae9d62 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorRegressionSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorRegressionSmokeTest.java @@ -208,7 +208,8 @@ class GuiEditorRegressionSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -347,7 +348,8 @@ class GuiEditorRegressionSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -471,7 +473,8 @@ class GuiEditorRegressionSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -599,7 +602,8 @@ class GuiEditorRegressionSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -698,7 +702,8 @@ class GuiEditorRegressionSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorValidationSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorValidationSmokeTest.java index df22ac6..7b469aa 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorValidationSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiEditorValidationSmokeTest.java @@ -142,7 +142,8 @@ class GuiEditorValidationSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -272,7 +273,8 @@ class GuiEditorValidationSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiMessageAreaSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiMessageAreaSmokeTest.java index 925f1fd..02e5f9d 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiMessageAreaSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiMessageAreaSmokeTest.java @@ -336,7 +336,8 @@ class GuiMessageAreaSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -478,7 +479,8 @@ class GuiMessageAreaSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -565,7 +567,8 @@ class GuiMessageAreaSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -888,7 +891,8 @@ class GuiMessageAreaSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogSmokeTest.java index a9d2cfa..59f85f7 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogSmokeTest.java @@ -529,7 +529,8 @@ class GuiModelCatalogSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTechnicalTestCoordinatorSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTechnicalTestCoordinatorSmokeTest.java index 04355ff..76e54c7 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTechnicalTestCoordinatorSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiTechnicalTestCoordinatorSmokeTest.java @@ -39,7 +39,7 @@ import javafx.scene.control.Button; * {@code technical-tests-button}. *
  • Triggering the coordinator synchronously populates {@code pendingMessages} * with entries tagged {@link GuiTechnicalTestCoordinator#SOURCE_TAG}.
  • - *
  • A second trigger appends a fresh batch of test entries (accumulation semantics).
  • + *
  • A second trigger replaces the previous batch of test entries.
  • *
  • The post-result callback is invoked after the result is applied.
  • * *

    @@ -138,12 +138,12 @@ class GuiTechnicalTestCoordinatorSmokeTest { /** * Smoke test: after one trigger, the number of entries tagged SOURCE_TAG equals - * 11 (one per checkpoint) plus 1 summary entry = 12. + * 12 (one per checkpoint) plus 1 summary entry = 13. * * @throws Exception if the FX thread task fails or times out */ @Test - void trigger_producesElevenCheckpointEntriesPlusSummary() throws Exception { + void trigger_producesTwelveCheckpointEntriesPlusSummary() throws Exception { runOnFx(() -> { List messages = new ArrayList<>(); GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { }); @@ -155,9 +155,9 @@ class GuiTechnicalTestCoordinatorSmokeTest { && GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get())) .count(); - // 11 checkpoint entries + 1 summary entry = 12 - assertEquals(12, taggedCount, - "Expected 11 checkpoint entries + 1 summary entry = 12 tagged messages"); + // 12 checkpoint entries + 1 summary entry = 13 + assertEquals(13, taggedCount, + "Expected 12 checkpoint entries + 1 summary entry = 13 tagged messages"); }); } @@ -256,12 +256,14 @@ class GuiTechnicalTestCoordinatorSmokeTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), noOpPathCheckPort(), - noOpProviderService()); + noOpProviderService(), + () -> java.util.Optional.empty()); GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator( orchestrator, currentInput::get, // always reads the current reference () -> "", + () -> "", messages, report -> { }); @@ -365,7 +367,8 @@ class GuiTechnicalTestCoordinatorSmokeTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), noOpPathCheckPort(), - noOpProviderService()); + noOpProviderService(), + () -> java.util.Optional.empty()); EditorValidationInput blankInput = new EditorValidationInput( "claude", @@ -380,6 +383,7 @@ class GuiTechnicalTestCoordinatorSmokeTest { orchestrator, () -> blankInput, () -> "", + () -> "", messages, postResultCallback); diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiUnsavedChangesGuardSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiUnsavedChangesGuardSmokeTest.java index 3f15480..15a06b0 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiUnsavedChangesGuardSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiUnsavedChangesGuardSmokeTest.java @@ -806,7 +806,8 @@ class GuiUnsavedChangesGuardSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -851,7 +852,8 @@ class GuiUnsavedChangesGuardSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())), + (fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiValidateActionSmokeTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiValidateActionSmokeTest.java index 9c79cb6..320fcb5 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiValidateActionSmokeTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiValidateActionSmokeTest.java @@ -323,7 +323,8 @@ class GuiValidateActionSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - noOpApiKeyResolutionPort())), + noOpApiKeyResolutionPort()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } @@ -390,7 +391,8 @@ class GuiValidateActionSmokeTest { }, new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService( req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"), - noOpApiKeyResolutionPort())), + noOpApiKeyResolutionPort()), + () -> java.util.Optional.empty()), new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() { @Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CheckpointId.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CheckpointId.java index 3d47167..f07d9d9 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CheckpointId.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CheckpointId.java @@ -84,5 +84,13 @@ public enum CheckpointId { * zeigt auf eine vorhandene Datei oder auf einen beschreibbaren Ordner, in dem die * Datei neu angelegt werden kann. */ - SQLITE_PATH_USABLE + SQLITE_PATH_USABLE, + + /** + * Log-Verzeichnis beschreibbar – das konfigurierte (oder Standard-)Log-Verzeichnis + * ist vorhanden und schreibbar. Zeigt zusätzlich den tatsächlichen Log-Dateipfad + * aus der aktiven Log4j2-Konfiguration an. Ein nicht beschreibbares Log-Verzeichnis + * ist eine Warnung, kein harter Fehler. + */ + LOG_DIRECTORY_USABLE } diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/LogDiagnosticsPort.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/LogDiagnosticsPort.java new file mode 100644 index 0000000..e551e02 --- /dev/null +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/LogDiagnosticsPort.java @@ -0,0 +1,28 @@ +package de.gecheckt.pdf.umbenenner.application.validation.technicaltest; + +import java.util.Optional; + +/** + * Ausgehender Port zur Diagnose des aktiven Log-Ausgabepfads. + *

    + * Implementierungen lesen den tatsächlich von der Logging-Infrastruktur verwendeten + * Dateipfad aus der laufenden Konfiguration des Logging-Frameworks aus. Der Port ist + * provider-neutral; er kennt weder Log4j2 noch andere Framework-spezifische Typen. + *

    + * Diese Information ergänzt den konfigurierten {@code log.directory}-Wert aus der + * Properties-Datei und zeigt, wo Logeinträge tatsächlich landen – unabhängig davon, + * ob das Log-Verzeichnis zum Zeitpunkt des Tests beschreibbar ist. + */ +public interface LogDiagnosticsPort { + + /** + * Ermittelt den absoluten Dateipfad der aktiven Log-Ausgabedatei. + *

    + * Gibt einen leeren {@link Optional} zurück, wenn der Pfad nicht bestimmbar ist – + * beispielsweise weil kein dateibasierter Appender aktiv ist oder die + * Logging-Konfiguration nicht ausgelesen werden kann. + * + * @return absoluter Pfad der aktiven Log-Datei; leer wenn nicht bestimmbar + */ + Optional resolveActiveLogFilePath(); +} diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestOrchestrator.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestOrchestrator.java index fbc29f3..d792824 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestOrchestrator.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestOrchestrator.java @@ -17,16 +17,18 @@ import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidation /** * Orchestrator für den vollständigen technischen Gesamttest der GUI-Konfiguration. *

    - * Führt alle elf definierten Prüfpunkte in drei voneinander unabhängigen Blöcken aus: + * Führt alle zwölf definierten Prüfpunkte in drei voneinander unabhängigen Blöcken aus: *

      *
    1. Lokale Validierung: Prüft den Editorzustand ohne I/O mithilfe des * {@link EditorConfigurationValidator}. Erzeugt Ergebnisse für * {@link CheckpointId#CONFIGURATION_BASIC_VALIDATION} und * {@link CheckpointId#PROVIDER_CONFIGURATION}.
    2. - *
    3. Pfadprüfungen: Prüft Quellordner, Zielordner, Prompt-Datei und - * SQLite-Pfad über den {@link PathCheckPort}. Erzeugt Ergebnisse für + *
    4. Pfadprüfungen: Prüft Quellordner, Zielordner, Prompt-Datei, + * SQLite-Pfad und Log-Verzeichnis über den {@link PathCheckPort} sowie den + * {@link LogDiagnosticsPort}. Erzeugt Ergebnisse für * {@link CheckpointId#PROMPT_FILE_PRESENT}, {@link CheckpointId#SOURCE_FOLDER_PRESENT}, - * {@link CheckpointId#TARGET_FOLDER_USABLE} und {@link CheckpointId#SQLITE_PATH_USABLE}.
    5. + * {@link CheckpointId#TARGET_FOLDER_USABLE}, {@link CheckpointId#SQLITE_PATH_USABLE} + * und {@link CheckpointId#LOG_DIRECTORY_USABLE}. *
    6. Provider-Prüfungen: Prüft Endpoint, API-Key, Modellliste und * Modellplausibilität über den {@link ProviderTechnicalTestService}. Erzeugt Ergebnisse für * {@link CheckpointId#BASE_URL_REACHABLE}, {@link CheckpointId#API_KEY_PRESENT}, @@ -38,7 +40,7 @@ import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidation * ausgeführt, auch wenn ein Block eine Exception wirft. In diesem Fall werden die * betroffenen Checkpoints als {@link CheckpointResult.Failure} mit Schweregrad ERROR * und dem Präfix „Interner Fehler:" markiert. Der Gesamtbericht enthält immer genau - * elf Einträge. + * zwölf Einträge. *

      * Threading-Kontrakt: Die Methode {@link #run(TechnicalTestRequest)} * ist synchron blockierend (der Provider-Prüfblock führt HTTP-Aufrufe durch). Sie darf @@ -51,28 +53,32 @@ import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidation * wird {@code config/prompt.txt} relativ zum Arbeitsverzeichnis verwendet. *

      * Dieser Service enthält keine JavaFX-Typen, keine NIO-Pfadobjekte in Signaturen und - * keine Infrastrukturabhängigkeiten jenseits der drei injizierten Abhängigkeiten. + * keine Infrastrukturabhängigkeiten jenseits der vier injizierten Abhängigkeiten. */ public class TechnicalTestOrchestrator { private final EditorConfigurationValidator editorValidator; private final PathCheckPort pathCheckPort; private final ProviderTechnicalTestService providerTestService; + private final LogDiagnosticsPort logDiagnosticsPort; /** - * Erstellt einen neuen Orchestrator mit den drei erforderlichen Abhängigkeiten. + * Erstellt einen neuen Orchestrator mit den vier erforderlichen Abhängigkeiten. * - * @param editorValidator Lokaler Konfigurationsvalidator; darf nicht {@code null} sein - * @param pathCheckPort Port für Dateisystem-Pfadprüfungen; darf nicht {@code null} sein + * @param editorValidator Lokaler Konfigurationsvalidator; darf nicht {@code null} sein + * @param pathCheckPort Port für Dateisystem-Pfadprüfungen; darf nicht {@code null} sein * @param providerTestService Service für provider-nahe technische Prüfungen; darf nicht {@code null} sein + * @param logDiagnosticsPort Port zur Auflösung des aktiven Log-Dateipfads; darf nicht {@code null} sein * @throws NullPointerException wenn einer der Parameter {@code null} ist */ public TechnicalTestOrchestrator(EditorConfigurationValidator editorValidator, PathCheckPort pathCheckPort, - ProviderTechnicalTestService providerTestService) { + ProviderTechnicalTestService providerTestService, + LogDiagnosticsPort logDiagnosticsPort) { this.editorValidator = Objects.requireNonNull(editorValidator, "editorValidator must not be null"); this.pathCheckPort = Objects.requireNonNull(pathCheckPort, "pathCheckPort must not be null"); this.providerTestService = Objects.requireNonNull(providerTestService, "providerTestService must not be null"); + this.logDiagnosticsPort = Objects.requireNonNull(logDiagnosticsPort, "logDiagnosticsPort must not be null"); } /** @@ -80,7 +86,7 @@ public class TechnicalTestOrchestrator { *

      * Alle drei Prüfblöcke werden immer vollständig ausgeführt. Ein Fehler in einem Block * führt nicht dazu, dass ein anderer Block übersprungen wird. Der zurückgegebene Bericht - * enthält immer genau elf {@link CheckpointResult}-Einträge. + * enthält immer genau zwölf {@link CheckpointResult}-Einträge. *

      * Prompt-Datei-Standardpfad: Wenn der Editorzustand keinen Prompt-Pfad * enthält, wird als Standardpfad der Elternordner der Konfigurationsdatei gewählt @@ -96,7 +102,7 @@ public class TechnicalTestOrchestrator { * abgeschlossen sind. Sie darf nicht auf dem JavaFX Application Thread aufgerufen werden. * * @param request Eingabedaten für den Gesamttest; darf nicht {@code null} sein - * @return vollständiger Gesamttestbericht mit genau elf Einträgen; nie {@code null} + * @return vollständiger Gesamttestbericht mit genau zwölf Einträgen; nie {@code null} * @throws NullPointerException wenn {@code request} {@code null} ist */ public TechnicalTestReport run(TechnicalTestRequest request) { @@ -104,13 +110,13 @@ public class TechnicalTestOrchestrator { Instant startTime = Instant.now(); EditorValidationInput input = request.validationInput(); - List results = new ArrayList<>(11); + List results = new ArrayList<>(12); // Block 1: Lokale Konfigurationsvalidierung (kein I/O) results.addAll(runLocalValidationBlock(input)); // Block 2: Pfadprüfungen (Dateisystem-I/O) - results.addAll(runPathCheckBlock(input, request.configFilePath())); + results.addAll(runPathCheckBlock(input, request.configFilePath(), request.logDirectory())); // Block 3: Provider-nahe technische Prüfungen (Netzwerk-I/O) results.addAll(runProviderCheckBlock(input)); @@ -222,25 +228,30 @@ public class TechnicalTestOrchestrator { // ========================================================================= /** - * Führt die Dateisystem-Pfadprüfungen für Prompt-Datei, Quellordner, Zielordner - * und SQLite-Pfad durch. + * Führt die Dateisystem-Pfadprüfungen für Prompt-Datei, Quellordner, Zielordner, + * SQLite-Pfad und Log-Verzeichnis durch. *

      * Der {@code configFilePath} wird genutzt, um bei fehlendem Prompt-Pfad im Editorzustand * einen sinnvollen Standardpfad zu bestimmen ({@code /prompt.txt}). + * Der {@code logDirectory} ist der konfigurierte Rohwert von {@code log.directory}; + * leer bedeutet Standardwert {@code ./logs/}. * * @param input aktueller Editorzustand * @param configFilePath Pfad der geladenen Konfigurationsdatei; leer wenn keine geladen - * @return Liste mit genau vier Einträgen + * @param logDirectory konfigurierter Rohwert von {@code log.directory}; leer = Standard + * @return Liste mit genau fünf Einträgen */ private List runPathCheckBlock(EditorValidationInput input, - String configFilePath) { + String configFilePath, + String logDirectory) { try { - List results = new ArrayList<>(4); + List results = new ArrayList<>(5); results.add(checkPromptFile(input.promptTemplateFile(), configFilePath, resolveMaxTitleLengthForPromptCreation(input.maxTitleLength()))); results.add(checkSourceFolder(input.sourceFolder())); results.add(checkTargetFolder(input.targetFolder())); results.add(checkSqlitePath(input.sqliteFile())); + results.add(checkLogDirectory(logDirectory)); return results; } catch (Exception e) { String errorMsg = "Interner Fehler bei den Pfadprüfungen: " + e.getMessage(); @@ -252,6 +263,8 @@ public class TechnicalTestOrchestrator { CheckpointResult.Failure.of(CheckpointId.TARGET_FOLDER_USABLE, CheckpointSeverity.ERROR, errorMsg), CheckpointResult.Failure.of(CheckpointId.SQLITE_PATH_USABLE, + CheckpointSeverity.ERROR, errorMsg), + CheckpointResult.Failure.of(CheckpointId.LOG_DIRECTORY_USABLE, CheckpointSeverity.ERROR, errorMsg) ); } @@ -471,6 +484,64 @@ public class TechnicalTestOrchestrator { suggestion); } + /** + * Prüft das Log-Verzeichnis auf Schreibbarkeit und zeigt den tatsächlichen + * Log-Dateipfad aus der aktiven Log4j2-Konfiguration an. + *

      + * Verzeichnis-Ermittlung: Wenn der konfigurierte {@code log.directory}-Wert + * leer ist, wird der Standard {@code ./logs} relativ zum Arbeitsverzeichnis angenommen. + * Der Wert wird zu einem absoluten Pfad aufgelöst. + *

      + * Ergebnis: + *

        + *
      • {@link CheckpointResult.Success}: Verzeichnis ist vorhanden und schreibbar. + * Die Meldung enthält den aufgelösten absoluten Pfad sowie – sofern ermittelbar – + * den tatsächlichen Log-Dateipfad aus Log4j2.
      • + *
      • {@link CheckpointResult.Failure} mit Schweregrad {@link CheckpointSeverity#WARNING}: + * Verzeichnis ist nicht vorhanden oder nicht schreibbar. Ein nicht beschreibbares + * Log-Verzeichnis ist eine Warnung, kein harter Fehler, da die Anwendung auch ohne + * Datei-Logging lauffähig ist. Die Meldung enthält Konfiguration und aufgelösten + * absoluten Pfad als Diagnoseinformation.
      • + *
      + * + * @param configuredLogDir konfigurierter Rohwert von {@code log.directory}; leer = Standard + * @return Prüfpunkt-Ergebnis + */ + private CheckpointResult checkLogDirectory(String configuredLogDir) { + String effectiveDir = (configuredLogDir == null || configuredLogDir.isBlank()) + ? "./logs" : configuredLogDir; + + String absolutePath; + try { + absolutePath = Paths.get(effectiveDir).toAbsolutePath().toString(); + } catch (java.nio.file.InvalidPathException e) { + return CheckpointResult.Failure.of(CheckpointId.LOG_DIRECTORY_USABLE, + CheckpointSeverity.WARNING, + "Log-Verzeichnis: ungültiger Pfad: " + effectiveDir); + } + + boolean configuredExplicitly = configuredLogDir != null && !configuredLogDir.isBlank(); + String configLabel = configuredExplicitly + ? "konfiguriert: " + configuredLogDir + " → " + : "Standard → "; + + java.util.Optional activeLogFile = logDiagnosticsPort.resolveActiveLogFilePath(); + String logFileInfo = activeLogFile + .map(p -> " | Aktive Log-Datei: " + p) + .orElse(""); + + if (pathCheckPort.isDirectoryWritableOrCreatable(absolutePath)) { + return new CheckpointResult.Success(CheckpointId.LOG_DIRECTORY_USABLE, + "Log-Verzeichnis beschreibbar (" + configLabel + absolutePath + ")" + logFileInfo); + } + + return CheckpointResult.Failure.of(CheckpointId.LOG_DIRECTORY_USABLE, + CheckpointSeverity.WARNING, + "Log-Verzeichnis nicht beschreibbar (" + configLabel + absolutePath + ")" + + logFileInfo + + ". Tipp: Absoluten Pfad mit Forward-Slashes verwenden, z. B. C:/Benutzer/Logs"); + } + // ========================================================================= // Block 3: Provider-nahe technische Prüfungen // ========================================================================= diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestRequest.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestRequest.java index ffbb8c2..9176455 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestRequest.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestRequest.java @@ -15,36 +15,44 @@ import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidation * Gesamttest, bei der automatischen Prompt-Erzeugung den Standardpfad relativ zur * Konfigurationsdatei zu bestimmen. Er ist leer, wenn keine Konfigurationsdatei geladen ist. *

      + * Das {@code logDirectory}-Feld trägt den konfigurierten Rohwert von {@code log.directory} + * aus dem Editor; leer bedeutet Standardwert {@code ./logs/}. + *

      * Dieser Record enthält keine JavaFX-Typen und keine Infrastrukturabhängigkeiten. * * @param validationInput aktueller Editorzustand; nie {@code null} * @param configFilePath optionaler Pfad der geladenen Konfigurationsdatei als String; * leer wenn keine Datei geladen ist + * @param logDirectory konfigurierter Rohwert von {@code log.directory}; + * leer wenn kein Wert konfiguriert ist (Standard {@code ./logs/}) */ public record TechnicalTestRequest( EditorValidationInput validationInput, - String configFilePath) { + String configFilePath, + String logDirectory) { /** * Erstellt eine neue Gesamttest-Anforderung. * * @param validationInput aktueller Editorzustand; darf nicht {@code null} sein * @param configFilePath Pfad der Konfigurationsdatei; {@code null} wird zu leerem String + * @param logDirectory Rohwert von {@code log.directory}; {@code null} wird zu leerem String * @throws NullPointerException wenn {@code validationInput} {@code null} ist */ public TechnicalTestRequest { Objects.requireNonNull(validationInput, "validationInput must not be null"); configFilePath = configFilePath == null ? "" : configFilePath; + logDirectory = logDirectory == null ? "" : logDirectory; } /** - * Erstellt eine Anforderung ohne geladene Konfigurationsdatei. + * Erstellt eine Anforderung ohne geladene Konfigurationsdatei und ohne Log-Verzeichnis-Angabe. * * @param validationInput aktueller Editorzustand; darf nicht {@code null} sein * @return eine neue Anforderung ohne Konfigurationsdateipfad */ public static TechnicalTestRequest of(EditorValidationInput validationInput) { - return new TechnicalTestRequest(validationInput, ""); + return new TechnicalTestRequest(validationInput, "", ""); } /** diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CheckpointIdTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CheckpointIdTest.java index 4845c1c..a066ef7 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CheckpointIdTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/CheckpointIdTest.java @@ -23,13 +23,14 @@ class CheckpointIdTest { CheckpointId.PROMPT_FILE_PRESENT, CheckpointId.SOURCE_FOLDER_PRESENT, CheckpointId.TARGET_FOLDER_USABLE, - CheckpointId.SQLITE_PATH_USABLE + CheckpointId.SQLITE_PATH_USABLE, + CheckpointId.LOG_DIRECTORY_USABLE ); } @Test - void enumHasExactlyElevenValues() { - assertThat(CheckpointId.values()).hasSize(11); + void enumHasTwelveValues() { + assertThat(CheckpointId.values()).hasSize(12); } @Test diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestOrchestratorTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestOrchestratorTest.java index 1c8db0a..a81bb1c 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestOrchestratorTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestOrchestratorTest.java @@ -15,7 +15,7 @@ import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidation *

      * Prüft insbesondere, dass alle drei Blöcke immer vollständig durchlaufen werden * (kein Frühabbruch), auch wenn ein Block eine Exception wirft, und dass der - * zurückgegebene Bericht immer genau elf Einträge enthält. + * zurückgegebene Bericht immer genau zwölf Einträge enthält. */ class TechnicalTestOrchestratorTest { @@ -88,6 +88,11 @@ class TechnicalTestOrchestratorTest { (family, propertyValue) -> EffectiveApiKeyDescriptor.fromPropertyFile()); } + /** No-op {@link LogDiagnosticsPort}: gibt immer einen leeren Optional zurück. */ + private static LogDiagnosticsPort noOpLogDiagnosticsPort() { + return () -> java.util.Optional.empty(); + } + private static CheckpointResult findById(List results, CheckpointId id) { return results.stream() .filter(r -> r.checkpointId() == id) @@ -98,19 +103,20 @@ class TechnicalTestOrchestratorTest { // ------------------------------------------------------------------ Tests: Vollständig grüner Pfad /** - * Alle drei Blöcke liefern Erfolg: der Bericht enthält genau 11 Success-Einträge. + * Alle drei Blöcke liefern Erfolg: der Bericht enthält genau 12 Success-Einträge. */ @Test - void allBlocksSucceed_reportContainsElevenSuccessEntries() { + void allBlocksSucceed_reportContainsTwelveSuccessEntries() { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), allOkPathCheckPort(), - allSuccessProviderService()); + allSuccessProviderService(), + noOpLogDiagnosticsPort()); TechnicalTestReport report = orchestrator.run( TechnicalTestRequest.of(validClaudeInput())); - assertThat(report.size()).isEqualTo(11); + assertThat(report.size()).isEqualTo(12); assertThat(report.results()) .allSatisfy(r -> assertThat(r).isInstanceOf(CheckpointResult.Success.class)); assertThat(report.hasErrors()).isFalse(); @@ -120,19 +126,20 @@ class TechnicalTestOrchestratorTest { // ------------------------------------------------------------------ Tests: Kein Frühabbruch /** - * Bericht enthält immer genau 11 Einträge, auch wenn Block 2 und Block 3 alle Fehler liefern. + * Bericht enthält immer genau 12 Einträge, auch wenn Block 2 und Block 3 alle Fehler liefern. */ @Test - void alwaysElevenCheckpointsInReport_evenWithFailures() { + void alwaysTwelveCheckpointsInReport_evenWithFailures() { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), noOpPathCheckPort(), // Block 2: alle Pfade nicht vorhanden - throwingProviderService()); // Block 3: Exception + throwingProviderService(), // Block 3: Exception + noOpLogDiagnosticsPort()); TechnicalTestReport report = orchestrator.run( TechnicalTestRequest.of(emptyProviderInput())); - assertThat(report.size()).isEqualTo(11); + assertThat(report.size()).isEqualTo(12); } // ------------------------------------------------------------------ Tests: Block 1 - Exception führt zu Failure @@ -155,12 +162,13 @@ class TechnicalTestOrchestratorTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( throwingValidator, allOkPathCheckPort(), - allSuccessProviderService()); + allSuccessProviderService(), + noOpLogDiagnosticsPort()); TechnicalTestReport report = orchestrator.run( TechnicalTestRequest.of(validClaudeInput())); - assertThat(report.size()).isEqualTo(11); + assertThat(report.size()).isEqualTo(12); // Block-1-Checkpoints müssen Failure mit "Interner Fehler" sein CheckpointResult basicValidation = findById(report.results(), @@ -197,12 +205,13 @@ class TechnicalTestOrchestratorTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), allOkPathCheckPort(), - throwingProviderService()); + throwingProviderService(), + noOpLogDiagnosticsPort()); TechnicalTestReport report = orchestrator.run( TechnicalTestRequest.of(validClaudeInput())); - assertThat(report.size()).isEqualTo(11); + assertThat(report.size()).isEqualTo(12); // Block-3-Checkpoints müssen Failure mit "Interner Fehler" sein List block3Ids = List.of( @@ -250,7 +259,8 @@ class TechnicalTestOrchestratorTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), pathPort, - allSuccessProviderService()); + allSuccessProviderService(), + noOpLogDiagnosticsPort()); TechnicalTestReport report = orchestrator.run( TechnicalTestRequest.of(validClaudeInput())); @@ -280,7 +290,8 @@ class TechnicalTestOrchestratorTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), pathPort, - allSuccessProviderService()); + allSuccessProviderService(), + noOpLogDiagnosticsPort()); TechnicalTestReport report = orchestrator.run( TechnicalTestRequest.of(validClaudeInput())); @@ -310,7 +321,8 @@ class TechnicalTestOrchestratorTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), pathPort, - allSuccessProviderService()); + allSuccessProviderService(), + noOpLogDiagnosticsPort()); TechnicalTestReport report = orchestrator.run( TechnicalTestRequest.of(validClaudeInput())); @@ -327,14 +339,15 @@ class TechnicalTestOrchestratorTest { // ------------------------------------------------------------------ Tests: Korrekte Checkpoint-Reihenfolge /** - * Der Bericht enthält genau die erwarteten 11 Checkpoint-IDs. + * Der Bericht enthält genau die erwarteten 12 Checkpoint-IDs. */ @Test void report_containsAllExpectedCheckpointIds() { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), noOpPathCheckPort(), - allSuccessProviderService()); + allSuccessProviderService(), + noOpLogDiagnosticsPort()); TechnicalTestReport report = orchestrator.run( TechnicalTestRequest.of(validClaudeInput())); @@ -350,6 +363,7 @@ class TechnicalTestOrchestratorTest { CheckpointId.SOURCE_FOLDER_PRESENT, CheckpointId.TARGET_FOLDER_USABLE, CheckpointId.SQLITE_PATH_USABLE, + CheckpointId.LOG_DIRECTORY_USABLE, CheckpointId.API_KEY_PRESENT, CheckpointId.BASE_URL_REACHABLE, CheckpointId.API_KEY_ACCEPTED, @@ -387,11 +401,12 @@ class TechnicalTestOrchestratorTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), pathPort, - allSuccessProviderService()); + allSuccessProviderService(), + noOpLogDiagnosticsPort()); // configFilePath = C:/config/application.properties → Elternordner = C:/config TechnicalTestReport report = orchestrator.run( - new TechnicalTestRequest(inputWithoutPrompt, "C:/config/application.properties")); + new TechnicalTestRequest(inputWithoutPrompt, "C:/config/application.properties", "")); CheckpointResult promptResult = findById(report.results(), CheckpointId.PROMPT_FILE_PRESENT); assertThat(promptResult).isInstanceOf(CheckpointResult.Failure.class); @@ -431,7 +446,8 @@ class TechnicalTestOrchestratorTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), pathPort, - allSuccessProviderService()); + allSuccessProviderService(), + noOpLogDiagnosticsPort()); // Kein configFilePath → Fallback auf config/prompt.txt TechnicalTestReport report = orchestrator.run( @@ -462,7 +478,8 @@ class TechnicalTestOrchestratorTest { TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), pathPort, - allSuccessProviderService()); + allSuccessProviderService(), + noOpLogDiagnosticsPort()); TechnicalTestReport report = orchestrator.run( TechnicalTestRequest.of(validClaudeInput())); diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestRequestTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestRequestTest.java index b23c09e..a72f60d 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestRequestTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/validation/technicaltest/TechnicalTestRequestTest.java @@ -29,18 +29,30 @@ class TechnicalTestRequestTest { @Test void withConfigFilePath_detectedAsPresent() { - var request = new TechnicalTestRequest(minimalInput(), "/config/app.properties"); + var request = new TechnicalTestRequest(minimalInput(), "/config/app.properties", ""); assertThat(request.hasConfigFilePath()).isTrue(); assertThat(request.configFilePath()).isEqualTo("/config/app.properties"); } @Test void nullConfigFilePath_normalisedToEmpty() { - var request = new TechnicalTestRequest(minimalInput(), null); + var request = new TechnicalTestRequest(minimalInput(), null, null); assertThat(request.configFilePath()).isEmpty(); assertThat(request.hasConfigFilePath()).isFalse(); } + @Test + void withLogDirectory_preservedAsIs() { + var request = new TechnicalTestRequest(minimalInput(), "", "C:/logs"); + assertThat(request.logDirectory()).isEqualTo("C:/logs"); + } + + @Test + void nullLogDirectory_normalisedToEmpty() { + var request = new TechnicalTestRequest(minimalInput(), "", null); + assertThat(request.logDirectory()).isEmpty(); + } + @Test void nullValidationInputThrows() { assertThatNullPointerException() 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 15bfa96..c0b1cec 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 @@ -109,6 +109,7 @@ import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderT import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator; import de.gecheckt.pdf.umbenenner.bootstrap.adapter.AiModelCatalogDispatcher; import de.gecheckt.pdf.umbenenner.bootstrap.adapter.GuiConfigurationPropertiesWriter; +import de.gecheckt.pdf.umbenenner.bootstrap.adapter.Log4jLogDiagnosticsAdapter; import de.gecheckt.pdf.umbenenner.bootstrap.adapter.Log4jProcessingLogger; import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.AnotherInstanceRunningException; import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.SingleInstanceGuard; @@ -807,7 +808,8 @@ public class BootstrapRunner { TechnicalTestOrchestrator technicalTestOrchestrator = new TechnicalTestOrchestrator( new EditorConfigurationValidator(), pathCheckPort, - providerTechnicalTestService); + providerTechnicalTestService, + new Log4jLogDiagnosticsAdapter()); de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService correctionExecutionService = new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService( new de.gecheckt.pdf.umbenenner.adapter.out.resourcecreation.FilesystemResourceCreationAdapter()); diff --git a/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jLogDiagnosticsAdapter.java b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jLogDiagnosticsAdapter.java new file mode 100644 index 0000000..6a30beb --- /dev/null +++ b/pdf-umbenenner-bootstrap/src/main/java/de/gecheckt/pdf/umbenenner/bootstrap/adapter/Log4jLogDiagnosticsAdapter.java @@ -0,0 +1,68 @@ +package de.gecheckt.pdf.umbenenner.bootstrap.adapter; + +import java.nio.file.InvalidPathException; +import java.nio.file.Paths; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.RollingFileAppender; + +import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.LogDiagnosticsPort; + +/** + * Log4j2-basierte Implementierung von {@link LogDiagnosticsPort}. + *

      + * Liest den aktiven {@link RollingFileAppender} aus dem laufenden {@link LoggerContext} + * und gibt dessen absoluten Dateipfad zurück. Diese Information zeigt, wo Log-Einträge + * tatsächlich landen – unabhängig vom konfigurierten {@code log.directory}-Wert in der + * Properties-Datei. + *

      + * Gibt einen leeren {@link Optional} zurück, wenn kein dateibasierter Appender aktiv ist, + * der Kontext nicht auslesbar ist oder der Pfad nicht zu einem absoluten Pfad aufgelöst + * werden kann. + */ +public class Log4jLogDiagnosticsAdapter implements LogDiagnosticsPort { + + /** + * Erstellt einen neuen {@code Log4jLogDiagnosticsAdapter}. + */ + public Log4jLogDiagnosticsAdapter() { + // zustandslos + } + + /** + * Ermittelt den absoluten Pfad der aktiven Log-Datei aus dem laufenden Log4j2-Kontext. + *

      + * Durchsucht die Appender-Liste des aktiven {@link LoggerContext} nach dem ersten + * {@link RollingFileAppender} und gibt dessen {@code fileName} als absoluten Pfad zurück. + * Alle Ausnahmen werden abgefangen und als leeres Ergebnis zurückgegeben. + * + * @return absoluter Pfad der aktiven Log-Datei; leer wenn nicht bestimmbar + */ + @Override + public Optional resolveActiveLogFilePath() { + try { + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + return ctx.getConfiguration().getAppenders().values().stream() + .filter(a -> a instanceof RollingFileAppender) + .map(a -> (RollingFileAppender) a) + .map(RollingFileAppender::getFileName) + .filter(name -> name != null && !name.isBlank()) + .map(this::toAbsolutePath) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } catch (Exception e) { + return Optional.empty(); + } + } + + private Optional toAbsolutePath(String path) { + try { + return Optional.of(Paths.get(path).toAbsolutePath().toString()); + } catch (InvalidPathException e) { + return Optional.empty(); + } + } +}