Compare commits
8 Commits
3b3e997d13
...
3.0.239
| Author | SHA1 | Date | |
|---|---|---|---|
| 406eac80e4 | |||
| 4fba3379b9 | |||
| 9307a18e04 | |||
| 6a5ae4e7b0 | |||
| 479d176536 | |||
| bd2be347f6 | |||
| 18f9c33bbb | |||
| 349ee69a7f |
@@ -240,6 +240,13 @@ Bestehende Kommentare mit solchen Bezeichnern, die durch eigene Änderungen ber
|
|||||||
- Keine stillen Änderungen am bestehenden headless Batch-Betrieb
|
- Keine stillen Änderungen am bestehenden headless Batch-Betrieb
|
||||||
- GUI-Code darf den headless Pfad nicht unnötig früh initialisieren
|
- GUI-Code darf den headless Pfad nicht unnötig früh initialisieren
|
||||||
|
|
||||||
|
## Commit und Push nach jeder Implementierung
|
||||||
|
Nach jeder Implementierung oder Dateiänderung wird ein Commit auf `main` erstellt und gepusht:
|
||||||
|
1. Geänderte Dateien stagen und committen
|
||||||
|
2. `git push origin main` ausführen
|
||||||
|
3. Schlägt der Push mit einem AUTH-Fehler fehl: 1 Sekunde warten, dann genau **einen** weiteren Versuch unternehmen
|
||||||
|
4. Schlägt auch der zweite Versuch fehl: Fehler benennen, keinen weiteren automatischen Retry
|
||||||
|
|
||||||
## Definition of Done pro Arbeitspaket
|
## Definition of Done pro Arbeitspaket
|
||||||
Ein Arbeitspaket ist erst fertig, wenn:
|
Ein Arbeitspaket ist erst fertig, wenn:
|
||||||
- der Zielumfang des aktuellen Arbeitspakets vollständig umgesetzt ist
|
- der Zielumfang des aktuellen Arbeitspakets vollständig umgesetzt ist
|
||||||
|
|||||||
+44
-6
@@ -550,13 +550,51 @@ Installationsverzeichnis ab. **Der Betreiber muss diese Beispieldatei manuell na
|
|||||||
Windows-SmartScreen-Warnung, die durch „Weitere Informationen → Trotzdem ausführen"
|
Windows-SmartScreen-Warnung, die durch „Weitere Informationen → Trotzdem ausführen"
|
||||||
bestätigt werden muss. Code-Signing ist für spätere Ausbaustufen vorgesehen.
|
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**,
|
Für den MSI-Betrieb (Startmenü, Task Scheduler) müssen alle Dateipfade als **absolute Pfade**
|
||||||
`prompt.template.file` und `sqlite.file` als **absolute Pfade** zu konfigurieren
|
konfiguriert werden. Relative Pfade werden relativ zum Installationsverzeichnis
|
||||||
oder auf `%APPDATA%`- bzw. `%ProgramData%`-Verzeichnisse zu zeigen.
|
`C:\Program Files\PDF KI Renamer\` aufgelöst, das **schreibgeschützt** ist. Dadurch
|
||||||
Relative Pfade beziehen sich auf das Arbeitsverzeichnis, das je nach Startart variiert
|
schlagen Schreibversuche (Logs, SQLite-Datenbank, Lock-Datei) ohne Fehlermeldung fehl.
|
||||||
(siehe Abschnitt „Prompt-Konfiguration").
|
|
||||||
|
> **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
|
### MSI-Release-Checkliste
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,10 @@
|
|||||||
## Geprüfter Stand
|
## Geprüfter Stand
|
||||||
|
|
||||||
- Git-Branch: `main`
|
- Git-Branch: `main`
|
||||||
- Versionsnummer: `3.0.x`
|
- Versionsnummer: `3.0.238`
|
||||||
*(Konkrete Build-Nummer wird beim Release-Build von Jenkins als Suffix gesetzt –
|
- MSI-Datei: `PDF-KI-Renamer-3.0.238.msi`
|
||||||
Marcus trägt sie hier nach dem finalen Release-Build ein.)*
|
- Freigabedatum: 2026-05-05
|
||||||
- Freigabedatum: 2026-05-03
|
- **Status:** freigegeben
|
||||||
- **Status:** vorläufige Implementierungs-Freigabe;
|
|
||||||
finale Release-Freigabe nach abgeschlossener MSI-Testmatrix und manuellem GUI-Produkttest
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+3
@@ -495,6 +495,7 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
() -> editorState.loadedFileSnapshot()
|
() -> editorState.loadedFileSnapshot()
|
||||||
.map(snapshot -> snapshot.filePath().toString())
|
.map(snapshot -> snapshot.filePath().toString())
|
||||||
.orElse(""),
|
.orElse(""),
|
||||||
|
() -> editorState.values().logDirectory(),
|
||||||
pendingMessages,
|
pendingMessages,
|
||||||
report -> {
|
report -> {
|
||||||
technicalTestsButton.setDisable(false);
|
technicalTestsButton.setDisable(false);
|
||||||
@@ -1389,6 +1390,8 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
boolean shouldDiscard = promptEditorTab.confirmDiscardIfDirty();
|
boolean shouldDiscard = promptEditorTab.confirmDiscardIfDirty();
|
||||||
if (!shouldDiscard) {
|
if (!shouldDiscard) {
|
||||||
Platform.runLater(() -> tabPane.getSelectionModel().select(oldTab));
|
Platform.runLater(() -> tabPane.getSelectionModel().select(oldTab));
|
||||||
|
} else {
|
||||||
|
promptEditorTab.discardChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+19
@@ -154,6 +154,25 @@ public class GuiPromptEditorTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwirft alle ungespeicherten Änderungen und setzt den Tab in den Lade-Bereitschaftszustand.
|
||||||
|
* <p>
|
||||||
|
* Setzt Dirty-State und Tab-Titel zurück. Ist der Tab zum Zeitpunkt des Aufrufs sichtbar,
|
||||||
|
* wird der Prompt-Inhalt sofort neu geladen; andernfalls erfolgt das Laden beim nächsten
|
||||||
|
* Öffnen des Tabs (gesteuert durch den Tab-Selektions-Listener).
|
||||||
|
* <p>
|
||||||
|
* Muss auf dem JavaFX Application Thread aufgerufen werden.
|
||||||
|
*/
|
||||||
|
public void discardChanges() {
|
||||||
|
this.loadedContent = null;
|
||||||
|
this.dirty = false;
|
||||||
|
this.tab.setText(TAB_TITLE);
|
||||||
|
this.saveButton.setDisable(true);
|
||||||
|
if (tab.isSelected()) {
|
||||||
|
loadPromptAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zeigt einen Bestätigungsdialog, wenn ungespeicherte Änderungen vorhanden sind.
|
* Zeigt einen Bestätigungsdialog, wenn ungespeicherte Änderungen vorhanden sind.
|
||||||
* Gibt {@code true} zurück, wenn die Änderungen verworfen werden dürfen.
|
* Gibt {@code true} zurück, wenn die Änderungen verworfen werden dürfen.
|
||||||
|
|||||||
+2
-1
@@ -344,7 +344,8 @@ public record GuiStartupContext(
|
|||||||
TechnicalTestOrchestrator noOpOrchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator noOpOrchestrator = new TechnicalTestOrchestrator(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(),
|
new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(),
|
||||||
noOpPathCheckPort,
|
noOpPathCheckPort,
|
||||||
noOpTestService);
|
noOpTestService,
|
||||||
|
() -> java.util.Optional.empty());
|
||||||
ResourceCreationPort noOpResourceCreationPort = new ResourceCreationPort() {
|
ResourceCreationPort noOpResourceCreationPort = new ResourceCreationPort() {
|
||||||
@Override
|
@Override
|
||||||
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
|
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
|
||||||
|
|||||||
+9
-1
@@ -64,6 +64,7 @@ public final class GuiTechnicalTestCoordinator {
|
|||||||
private final TechnicalTestOrchestrator orchestrator;
|
private final TechnicalTestOrchestrator orchestrator;
|
||||||
private final Supplier<EditorValidationInput> inputProvider;
|
private final Supplier<EditorValidationInput> inputProvider;
|
||||||
private final Supplier<String> configFilePathProvider;
|
private final Supplier<String> configFilePathProvider;
|
||||||
|
private final Supplier<String> logDirectoryProvider;
|
||||||
private final List<GuiMessageEntry> pendingMessages;
|
private final List<GuiMessageEntry> pendingMessages;
|
||||||
private final Consumer<TechnicalTestReport> postResultCallback;
|
private final Consumer<TechnicalTestReport> postResultCallback;
|
||||||
|
|
||||||
@@ -89,6 +90,9 @@ public final class GuiTechnicalTestCoordinator {
|
|||||||
* @param configFilePathProvider Lieferant des aktuell geladenen Konfigurationsdateipfads als String;
|
* @param configFilePathProvider Lieferant des aktuell geladenen Konfigurationsdateipfads als String;
|
||||||
* gibt eine leere Zeichenkette zurück wenn keine Datei geladen ist;
|
* gibt eine leere Zeichenkette zurück wenn keine Datei geladen ist;
|
||||||
* darf nicht {@code null} sein
|
* 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 pendingMessages geteilte veränderliche Nachrichtenliste; darf nicht {@code null} sein
|
||||||
* @param postResultCallback Callback nach erfolgreicher Ergebnisanwendung; 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
|
* @throws NullPointerException wenn einer der Parameter {@code null} ist
|
||||||
@@ -96,11 +100,13 @@ public final class GuiTechnicalTestCoordinator {
|
|||||||
public GuiTechnicalTestCoordinator(TechnicalTestOrchestrator orchestrator,
|
public GuiTechnicalTestCoordinator(TechnicalTestOrchestrator orchestrator,
|
||||||
Supplier<EditorValidationInput> inputProvider,
|
Supplier<EditorValidationInput> inputProvider,
|
||||||
Supplier<String> configFilePathProvider,
|
Supplier<String> configFilePathProvider,
|
||||||
|
Supplier<String> logDirectoryProvider,
|
||||||
List<GuiMessageEntry> pendingMessages,
|
List<GuiMessageEntry> pendingMessages,
|
||||||
Consumer<TechnicalTestReport> postResultCallback) {
|
Consumer<TechnicalTestReport> postResultCallback) {
|
||||||
this.orchestrator = Objects.requireNonNull(orchestrator, "orchestrator must not be null");
|
this.orchestrator = Objects.requireNonNull(orchestrator, "orchestrator must not be null");
|
||||||
this.inputProvider = Objects.requireNonNull(inputProvider, "inputProvider 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.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.pendingMessages = Objects.requireNonNull(pendingMessages, "pendingMessages must not be null");
|
||||||
this.postResultCallback = Objects.requireNonNull(postResultCallback, "postResultCallback must not be null");
|
this.postResultCallback = Objects.requireNonNull(postResultCallback, "postResultCallback must not be null");
|
||||||
this.testThreadFactory = task -> {
|
this.testThreadFactory = task -> {
|
||||||
@@ -134,7 +140,8 @@ public final class GuiTechnicalTestCoordinator {
|
|||||||
pendingMessages.clear();
|
pendingMessages.clear();
|
||||||
EditorValidationInput input = inputProvider.get();
|
EditorValidationInput input = inputProvider.get();
|
||||||
String configFilePath = configFilePathProvider.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.");
|
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 SOURCE_FOLDER_PRESENT -> "Quellordner vorhanden und lesbar";
|
||||||
case TARGET_FOLDER_USABLE -> "Zielordner vorhanden oder anlegbar sowie schreibbar";
|
case TARGET_FOLDER_USABLE -> "Zielordner vorhanden oder anlegbar sowie schreibbar";
|
||||||
case SQLITE_PATH_USABLE -> "SQLite-Pfad technisch nutzbar";
|
case SQLITE_PATH_USABLE -> "SQLite-Pfad technisch nutzbar";
|
||||||
|
case LOG_DIRECTORY_USABLE -> "Log-Verzeichnis beschreibbar";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -419,7 +419,8 @@ class GuiAdapterSmokeTest {
|
|||||||
},
|
},
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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"); }
|
||||||
|
|||||||
+2
-1
@@ -345,7 +345,8 @@ class GuiEditorFieldBindingTest {
|
|||||||
},
|
},
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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"); }
|
||||||
|
|||||||
+6
-3
@@ -137,7 +137,8 @@ class GuiEditorIntegrationTest {
|
|||||||
},
|
},
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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"); }
|
||||||
|
|||||||
+10
-5
@@ -208,7 +208,8 @@ class GuiEditorRegressionSmokeTest {
|
|||||||
},
|
},
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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"); }
|
||||||
|
|||||||
+4
-2
@@ -142,7 +142,8 @@ class GuiEditorValidationSmokeTest {
|
|||||||
},
|
},
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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"); }
|
||||||
|
|||||||
+8
-4
@@ -336,7 +336,8 @@ class GuiMessageAreaSmokeTest {
|
|||||||
},
|
},
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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"); }
|
||||||
|
|||||||
+2
-1
@@ -529,7 +529,8 @@ class GuiModelCatalogSmokeTest {
|
|||||||
},
|
},
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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"); }
|
||||||
|
|||||||
+40
@@ -328,4 +328,44 @@ class GuiPromptEditorTabSmokeTest {
|
|||||||
"Tab-Titel muss nach Bearbeitung (resetToDefault) einen Asterisk enthalten; Titel war: "
|
"Tab-Titel muss nach Bearbeitung (resetToDefault) einen Asterisk enthalten; Titel war: "
|
||||||
+ titleRef.get());
|
+ titleRef.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void discardChanges_shouldResetDirtyStateAndTitle() throws Exception {
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
AtomicReference<Throwable> fxError = new AtomicReference<>();
|
||||||
|
AtomicBoolean dirtyRef = new AtomicBoolean(true);
|
||||||
|
AtomicReference<String> titleRef = new AtomicReference<>();
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
try {
|
||||||
|
SyncPromptEditorPort port = new SyncPromptEditorPort();
|
||||||
|
GuiPromptEditorTab editorTab = buildSyncTab(port);
|
||||||
|
editorTab.loadPromptAsync();
|
||||||
|
editorTab.resetToDefault();
|
||||||
|
// Vorbedingung: Dirty-State muss aktiv sein
|
||||||
|
assertTrue(editorTab.hasDirtyContent(),
|
||||||
|
"Vorbedingung: Dirty-State muss nach resetToDefault aktiv sein");
|
||||||
|
|
||||||
|
// Verwerfen simulieren
|
||||||
|
editorTab.discardChanges();
|
||||||
|
|
||||||
|
dirtyRef.set(editorTab.hasDirtyContent());
|
||||||
|
titleRef.set(editorTab.tab().getText());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
fxError.set(t);
|
||||||
|
} finally {
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS));
|
||||||
|
if (fxError.get() != null) {
|
||||||
|
throw new AssertionError("FX-Thread hat eine Ausnahme geworfen", fxError.get());
|
||||||
|
}
|
||||||
|
assertFalse(dirtyRef.get(),
|
||||||
|
"Dirty-State muss nach discardChanges false sein");
|
||||||
|
assertFalse(titleRef.get().contains("*"),
|
||||||
|
"Tab-Titel darf nach discardChanges keinen Asterisk enthalten; Titel war: "
|
||||||
|
+ titleRef.get());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-8
@@ -39,7 +39,7 @@ import javafx.scene.control.Button;
|
|||||||
* {@code technical-tests-button}.</li>
|
* {@code technical-tests-button}.</li>
|
||||||
* <li>Triggering the coordinator synchronously populates {@code pendingMessages}
|
* <li>Triggering the coordinator synchronously populates {@code pendingMessages}
|
||||||
* with entries tagged {@link GuiTechnicalTestCoordinator#SOURCE_TAG}.</li>
|
* with entries tagged {@link GuiTechnicalTestCoordinator#SOURCE_TAG}.</li>
|
||||||
* <li>A second trigger appends a fresh batch of test entries (accumulation semantics).</li>
|
* <li>A second trigger replaces the previous batch of test entries.</li>
|
||||||
* <li>The post-result callback is invoked after the result is applied.</li>
|
* <li>The post-result callback is invoked after the result is applied.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* <p>
|
* <p>
|
||||||
@@ -138,12 +138,12 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Smoke test: after one trigger, the number of entries tagged SOURCE_TAG equals
|
* 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
|
* @throws Exception if the FX thread task fails or times out
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void trigger_producesElevenCheckpointEntriesPlusSummary() throws Exception {
|
void trigger_producesTwelveCheckpointEntriesPlusSummary() throws Exception {
|
||||||
runOnFx(() -> {
|
runOnFx(() -> {
|
||||||
List<GuiMessageEntry> messages = new ArrayList<>();
|
List<GuiMessageEntry> messages = new ArrayList<>();
|
||||||
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });
|
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });
|
||||||
@@ -155,9 +155,9 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
|||||||
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
|
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
// 11 checkpoint entries + 1 summary entry = 12
|
// 12 checkpoint entries + 1 summary entry = 13
|
||||||
assertEquals(12, taggedCount,
|
assertEquals(13, taggedCount,
|
||||||
"Expected 11 checkpoint entries + 1 summary entry = 12 tagged messages");
|
"Expected 12 checkpoint entries + 1 summary entry = 13 tagged messages");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,12 +256,14 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
noOpPathCheckPort(),
|
noOpPathCheckPort(),
|
||||||
noOpProviderService());
|
noOpProviderService(),
|
||||||
|
() -> java.util.Optional.empty());
|
||||||
|
|
||||||
GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator(
|
GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator(
|
||||||
orchestrator,
|
orchestrator,
|
||||||
currentInput::get, // always reads the current reference
|
currentInput::get, // always reads the current reference
|
||||||
() -> "",
|
() -> "",
|
||||||
|
() -> "",
|
||||||
messages,
|
messages,
|
||||||
report -> { });
|
report -> { });
|
||||||
|
|
||||||
@@ -365,7 +367,8 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
noOpPathCheckPort(),
|
noOpPathCheckPort(),
|
||||||
noOpProviderService());
|
noOpProviderService(),
|
||||||
|
() -> java.util.Optional.empty());
|
||||||
|
|
||||||
EditorValidationInput blankInput = new EditorValidationInput(
|
EditorValidationInput blankInput = new EditorValidationInput(
|
||||||
"claude",
|
"claude",
|
||||||
@@ -380,6 +383,7 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
|||||||
orchestrator,
|
orchestrator,
|
||||||
() -> blankInput,
|
() -> blankInput,
|
||||||
() -> "",
|
() -> "",
|
||||||
|
() -> "",
|
||||||
messages,
|
messages,
|
||||||
postResultCallback);
|
postResultCallback);
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -806,7 +806,8 @@ class GuiUnsavedChangesGuardSmokeTest {
|
|||||||
},
|
},
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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"); }
|
||||||
|
|||||||
+4
-2
@@ -323,7 +323,8 @@ class GuiValidateActionSmokeTest {
|
|||||||
},
|
},
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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"); }
|
@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"); }
|
||||||
|
|||||||
+15
-5
@@ -6,6 +6,7 @@ import de.gecheckt.pdf.umbenenner.domain.model.AiTechnicalFailure;
|
|||||||
import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
|
import de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome;
|
||||||
import de.gecheckt.pdf.umbenenner.domain.model.NamingProposalReady;
|
import de.gecheckt.pdf.umbenenner.domain.model.NamingProposalReady;
|
||||||
import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed;
|
import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailed;
|
||||||
|
import de.gecheckt.pdf.umbenenner.domain.model.PreCheckFailureReason;
|
||||||
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
||||||
import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;
|
import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;
|
||||||
|
|
||||||
@@ -26,10 +27,14 @@ import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;
|
|||||||
* <li><strong>Naming proposal ready:</strong> Status becomes
|
* <li><strong>Naming proposal ready:</strong> Status becomes
|
||||||
* {@link ProcessingStatus#PROPOSAL_READY}, counters unchanged,
|
* {@link ProcessingStatus#PROPOSAL_READY}, counters unchanged,
|
||||||
* {@code retryable=false}.</li>
|
* {@code retryable=false}.</li>
|
||||||
* <li><strong>Pre-check content error (first occurrence):</strong>
|
* <li><strong>Pre-check content error {@link PreCheckFailureReason#NO_USABLE_TEXT}:</strong>
|
||||||
|
* Status becomes {@link ProcessingStatus#FAILED_FINAL} immediately,
|
||||||
|
* content error counter incremented by 1, {@code retryable=false}.
|
||||||
|
* Image-only PDFs without OCR text will not yield usable text on retry.</li>
|
||||||
|
* <li><strong>Pre-check content error (other reason, first occurrence):</strong>
|
||||||
* Status becomes {@link ProcessingStatus#FAILED_RETRYABLE},
|
* Status becomes {@link ProcessingStatus#FAILED_RETRYABLE},
|
||||||
* content error counter incremented by 1, {@code retryable=true}.</li>
|
* content error counter incremented by 1, {@code retryable=true}.</li>
|
||||||
* <li><strong>Pre-check content error (second or later occurrence):</strong>
|
* <li><strong>Pre-check content error (other reason, second or later occurrence):</strong>
|
||||||
* Status becomes {@link ProcessingStatus#FAILED_FINAL},
|
* Status becomes {@link ProcessingStatus#FAILED_FINAL},
|
||||||
* content error counter incremented by 1, {@code retryable=false}.</li>
|
* content error counter incremented by 1, {@code retryable=false}.</li>
|
||||||
* <li><strong>AI functional failure (first occurrence):</strong>
|
* <li><strong>AI functional failure (first occurrence):</strong>
|
||||||
@@ -112,11 +117,16 @@ final class ProcessingOutcomeTransition {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case PreCheckFailed ignored2 -> {
|
case PreCheckFailed preCheckFailed -> {
|
||||||
// Deterministic content error from pre-check: apply the 1-retry rule
|
|
||||||
FailureCounters updatedCounters = existingCounters.withIncrementedContentErrorCount();
|
FailureCounters updatedCounters = existingCounters.withIncrementedContentErrorCount();
|
||||||
boolean isFirstOccurrence = existingCounters.contentErrorCount() == 0;
|
|
||||||
|
|
||||||
|
if (preCheckFailed.failureReason() == PreCheckFailureReason.NO_USABLE_TEXT) {
|
||||||
|
// Image-only PDFs without OCR text will not change on retry.
|
||||||
|
yield new ProcessingOutcome(ProcessingStatus.FAILED_FINAL, updatedCounters, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other deterministic content errors: apply the 1-retry rule
|
||||||
|
boolean isFirstOccurrence = existingCounters.contentErrorCount() == 0;
|
||||||
if (isFirstOccurrence) {
|
if (isFirstOccurrence) {
|
||||||
yield new ProcessingOutcome(ProcessingStatus.FAILED_RETRYABLE, updatedCounters, true);
|
yield new ProcessingOutcome(ProcessingStatus.FAILED_RETRYABLE, updatedCounters, true);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+9
-1
@@ -84,5 +84,13 @@ public enum CheckpointId {
|
|||||||
* zeigt auf eine vorhandene Datei oder auf einen beschreibbaren Ordner, in dem die
|
* zeigt auf eine vorhandene Datei oder auf einen beschreibbaren Ordner, in dem die
|
||||||
* Datei neu angelegt werden kann.
|
* 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
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.application.validation.technicaltest;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ausgehender Port zur Diagnose des aktiven Log-Ausgabepfads.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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<String> resolveActiveLogFilePath();
|
||||||
|
}
|
||||||
+88
-17
@@ -17,16 +17,18 @@ import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidation
|
|||||||
/**
|
/**
|
||||||
* Orchestrator für den vollständigen technischen Gesamttest der GUI-Konfiguration.
|
* Orchestrator für den vollständigen technischen Gesamttest der GUI-Konfiguration.
|
||||||
* <p>
|
* <p>
|
||||||
* 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:
|
||||||
* <ol>
|
* <ol>
|
||||||
* <li><strong>Lokale Validierung:</strong> Prüft den Editorzustand ohne I/O mithilfe des
|
* <li><strong>Lokale Validierung:</strong> Prüft den Editorzustand ohne I/O mithilfe des
|
||||||
* {@link EditorConfigurationValidator}. Erzeugt Ergebnisse für
|
* {@link EditorConfigurationValidator}. Erzeugt Ergebnisse für
|
||||||
* {@link CheckpointId#CONFIGURATION_BASIC_VALIDATION} und
|
* {@link CheckpointId#CONFIGURATION_BASIC_VALIDATION} und
|
||||||
* {@link CheckpointId#PROVIDER_CONFIGURATION}.</li>
|
* {@link CheckpointId#PROVIDER_CONFIGURATION}.</li>
|
||||||
* <li><strong>Pfadprüfungen:</strong> Prüft Quellordner, Zielordner, Prompt-Datei und
|
* <li><strong>Pfadprüfungen:</strong> Prüft Quellordner, Zielordner, Prompt-Datei,
|
||||||
* SQLite-Pfad über den {@link PathCheckPort}. Erzeugt Ergebnisse für
|
* 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#PROMPT_FILE_PRESENT}, {@link CheckpointId#SOURCE_FOLDER_PRESENT},
|
||||||
* {@link CheckpointId#TARGET_FOLDER_USABLE} und {@link CheckpointId#SQLITE_PATH_USABLE}.</li>
|
* {@link CheckpointId#TARGET_FOLDER_USABLE}, {@link CheckpointId#SQLITE_PATH_USABLE}
|
||||||
|
* und {@link CheckpointId#LOG_DIRECTORY_USABLE}.</li>
|
||||||
* <li><strong>Provider-Prüfungen:</strong> Prüft Endpoint, API-Key, Modellliste und
|
* <li><strong>Provider-Prüfungen:</strong> Prüft Endpoint, API-Key, Modellliste und
|
||||||
* Modellplausibilität über den {@link ProviderTechnicalTestService}. Erzeugt Ergebnisse für
|
* Modellplausibilität über den {@link ProviderTechnicalTestService}. Erzeugt Ergebnisse für
|
||||||
* {@link CheckpointId#BASE_URL_REACHABLE}, {@link CheckpointId#API_KEY_PRESENT},
|
* {@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
|
* ausgeführt, auch wenn ein Block eine Exception wirft. In diesem Fall werden die
|
||||||
* betroffenen Checkpoints als {@link CheckpointResult.Failure} mit Schweregrad ERROR
|
* betroffenen Checkpoints als {@link CheckpointResult.Failure} mit Schweregrad ERROR
|
||||||
* und dem Präfix „Interner Fehler:" markiert. Der Gesamtbericht enthält immer genau
|
* und dem Präfix „Interner Fehler:" markiert. Der Gesamtbericht enthält immer genau
|
||||||
* elf Einträge.
|
* zwölf Einträge.
|
||||||
* <p>
|
* <p>
|
||||||
* <strong>Threading-Kontrakt:</strong> Die Methode {@link #run(TechnicalTestRequest)}
|
* <strong>Threading-Kontrakt:</strong> Die Methode {@link #run(TechnicalTestRequest)}
|
||||||
* ist synchron blockierend (der Provider-Prüfblock führt HTTP-Aufrufe durch). Sie darf
|
* 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.
|
* wird {@code config/prompt.txt} relativ zum Arbeitsverzeichnis verwendet.
|
||||||
* <p>
|
* <p>
|
||||||
* Dieser Service enthält keine JavaFX-Typen, keine NIO-Pfadobjekte in Signaturen und
|
* 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 {
|
public class TechnicalTestOrchestrator {
|
||||||
|
|
||||||
private final EditorConfigurationValidator editorValidator;
|
private final EditorConfigurationValidator editorValidator;
|
||||||
private final PathCheckPort pathCheckPort;
|
private final PathCheckPort pathCheckPort;
|
||||||
private final ProviderTechnicalTestService providerTestService;
|
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 editorValidator Lokaler Konfigurationsvalidator; darf nicht {@code null} sein
|
||||||
* @param pathCheckPort Port für Dateisystem-Pfadprüfungen; 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 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
|
* @throws NullPointerException wenn einer der Parameter {@code null} ist
|
||||||
*/
|
*/
|
||||||
public TechnicalTestOrchestrator(EditorConfigurationValidator editorValidator,
|
public TechnicalTestOrchestrator(EditorConfigurationValidator editorValidator,
|
||||||
PathCheckPort pathCheckPort,
|
PathCheckPort pathCheckPort,
|
||||||
ProviderTechnicalTestService providerTestService) {
|
ProviderTechnicalTestService providerTestService,
|
||||||
|
LogDiagnosticsPort logDiagnosticsPort) {
|
||||||
this.editorValidator = Objects.requireNonNull(editorValidator, "editorValidator must not be null");
|
this.editorValidator = Objects.requireNonNull(editorValidator, "editorValidator must not be null");
|
||||||
this.pathCheckPort = Objects.requireNonNull(pathCheckPort, "pathCheckPort 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.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 {
|
|||||||
* <p>
|
* <p>
|
||||||
* Alle drei Prüfblöcke werden immer vollständig ausgeführt. Ein Fehler in einem Block
|
* 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
|
* 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.
|
||||||
* <p>
|
* <p>
|
||||||
* <strong>Prompt-Datei-Standardpfad:</strong> Wenn der Editorzustand keinen Prompt-Pfad
|
* <strong>Prompt-Datei-Standardpfad:</strong> Wenn der Editorzustand keinen Prompt-Pfad
|
||||||
* enthält, wird als Standardpfad der Elternordner der Konfigurationsdatei gewählt
|
* 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.
|
* abgeschlossen sind. Sie darf nicht auf dem JavaFX Application Thread aufgerufen werden.
|
||||||
*
|
*
|
||||||
* @param request Eingabedaten für den Gesamttest; darf nicht {@code null} sein
|
* @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
|
* @throws NullPointerException wenn {@code request} {@code null} ist
|
||||||
*/
|
*/
|
||||||
public TechnicalTestReport run(TechnicalTestRequest request) {
|
public TechnicalTestReport run(TechnicalTestRequest request) {
|
||||||
@@ -104,13 +110,13 @@ public class TechnicalTestOrchestrator {
|
|||||||
Instant startTime = Instant.now();
|
Instant startTime = Instant.now();
|
||||||
EditorValidationInput input = request.validationInput();
|
EditorValidationInput input = request.validationInput();
|
||||||
|
|
||||||
List<CheckpointResult> results = new ArrayList<>(11);
|
List<CheckpointResult> results = new ArrayList<>(12);
|
||||||
|
|
||||||
// Block 1: Lokale Konfigurationsvalidierung (kein I/O)
|
// Block 1: Lokale Konfigurationsvalidierung (kein I/O)
|
||||||
results.addAll(runLocalValidationBlock(input));
|
results.addAll(runLocalValidationBlock(input));
|
||||||
|
|
||||||
// Block 2: Pfadprüfungen (Dateisystem-I/O)
|
// 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)
|
// Block 3: Provider-nahe technische Prüfungen (Netzwerk-I/O)
|
||||||
results.addAll(runProviderCheckBlock(input));
|
results.addAll(runProviderCheckBlock(input));
|
||||||
@@ -222,25 +228,30 @@ public class TechnicalTestOrchestrator {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Führt die Dateisystem-Pfadprüfungen für Prompt-Datei, Quellordner, Zielordner
|
* Führt die Dateisystem-Pfadprüfungen für Prompt-Datei, Quellordner, Zielordner,
|
||||||
* und SQLite-Pfad durch.
|
* SQLite-Pfad und Log-Verzeichnis durch.
|
||||||
* <p>
|
* <p>
|
||||||
* Der {@code configFilePath} wird genutzt, um bei fehlendem Prompt-Pfad im Editorzustand
|
* Der {@code configFilePath} wird genutzt, um bei fehlendem Prompt-Pfad im Editorzustand
|
||||||
* einen sinnvollen Standardpfad zu bestimmen ({@code <config-parent>/prompt.txt}).
|
* einen sinnvollen Standardpfad zu bestimmen ({@code <config-parent>/prompt.txt}).
|
||||||
|
* Der {@code logDirectory} ist der konfigurierte Rohwert von {@code log.directory};
|
||||||
|
* leer bedeutet Standardwert {@code ./logs/}.
|
||||||
*
|
*
|
||||||
* @param input aktueller Editorzustand
|
* @param input aktueller Editorzustand
|
||||||
* @param configFilePath Pfad der geladenen Konfigurationsdatei; leer wenn keine geladen
|
* @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<CheckpointResult> runPathCheckBlock(EditorValidationInput input,
|
private List<CheckpointResult> runPathCheckBlock(EditorValidationInput input,
|
||||||
String configFilePath) {
|
String configFilePath,
|
||||||
|
String logDirectory) {
|
||||||
try {
|
try {
|
||||||
List<CheckpointResult> results = new ArrayList<>(4);
|
List<CheckpointResult> results = new ArrayList<>(5);
|
||||||
results.add(checkPromptFile(input.promptTemplateFile(), configFilePath,
|
results.add(checkPromptFile(input.promptTemplateFile(), configFilePath,
|
||||||
resolveMaxTitleLengthForPromptCreation(input.maxTitleLength())));
|
resolveMaxTitleLengthForPromptCreation(input.maxTitleLength())));
|
||||||
results.add(checkSourceFolder(input.sourceFolder()));
|
results.add(checkSourceFolder(input.sourceFolder()));
|
||||||
results.add(checkTargetFolder(input.targetFolder()));
|
results.add(checkTargetFolder(input.targetFolder()));
|
||||||
results.add(checkSqlitePath(input.sqliteFile()));
|
results.add(checkSqlitePath(input.sqliteFile()));
|
||||||
|
results.add(checkLogDirectory(logDirectory));
|
||||||
return results;
|
return results;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
String errorMsg = "Interner Fehler bei den Pfadprüfungen: " + e.getMessage();
|
String errorMsg = "Interner Fehler bei den Pfadprüfungen: " + e.getMessage();
|
||||||
@@ -252,6 +263,8 @@ public class TechnicalTestOrchestrator {
|
|||||||
CheckpointResult.Failure.of(CheckpointId.TARGET_FOLDER_USABLE,
|
CheckpointResult.Failure.of(CheckpointId.TARGET_FOLDER_USABLE,
|
||||||
CheckpointSeverity.ERROR, errorMsg),
|
CheckpointSeverity.ERROR, errorMsg),
|
||||||
CheckpointResult.Failure.of(CheckpointId.SQLITE_PATH_USABLE,
|
CheckpointResult.Failure.of(CheckpointId.SQLITE_PATH_USABLE,
|
||||||
|
CheckpointSeverity.ERROR, errorMsg),
|
||||||
|
CheckpointResult.Failure.of(CheckpointId.LOG_DIRECTORY_USABLE,
|
||||||
CheckpointSeverity.ERROR, errorMsg)
|
CheckpointSeverity.ERROR, errorMsg)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -471,6 +484,64 @@ public class TechnicalTestOrchestrator {
|
|||||||
suggestion);
|
suggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft das Log-Verzeichnis auf Schreibbarkeit und zeigt den tatsächlichen
|
||||||
|
* Log-Dateipfad aus der aktiven Log4j2-Konfiguration an.
|
||||||
|
* <p>
|
||||||
|
* <strong>Verzeichnis-Ermittlung:</strong> 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.
|
||||||
|
* <p>
|
||||||
|
* <strong>Ergebnis:</strong>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@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.</li>
|
||||||
|
* <li>{@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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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<String> 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
|
// Block 3: Provider-nahe technische Prüfungen
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
+11
-3
@@ -15,36 +15,44 @@ import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidation
|
|||||||
* Gesamttest, bei der automatischen Prompt-Erzeugung den Standardpfad relativ zur
|
* Gesamttest, bei der automatischen Prompt-Erzeugung den Standardpfad relativ zur
|
||||||
* Konfigurationsdatei zu bestimmen. Er ist leer, wenn keine Konfigurationsdatei geladen ist.
|
* Konfigurationsdatei zu bestimmen. Er ist leer, wenn keine Konfigurationsdatei geladen ist.
|
||||||
* <p>
|
* <p>
|
||||||
|
* Das {@code logDirectory}-Feld trägt den konfigurierten Rohwert von {@code log.directory}
|
||||||
|
* aus dem Editor; leer bedeutet Standardwert {@code ./logs/}.
|
||||||
|
* <p>
|
||||||
* Dieser Record enthält keine JavaFX-Typen und keine Infrastrukturabhängigkeiten.
|
* Dieser Record enthält keine JavaFX-Typen und keine Infrastrukturabhängigkeiten.
|
||||||
*
|
*
|
||||||
* @param validationInput aktueller Editorzustand; nie {@code null}
|
* @param validationInput aktueller Editorzustand; nie {@code null}
|
||||||
* @param configFilePath optionaler Pfad der geladenen Konfigurationsdatei als String;
|
* @param configFilePath optionaler Pfad der geladenen Konfigurationsdatei als String;
|
||||||
* leer wenn keine Datei geladen ist
|
* 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(
|
public record TechnicalTestRequest(
|
||||||
EditorValidationInput validationInput,
|
EditorValidationInput validationInput,
|
||||||
String configFilePath) {
|
String configFilePath,
|
||||||
|
String logDirectory) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt eine neue Gesamttest-Anforderung.
|
* Erstellt eine neue Gesamttest-Anforderung.
|
||||||
*
|
*
|
||||||
* @param validationInput aktueller Editorzustand; darf nicht {@code null} sein
|
* @param validationInput aktueller Editorzustand; darf nicht {@code null} sein
|
||||||
* @param configFilePath Pfad der Konfigurationsdatei; {@code null} wird zu leerem String
|
* @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
|
* @throws NullPointerException wenn {@code validationInput} {@code null} ist
|
||||||
*/
|
*/
|
||||||
public TechnicalTestRequest {
|
public TechnicalTestRequest {
|
||||||
Objects.requireNonNull(validationInput, "validationInput must not be null");
|
Objects.requireNonNull(validationInput, "validationInput must not be null");
|
||||||
configFilePath = configFilePath == null ? "" : configFilePath;
|
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
|
* @param validationInput aktueller Editorzustand; darf nicht {@code null} sein
|
||||||
* @return eine neue Anforderung ohne Konfigurationsdateipfad
|
* @return eine neue Anforderung ohne Konfigurationsdateipfad
|
||||||
*/
|
*/
|
||||||
public static TechnicalTestRequest of(EditorValidationInput validationInput) {
|
public static TechnicalTestRequest of(EditorValidationInput validationInput) {
|
||||||
return new TechnicalTestRequest(validationInput, "");
|
return new TechnicalTestRequest(validationInput, "", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+62
-7
@@ -154,13 +154,36 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void process_newDocument_firstContentError_persistsFailedRetryable_contentCounterOne() {
|
void process_newDocument_noUsableText_persistsFailedFinal_contentCounterOne() {
|
||||||
|
// NO_USABLE_TEXT (image-only PDF) finalises immediately — no retry.
|
||||||
recordRepo.setLookupResult(new DocumentUnknown());
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
DocumentProcessingOutcome outcome = new PreCheckFailed(
|
DocumentProcessingOutcome outcome = new PreCheckFailed(
|
||||||
candidate, PreCheckFailureReason.NO_USABLE_TEXT);
|
candidate, PreCheckFailureReason.NO_USABLE_TEXT);
|
||||||
|
|
||||||
processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
|
assertEquals(1, attemptRepo.savedAttempts.size());
|
||||||
|
ProcessingAttempt attempt = attemptRepo.savedAttempts.get(0);
|
||||||
|
assertEquals(ProcessingStatus.FAILED_FINAL, attempt.status());
|
||||||
|
assertFalse(attempt.retryable());
|
||||||
|
|
||||||
|
assertEquals(1, recordRepo.createdRecords.size());
|
||||||
|
DocumentRecord record = recordRepo.createdRecords.get(0);
|
||||||
|
assertEquals(ProcessingStatus.FAILED_FINAL, record.overallStatus());
|
||||||
|
assertEquals(1, record.failureCounters().contentErrorCount());
|
||||||
|
assertEquals(0, record.failureCounters().transientErrorCount());
|
||||||
|
assertNotNull(record.lastFailureInstant());
|
||||||
|
assertNull(record.lastSuccessInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_newDocument_firstPageLimitExceeded_persistsFailedRetryable_contentCounterOne() {
|
||||||
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
DocumentProcessingOutcome outcome = new PreCheckFailed(
|
||||||
|
candidate, PreCheckFailureReason.PAGE_LIMIT_EXCEEDED);
|
||||||
|
|
||||||
|
processor.process(candidate, fingerprint, outcome, context, attemptStart);
|
||||||
|
|
||||||
assertEquals(1, attemptRepo.savedAttempts.size());
|
assertEquals(1, attemptRepo.savedAttempts.size());
|
||||||
ProcessingAttempt attempt = attemptRepo.savedAttempts.get(0);
|
ProcessingAttempt attempt = attemptRepo.savedAttempts.get(0);
|
||||||
assertEquals(ProcessingStatus.FAILED_RETRYABLE, attempt.status());
|
assertEquals(ProcessingStatus.FAILED_RETRYABLE, attempt.status());
|
||||||
@@ -1191,17 +1214,18 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void process_contentErrorLifecycle_firstRunRetryable_secondRunFinal_thirdRunSkipped() {
|
void process_contentErrorLifecycle_pageLimitExceeded_firstRunRetryable_secondRunFinal_thirdRunSkipped() {
|
||||||
// Run 1: new document, first deterministic content error → FAILED_RETRYABLE
|
// PAGE_LIMIT_EXCEEDED follows the 1-retry rule: first run → FAILED_RETRYABLE, second → FAILED_FINAL.
|
||||||
recordRepo.setLookupResult(new DocumentUnknown());
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
DocumentProcessingOutcome contentError = new PreCheckFailed(
|
DocumentProcessingOutcome contentError = new PreCheckFailed(
|
||||||
candidate, PreCheckFailureReason.NO_USABLE_TEXT);
|
candidate, PreCheckFailureReason.PAGE_LIMIT_EXCEEDED);
|
||||||
|
|
||||||
|
// Run 1: new document, first content error → FAILED_RETRYABLE
|
||||||
processor.process(candidate, fingerprint, contentError, context, attemptStart);
|
processor.process(candidate, fingerprint, contentError, context, attemptStart);
|
||||||
|
|
||||||
DocumentRecord afterRun1 = recordRepo.createdRecords.get(0);
|
DocumentRecord afterRun1 = recordRepo.createdRecords.get(0);
|
||||||
assertEquals(ProcessingStatus.FAILED_RETRYABLE, afterRun1.overallStatus(),
|
assertEquals(ProcessingStatus.FAILED_RETRYABLE, afterRun1.overallStatus(),
|
||||||
"First content error must yield FAILED_RETRYABLE");
|
"First PAGE_LIMIT_EXCEEDED must yield FAILED_RETRYABLE");
|
||||||
assertEquals(1, afterRun1.failureCounters().contentErrorCount());
|
assertEquals(1, afterRun1.failureCounters().contentErrorCount());
|
||||||
assertTrue(attemptRepo.savedAttempts.get(0).retryable(),
|
assertTrue(attemptRepo.savedAttempts.get(0).retryable(),
|
||||||
"First content error attempt must be retryable");
|
"First content error attempt must be retryable");
|
||||||
@@ -1236,6 +1260,36 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
"Transient error counter must remain 0 after a SKIPPED_FINAL_FAILURE event");
|
"Transient error counter must remain 0 after a SKIPPED_FINAL_FAILURE event");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void process_contentErrorLifecycle_noUsableText_immediatelyFinal_secondRunSkipped() {
|
||||||
|
// NO_USABLE_TEXT (image-only PDF): first run is immediately FAILED_FINAL, second is skipped.
|
||||||
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
DocumentProcessingOutcome noTextError = new PreCheckFailed(
|
||||||
|
candidate, PreCheckFailureReason.NO_USABLE_TEXT);
|
||||||
|
|
||||||
|
// Run 1: new document → FAILED_FINAL immediately
|
||||||
|
processor.process(candidate, fingerprint, noTextError, context, attemptStart);
|
||||||
|
|
||||||
|
DocumentRecord afterRun1 = recordRepo.createdRecords.get(0);
|
||||||
|
assertEquals(ProcessingStatus.FAILED_FINAL, afterRun1.overallStatus(),
|
||||||
|
"NO_USABLE_TEXT must yield FAILED_FINAL immediately");
|
||||||
|
assertEquals(1, afterRun1.failureCounters().contentErrorCount());
|
||||||
|
assertFalse(attemptRepo.savedAttempts.get(0).retryable());
|
||||||
|
|
||||||
|
// Run 2: terminal FAILED_FINAL → SKIPPED_FINAL_FAILURE; counters must not change
|
||||||
|
recordRepo.setLookupResult(new DocumentTerminalFinalFailure(afterRun1));
|
||||||
|
|
||||||
|
processor.process(candidate, fingerprint, noTextError, context, attemptStart);
|
||||||
|
|
||||||
|
assertEquals(2, attemptRepo.savedAttempts.size());
|
||||||
|
ProcessingAttempt skipAttempt = attemptRepo.savedAttempts.get(1);
|
||||||
|
assertEquals(ProcessingStatus.SKIPPED_FINAL_FAILURE, skipAttempt.status());
|
||||||
|
|
||||||
|
DocumentRecord afterRun2 = recordRepo.updatedRecords.get(0);
|
||||||
|
assertEquals(1, afterRun2.failureCounters().contentErrorCount(),
|
||||||
|
"Content error counter must remain 1 after SKIPPED_FINAL_FAILURE");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void process_transientErrorLifecycle_maxRetriesTransient2_firstRetryable_secondFinal() {
|
void process_transientErrorLifecycle_maxRetriesTransient2_firstRetryable_secondFinal() {
|
||||||
// maxRetriesTransient=2: first transient error → FAILED_RETRYABLE, second → FAILED_FINAL
|
// maxRetriesTransient=2: first transient error → FAILED_RETRYABLE, second → FAILED_FINAL
|
||||||
@@ -1594,8 +1648,9 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void process_firstContentError_retryDecisionLog_containsFingerprintAndFailedRetryable() {
|
void process_firstContentError_retryDecisionLog_containsFingerprintAndFailedRetryable() {
|
||||||
// Proves that the retry decision log for a first deterministic content error contains
|
// Proves that the retry decision log for a first retryable content error contains
|
||||||
// both the document fingerprint and the FAILED_RETRYABLE classification.
|
// both the document fingerprint and the FAILED_RETRYABLE classification.
|
||||||
|
// Uses PAGE_LIMIT_EXCEEDED which follows the 1-retry rule.
|
||||||
MessageCapturingProcessingLogger capturingLogger = new MessageCapturingProcessingLogger();
|
MessageCapturingProcessingLogger capturingLogger = new MessageCapturingProcessingLogger();
|
||||||
DocumentProcessingCoordinator coordinatorWithCapturing =
|
DocumentProcessingCoordinator coordinatorWithCapturing =
|
||||||
new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort,
|
new DocumentProcessingCoordinator(recordRepo, attemptRepo, unitOfWorkPort,
|
||||||
@@ -1604,7 +1659,7 @@ class DocumentProcessingCoordinatorTest {
|
|||||||
recordRepo.setLookupResult(new DocumentUnknown());
|
recordRepo.setLookupResult(new DocumentUnknown());
|
||||||
|
|
||||||
coordinatorWithCapturing.process(candidate, fingerprint,
|
coordinatorWithCapturing.process(candidate, fingerprint,
|
||||||
new PreCheckFailed(candidate, PreCheckFailureReason.NO_USABLE_TEXT),
|
new PreCheckFailed(candidate, PreCheckFailureReason.PAGE_LIMIT_EXCEEDED),
|
||||||
context, attemptStart);
|
context, attemptStart);
|
||||||
|
|
||||||
assertTrue(capturingLogger.anyWarnContains(FINGERPRINT_HEX),
|
assertTrue(capturingLogger.anyWarnContains(FINGERPRINT_HEX),
|
||||||
|
|||||||
+20
-4
@@ -103,13 +103,28 @@ class ProcessingOutcomeTransitionTest {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void forNewDocument_firstPreCheckFailed_returnsFailedRetryable_contentCounterOne() {
|
void forNewDocument_noUsableText_immediatelyFailedFinal_noRetry() {
|
||||||
PreCheckFailed outcome = new PreCheckFailed(candidate(), PreCheckFailureReason.NO_USABLE_TEXT);
|
PreCheckFailed outcome = new PreCheckFailed(candidate(), PreCheckFailureReason.NO_USABLE_TEXT);
|
||||||
|
|
||||||
ProcessingOutcomeTransition.ProcessingOutcome result =
|
ProcessingOutcomeTransition.ProcessingOutcome result =
|
||||||
ProcessingOutcomeTransition.forNewDocument(outcome, LIMIT_1);
|
ProcessingOutcomeTransition.forNewDocument(outcome, LIMIT_1);
|
||||||
|
|
||||||
assertEquals(ProcessingStatus.FAILED_RETRYABLE, result.overallStatus());
|
assertEquals(ProcessingStatus.FAILED_FINAL, result.overallStatus(),
|
||||||
|
"NO_USABLE_TEXT must finalise immediately without retry");
|
||||||
|
assertFalse(result.retryable());
|
||||||
|
assertEquals(1, result.counters().contentErrorCount());
|
||||||
|
assertEquals(0, result.counters().transientErrorCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void forNewDocument_firstPageLimitExceeded_returnsFailedRetryable_contentCounterOne() {
|
||||||
|
PreCheckFailed outcome = new PreCheckFailed(candidate(), PreCheckFailureReason.PAGE_LIMIT_EXCEEDED);
|
||||||
|
|
||||||
|
ProcessingOutcomeTransition.ProcessingOutcome result =
|
||||||
|
ProcessingOutcomeTransition.forNewDocument(outcome, LIMIT_1);
|
||||||
|
|
||||||
|
assertEquals(ProcessingStatus.FAILED_RETRYABLE, result.overallStatus(),
|
||||||
|
"PAGE_LIMIT_EXCEEDED first occurrence must be retryable");
|
||||||
assertTrue(result.retryable());
|
assertTrue(result.retryable());
|
||||||
assertEquals(1, result.counters().contentErrorCount());
|
assertEquals(1, result.counters().contentErrorCount());
|
||||||
assertEquals(0, result.counters().transientErrorCount());
|
assertEquals(0, result.counters().transientErrorCount());
|
||||||
@@ -149,9 +164,10 @@ class ProcessingOutcomeTransitionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void forNewDocument_contentError_transientCounterIsIrrelevant() {
|
void forNewDocument_contentError_transientCounterIsIrrelevant() {
|
||||||
PreCheckFailed outcome = new PreCheckFailed(candidate(), PreCheckFailureReason.NO_USABLE_TEXT);
|
// PAGE_LIMIT_EXCEEDED is used here: it follows the 1-retry rule, and a non-zero
|
||||||
|
// transient counter must not influence the content-error decision.
|
||||||
|
PreCheckFailed outcome = new PreCheckFailed(candidate(), PreCheckFailureReason.PAGE_LIMIT_EXCEEDED);
|
||||||
|
|
||||||
// Counter before: 0 content errors (first occurrence), transient ignored
|
|
||||||
ProcessingOutcomeTransition.ProcessingOutcome result =
|
ProcessingOutcomeTransition.ProcessingOutcome result =
|
||||||
ProcessingOutcomeTransition.forKnownDocument(
|
ProcessingOutcomeTransition.forKnownDocument(
|
||||||
outcome, new FailureCounters(0, 5), LIMIT_1);
|
outcome, new FailureCounters(0, 5), LIMIT_1);
|
||||||
|
|||||||
+4
-3
@@ -23,13 +23,14 @@ class CheckpointIdTest {
|
|||||||
CheckpointId.PROMPT_FILE_PRESENT,
|
CheckpointId.PROMPT_FILE_PRESENT,
|
||||||
CheckpointId.SOURCE_FOLDER_PRESENT,
|
CheckpointId.SOURCE_FOLDER_PRESENT,
|
||||||
CheckpointId.TARGET_FOLDER_USABLE,
|
CheckpointId.TARGET_FOLDER_USABLE,
|
||||||
CheckpointId.SQLITE_PATH_USABLE
|
CheckpointId.SQLITE_PATH_USABLE,
|
||||||
|
CheckpointId.LOG_DIRECTORY_USABLE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void enumHasExactlyElevenValues() {
|
void enumHasTwelveValues() {
|
||||||
assertThat(CheckpointId.values()).hasSize(11);
|
assertThat(CheckpointId.values()).hasSize(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+39
-22
@@ -15,7 +15,7 @@ import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidation
|
|||||||
* <p>
|
* <p>
|
||||||
* Prüft insbesondere, dass alle drei Blöcke immer vollständig durchlaufen werden
|
* 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
|
* (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 {
|
class TechnicalTestOrchestratorTest {
|
||||||
|
|
||||||
@@ -88,6 +88,11 @@ class TechnicalTestOrchestratorTest {
|
|||||||
(family, propertyValue) -> EffectiveApiKeyDescriptor.fromPropertyFile());
|
(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<CheckpointResult> results, CheckpointId id) {
|
private static CheckpointResult findById(List<CheckpointResult> results, CheckpointId id) {
|
||||||
return results.stream()
|
return results.stream()
|
||||||
.filter(r -> r.checkpointId() == id)
|
.filter(r -> r.checkpointId() == id)
|
||||||
@@ -98,19 +103,20 @@ class TechnicalTestOrchestratorTest {
|
|||||||
// ------------------------------------------------------------------ Tests: Vollständig grüner Pfad
|
// ------------------------------------------------------------------ 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
|
@Test
|
||||||
void allBlocksSucceed_reportContainsElevenSuccessEntries() {
|
void allBlocksSucceed_reportContainsTwelveSuccessEntries() {
|
||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
allOkPathCheckPort(),
|
allOkPathCheckPort(),
|
||||||
allSuccessProviderService());
|
allSuccessProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
TechnicalTestRequest.of(validClaudeInput()));
|
TechnicalTestRequest.of(validClaudeInput()));
|
||||||
|
|
||||||
assertThat(report.size()).isEqualTo(11);
|
assertThat(report.size()).isEqualTo(12);
|
||||||
assertThat(report.results())
|
assertThat(report.results())
|
||||||
.allSatisfy(r -> assertThat(r).isInstanceOf(CheckpointResult.Success.class));
|
.allSatisfy(r -> assertThat(r).isInstanceOf(CheckpointResult.Success.class));
|
||||||
assertThat(report.hasErrors()).isFalse();
|
assertThat(report.hasErrors()).isFalse();
|
||||||
@@ -120,19 +126,20 @@ class TechnicalTestOrchestratorTest {
|
|||||||
// ------------------------------------------------------------------ Tests: Kein Frühabbruch
|
// ------------------------------------------------------------------ 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
|
@Test
|
||||||
void alwaysElevenCheckpointsInReport_evenWithFailures() {
|
void alwaysTwelveCheckpointsInReport_evenWithFailures() {
|
||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
noOpPathCheckPort(), // Block 2: alle Pfade nicht vorhanden
|
noOpPathCheckPort(), // Block 2: alle Pfade nicht vorhanden
|
||||||
throwingProviderService()); // Block 3: Exception
|
throwingProviderService(), // Block 3: Exception
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
TechnicalTestRequest.of(emptyProviderInput()));
|
TechnicalTestRequest.of(emptyProviderInput()));
|
||||||
|
|
||||||
assertThat(report.size()).isEqualTo(11);
|
assertThat(report.size()).isEqualTo(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ Tests: Block 1 - Exception führt zu Failure
|
// ------------------------------------------------------------------ Tests: Block 1 - Exception führt zu Failure
|
||||||
@@ -155,12 +162,13 @@ class TechnicalTestOrchestratorTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
throwingValidator,
|
throwingValidator,
|
||||||
allOkPathCheckPort(),
|
allOkPathCheckPort(),
|
||||||
allSuccessProviderService());
|
allSuccessProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
TechnicalTestRequest.of(validClaudeInput()));
|
TechnicalTestRequest.of(validClaudeInput()));
|
||||||
|
|
||||||
assertThat(report.size()).isEqualTo(11);
|
assertThat(report.size()).isEqualTo(12);
|
||||||
|
|
||||||
// Block-1-Checkpoints müssen Failure mit "Interner Fehler" sein
|
// Block-1-Checkpoints müssen Failure mit "Interner Fehler" sein
|
||||||
CheckpointResult basicValidation = findById(report.results(),
|
CheckpointResult basicValidation = findById(report.results(),
|
||||||
@@ -197,12 +205,13 @@ class TechnicalTestOrchestratorTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
allOkPathCheckPort(),
|
allOkPathCheckPort(),
|
||||||
throwingProviderService());
|
throwingProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
TechnicalTestRequest.of(validClaudeInput()));
|
TechnicalTestRequest.of(validClaudeInput()));
|
||||||
|
|
||||||
assertThat(report.size()).isEqualTo(11);
|
assertThat(report.size()).isEqualTo(12);
|
||||||
|
|
||||||
// Block-3-Checkpoints müssen Failure mit "Interner Fehler" sein
|
// Block-3-Checkpoints müssen Failure mit "Interner Fehler" sein
|
||||||
List<CheckpointId> block3Ids = List.of(
|
List<CheckpointId> block3Ids = List.of(
|
||||||
@@ -250,7 +259,8 @@ class TechnicalTestOrchestratorTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
pathPort,
|
pathPort,
|
||||||
allSuccessProviderService());
|
allSuccessProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
TechnicalTestRequest.of(validClaudeInput()));
|
TechnicalTestRequest.of(validClaudeInput()));
|
||||||
@@ -280,7 +290,8 @@ class TechnicalTestOrchestratorTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
pathPort,
|
pathPort,
|
||||||
allSuccessProviderService());
|
allSuccessProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
TechnicalTestRequest.of(validClaudeInput()));
|
TechnicalTestRequest.of(validClaudeInput()));
|
||||||
@@ -310,7 +321,8 @@ class TechnicalTestOrchestratorTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
pathPort,
|
pathPort,
|
||||||
allSuccessProviderService());
|
allSuccessProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
TechnicalTestRequest.of(validClaudeInput()));
|
TechnicalTestRequest.of(validClaudeInput()));
|
||||||
@@ -327,14 +339,15 @@ class TechnicalTestOrchestratorTest {
|
|||||||
// ------------------------------------------------------------------ Tests: Korrekte Checkpoint-Reihenfolge
|
// ------------------------------------------------------------------ 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
|
@Test
|
||||||
void report_containsAllExpectedCheckpointIds() {
|
void report_containsAllExpectedCheckpointIds() {
|
||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
noOpPathCheckPort(),
|
noOpPathCheckPort(),
|
||||||
allSuccessProviderService());
|
allSuccessProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
TechnicalTestRequest.of(validClaudeInput()));
|
TechnicalTestRequest.of(validClaudeInput()));
|
||||||
@@ -350,6 +363,7 @@ class TechnicalTestOrchestratorTest {
|
|||||||
CheckpointId.SOURCE_FOLDER_PRESENT,
|
CheckpointId.SOURCE_FOLDER_PRESENT,
|
||||||
CheckpointId.TARGET_FOLDER_USABLE,
|
CheckpointId.TARGET_FOLDER_USABLE,
|
||||||
CheckpointId.SQLITE_PATH_USABLE,
|
CheckpointId.SQLITE_PATH_USABLE,
|
||||||
|
CheckpointId.LOG_DIRECTORY_USABLE,
|
||||||
CheckpointId.API_KEY_PRESENT,
|
CheckpointId.API_KEY_PRESENT,
|
||||||
CheckpointId.BASE_URL_REACHABLE,
|
CheckpointId.BASE_URL_REACHABLE,
|
||||||
CheckpointId.API_KEY_ACCEPTED,
|
CheckpointId.API_KEY_ACCEPTED,
|
||||||
@@ -387,11 +401,12 @@ class TechnicalTestOrchestratorTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
pathPort,
|
pathPort,
|
||||||
allSuccessProviderService());
|
allSuccessProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
// configFilePath = C:/config/application.properties → Elternordner = C:/config
|
// configFilePath = C:/config/application.properties → Elternordner = C:/config
|
||||||
TechnicalTestReport report = orchestrator.run(
|
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);
|
CheckpointResult promptResult = findById(report.results(), CheckpointId.PROMPT_FILE_PRESENT);
|
||||||
assertThat(promptResult).isInstanceOf(CheckpointResult.Failure.class);
|
assertThat(promptResult).isInstanceOf(CheckpointResult.Failure.class);
|
||||||
@@ -431,7 +446,8 @@ class TechnicalTestOrchestratorTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
pathPort,
|
pathPort,
|
||||||
allSuccessProviderService());
|
allSuccessProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
// Kein configFilePath → Fallback auf config/prompt.txt
|
// Kein configFilePath → Fallback auf config/prompt.txt
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
@@ -462,7 +478,8 @@ class TechnicalTestOrchestratorTest {
|
|||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
pathPort,
|
pathPort,
|
||||||
allSuccessProviderService());
|
allSuccessProviderService(),
|
||||||
|
noOpLogDiagnosticsPort());
|
||||||
|
|
||||||
TechnicalTestReport report = orchestrator.run(
|
TechnicalTestReport report = orchestrator.run(
|
||||||
TechnicalTestRequest.of(validClaudeInput()));
|
TechnicalTestRequest.of(validClaudeInput()));
|
||||||
|
|||||||
+14
-2
@@ -29,18 +29,30 @@ class TechnicalTestRequestTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void withConfigFilePath_detectedAsPresent() {
|
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.hasConfigFilePath()).isTrue();
|
||||||
assertThat(request.configFilePath()).isEqualTo("/config/app.properties");
|
assertThat(request.configFilePath()).isEqualTo("/config/app.properties");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void nullConfigFilePath_normalisedToEmpty() {
|
void nullConfigFilePath_normalisedToEmpty() {
|
||||||
var request = new TechnicalTestRequest(minimalInput(), null);
|
var request = new TechnicalTestRequest(minimalInput(), null, null);
|
||||||
assertThat(request.configFilePath()).isEmpty();
|
assertThat(request.configFilePath()).isEmpty();
|
||||||
assertThat(request.hasConfigFilePath()).isFalse();
|
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
|
@Test
|
||||||
void nullValidationInputThrows() {
|
void nullValidationInputThrows() {
|
||||||
assertThatNullPointerException()
|
assertThatNullPointerException()
|
||||||
|
|||||||
+3
-1
@@ -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.application.validation.technicaltest.TechnicalTestOrchestrator;
|
||||||
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.AiModelCatalogDispatcher;
|
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.AiModelCatalogDispatcher;
|
||||||
import de.gecheckt.pdf.umbenenner.bootstrap.adapter.GuiConfigurationPropertiesWriter;
|
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.adapter.Log4jProcessingLogger;
|
||||||
import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.AnotherInstanceRunningException;
|
import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.AnotherInstanceRunningException;
|
||||||
import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.SingleInstanceGuard;
|
import de.gecheckt.pdf.umbenenner.bootstrap.singleinstance.SingleInstanceGuard;
|
||||||
@@ -807,7 +808,8 @@ public class BootstrapRunner {
|
|||||||
TechnicalTestOrchestrator technicalTestOrchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator technicalTestOrchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
pathCheckPort,
|
pathCheckPort,
|
||||||
providerTechnicalTestService);
|
providerTechnicalTestService,
|
||||||
|
new Log4jLogDiagnosticsAdapter());
|
||||||
de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService correctionExecutionService =
|
de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService correctionExecutionService =
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService(
|
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.adapter.out.resourcecreation.FilesystemResourceCreationAdapter());
|
new de.gecheckt.pdf.umbenenner.adapter.out.resourcecreation.FilesystemResourceCreationAdapter());
|
||||||
|
|||||||
+7
-2
@@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import de.gecheckt.pdf.umbenenner.bootstrap.startup.CliArgumentParser;
|
import de.gecheckt.pdf.umbenenner.bootstrap.startup.CliArgumentParser;
|
||||||
|
import de.gecheckt.pdf.umbenenner.bootstrap.startup.EarlyLogDirectoryInitializer;
|
||||||
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArguments;
|
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArguments;
|
||||||
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArgumentsParseResult;
|
import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArgumentsParseResult;
|
||||||
|
|
||||||
@@ -28,18 +29,22 @@ import de.gecheckt.pdf.umbenenner.bootstrap.startup.StartupArgumentsParseResult;
|
|||||||
*/
|
*/
|
||||||
public class PdfUmbenennerApplication {
|
public class PdfUmbenennerApplication {
|
||||||
|
|
||||||
private static final Logger LOG = LogManager.getLogger(PdfUmbenennerApplication.class);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application entry point.
|
* Application entry point.
|
||||||
* <p>
|
* <p>
|
||||||
* Parses the command-line arguments and delegates to {@link BootstrapRunner}.
|
* Parses the command-line arguments and delegates to {@link BootstrapRunner}.
|
||||||
* If the arguments cannot be parsed, an error is logged and the process exits
|
* If the arguments cannot be parsed, an error is logged and the process exits
|
||||||
* with code 1 before any further initialisation takes place.
|
* with code 1 before any further initialisation takes place.
|
||||||
|
* <p>
|
||||||
|
* Vor jeder Logger-Nutzung wird {@link EarlyLogDirectoryInitializer} aufgerufen,
|
||||||
|
* damit die Property {@code log.directory} aus der aktiven Konfigurationsdatei
|
||||||
|
* bereits vor der einmaligen Log4j2-Initialisierung gesetzt ist.
|
||||||
*
|
*
|
||||||
* @param args command-line arguments; see class JavaDoc for supported options
|
* @param args command-line arguments; see class JavaDoc for supported options
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
EarlyLogDirectoryInitializer.applyFromArgs(args);
|
||||||
|
Logger LOG = LogManager.getLogger(PdfUmbenennerApplication.class);
|
||||||
LOG.info("Starting PDF Umbenenner application...");
|
LOG.info("Starting PDF Umbenenner application...");
|
||||||
try {
|
try {
|
||||||
StartupArgumentsParseResult parseResult = new CliArgumentParser().parse(args);
|
StartupArgumentsParseResult parseResult = new CliArgumentParser().parse(args);
|
||||||
|
|||||||
+68
@@ -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}.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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<String> 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<String> toAbsolutePath(String path) {
|
||||||
|
try {
|
||||||
|
return Optional.of(Paths.get(path).toAbsolutePath().toString());
|
||||||
|
} catch (InvalidPathException e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package de.gecheckt.pdf.umbenenner.bootstrap.startup;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest die Log-Verzeichnis-Angabe so früh wie möglich aus der aktiven Konfigurationsdatei
|
||||||
|
* und setzt sie als System-Property {@code log.directory}, bevor Log4j2 zum ersten Mal
|
||||||
|
* initialisiert wird.
|
||||||
|
* <p>
|
||||||
|
* Hintergrund: {@code log4j2.xml} referenziert {@code ${sys:log.directory}} für den
|
||||||
|
* Pfad der Rolling-File-Datei. Da Log4j2 sich beim ersten {@code LogManager}-Aufruf
|
||||||
|
* einmalig konfiguriert, muss die System-Property bereits vorher gesetzt sein. Greift
|
||||||
|
* die Property nicht, fällt {@code log4j2.xml} auf ein nutzerschreibbares
|
||||||
|
* Default-Verzeichnis zurück, damit auch im MSI-Betrieb (Arbeitsverzeichnis unter
|
||||||
|
* {@code Program Files}, typischerweise nicht beschreibbar) eine Log-Datei entsteht.
|
||||||
|
* <p>
|
||||||
|
* Diese Klasse vermeidet bewusst jede Logger-Nutzung und schluckt sämtliche Fehler:
|
||||||
|
* Sie soll niemals den Programmstart verhindern, sondern lediglich einen frühen
|
||||||
|
* Best-Effort-Hinweis an Log4j2 liefern.
|
||||||
|
*/
|
||||||
|
public final class EarlyLogDirectoryInitializer {
|
||||||
|
|
||||||
|
private static final String SYSTEM_PROPERTY_KEY = "log.directory";
|
||||||
|
private static final String CONFIG_OPTION = "--config";
|
||||||
|
private static final Path DEFAULT_CONFIG_PATH = Paths.get("config/application.properties");
|
||||||
|
private static final String CONFIG_PROPERTY_KEY = "log.directory";
|
||||||
|
|
||||||
|
private EarlyLogDirectoryInitializer() {
|
||||||
|
// utility
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versucht, aus der aktiven Konfigurationsdatei den Wert von {@code log.directory}
|
||||||
|
* zu lesen, und setzt ihn als System-Property, sofern er ein nicht-leerer String ist.
|
||||||
|
* <p>
|
||||||
|
* Greift {@code --config <pfad>} auf, ansonsten {@code config/application.properties}
|
||||||
|
* relativ zum Arbeitsverzeichnis. Ist kein Wert ableitbar, bleibt die System-Property
|
||||||
|
* unverändert; in diesem Fall greift der in {@code log4j2.xml} hinterlegte Fallback.
|
||||||
|
*
|
||||||
|
* @param args Kommandozeilenargumente, dürfen {@code null} sein
|
||||||
|
*/
|
||||||
|
public static void applyFromArgs(String[] args) {
|
||||||
|
try {
|
||||||
|
if (System.getProperty(SYSTEM_PROPERTY_KEY) != null
|
||||||
|
&& !System.getProperty(SYSTEM_PROPERTY_KEY).isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Path configPath = resolveConfigPath(args);
|
||||||
|
if (configPath == null || !Files.isRegularFile(configPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Properties properties = new Properties();
|
||||||
|
try (InputStream in = Files.newInputStream(configPath)) {
|
||||||
|
properties.load(in);
|
||||||
|
}
|
||||||
|
String value = properties.getProperty(CONFIG_PROPERTY_KEY);
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
System.setProperty(SYSTEM_PROPERTY_KEY, value.trim());
|
||||||
|
}
|
||||||
|
} catch (IOException | RuntimeException ignored) {
|
||||||
|
// bewusst still: Log4j2-Fallback aus log4j2.xml übernimmt ansonsten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path resolveConfigPath(String[] args) {
|
||||||
|
if (args != null) {
|
||||||
|
for (int i = 0; i < args.length - 1; i++) {
|
||||||
|
if (CONFIG_OPTION.equals(args[i])) {
|
||||||
|
String raw = args[i + 1];
|
||||||
|
if (raw != null && !raw.isBlank()) {
|
||||||
|
try {
|
||||||
|
return Paths.get(raw);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DEFAULT_CONFIG_PATH;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,13 @@
|
|||||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
|
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
|
||||||
</Console>
|
</Console>
|
||||||
|
|
||||||
<!-- Rolling file appender for logs in ./logs/ directory -->
|
<!-- Rolling file appender. Honours the system property log.directory which
|
||||||
<RollingFile name="File" fileName="logs/pdf-umbenenner.log"
|
is set very early in the bootstrap from the active configuration file.
|
||||||
filePattern="logs/pdf-umbenenner-%d{yyyy-MM-dd}-%i.log.gz">
|
The fallback intentionally points to a guaranteed-writable user-scoped
|
||||||
|
directory so MSI installs (where the working directory typically lives
|
||||||
|
under Program Files and is not user-writable) still produce log files. -->
|
||||||
|
<RollingFile name="File" fileName="${sys:log.directory:-${sys:user.home}/pdf-umbenenner/logs}/pdf-umbenenner.log"
|
||||||
|
filePattern="${sys:log.directory:-${sys:user.home}/pdf-umbenenner/logs}/pdf-umbenenner-%d{yyyy-MM-dd}-%i.log.gz">
|
||||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
|
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
|
||||||
<Policies>
|
<Policies>
|
||||||
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
||||||
|
|||||||
+45
-36
@@ -33,8 +33,8 @@ import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
|||||||
* the document's master record reaches {@code SUCCESS} and the target file is on disk
|
* the document's master record reaches {@code SUCCESS} and the target file is on disk
|
||||||
* after the first run.</li>
|
* after the first run.</li>
|
||||||
* <li><strong>Deterministic content error</strong>: blank PDFs (no extractable text) reach
|
* <li><strong>Deterministic content error</strong>: blank PDFs (no extractable text) reach
|
||||||
* {@code FAILED_RETRYABLE} after the first run and {@code FAILED_FINAL} after the
|
* {@code FAILED_FINAL} immediately after the first run, because {@code NO_USABLE_TEXT}
|
||||||
* second run, exercising the one-retry rule for deterministic content errors.</li>
|
* is not retryable — an image-only scan without OCR text will not change on retry.</li>
|
||||||
* <li><strong>Transient technical error</strong>: AI stub failures produce
|
* <li><strong>Transient technical error</strong>: AI stub failures produce
|
||||||
* {@code FAILED_RETRYABLE} (transient counter incremented) without a target file.</li>
|
* {@code FAILED_RETRYABLE} (transient counter incremented) without a target file.</li>
|
||||||
* <li><strong>Transient error exhaustion</strong>: repeated AI stub failures across
|
* <li><strong>Transient error exhaustion</strong>: repeated AI stub failures across
|
||||||
@@ -124,27 +124,29 @@ class BatchRunEndToEndTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Scenario 2: Deterministic content error → FAILED_RETRYABLE → FAILED_FINAL
|
// Scenario 2: Deterministic content error (NO_USABLE_TEXT) → immediate FAILED_FINAL
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies the one-retry rule for deterministic content errors:
|
* Verifies that a blank PDF (no extractable text) reaches {@code FAILED_FINAL} immediately
|
||||||
|
* in a single run without any retry:
|
||||||
* <ol>
|
* <ol>
|
||||||
* <li>Run 1: blank PDF → pre-check fails (no extractable text) →
|
* <li>Run 1: blank PDF → pre-check fails ({@code NO_USABLE_TEXT}) →
|
||||||
* {@code FAILED_RETRYABLE}, content error counter = 1.</li>
|
* {@code FAILED_FINAL} immediately, content error counter = 1.</li>
|
||||||
* <li>Run 2: same outcome again → {@code FAILED_FINAL}, content error counter = 2.</li>
|
* <li>Run 2: document is already terminal → {@code SKIPPED_FINAL_FAILURE}.</li>
|
||||||
* </ol>
|
* </ol>
|
||||||
* No AI call is made in either run because the content pre-check prevents it.
|
* No AI call is made because the content pre-check prevents it.
|
||||||
|
* An image-only scan without OCR text will not change between runs, so no retry is useful.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void deterministicContentError_twoRuns_reachesFailedFinal(@TempDir Path tempDir)
|
void deterministicContentError_noUsableText_immediatelyFailedFinal(@TempDir Path tempDir)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
try (E2ETestContext ctx = E2ETestContext.initialize(tempDir)) {
|
try (E2ETestContext ctx = E2ETestContext.initialize(tempDir)) {
|
||||||
ctx.createBlankPdf("blank.pdf");
|
ctx.createBlankPdf("blank.pdf");
|
||||||
Path pdfPath = ctx.sourceFolder().resolve("blank.pdf");
|
Path pdfPath = ctx.sourceFolder().resolve("blank.pdf");
|
||||||
DocumentFingerprint fp = ctx.computeFingerprint(pdfPath);
|
DocumentFingerprint fp = ctx.computeFingerprint(pdfPath);
|
||||||
|
|
||||||
// --- Run 1 ---
|
// --- Run 1: NO_USABLE_TEXT → FAILED_FINAL immediately ---
|
||||||
ctx.runBatch();
|
ctx.runBatch();
|
||||||
|
|
||||||
assertThat(ctx.aiStub.invocationCount())
|
assertThat(ctx.aiStub.invocationCount())
|
||||||
@@ -152,26 +154,29 @@ class BatchRunEndToEndTest {
|
|||||||
.isEqualTo(0);
|
.isEqualTo(0);
|
||||||
|
|
||||||
DocumentRecord record1 = ctx.findDocumentRecord(fp).orElseThrow();
|
DocumentRecord record1 = ctx.findDocumentRecord(fp).orElseThrow();
|
||||||
assertThat(record1.overallStatus()).isEqualTo(ProcessingStatus.FAILED_RETRYABLE);
|
assertThat(record1.overallStatus())
|
||||||
|
.as("Blank PDF must finalise immediately without retry")
|
||||||
|
.isEqualTo(ProcessingStatus.FAILED_FINAL);
|
||||||
assertThat(record1.failureCounters().contentErrorCount()).isEqualTo(1);
|
assertThat(record1.failureCounters().contentErrorCount()).isEqualTo(1);
|
||||||
assertThat(record1.failureCounters().transientErrorCount()).isEqualTo(0);
|
assertThat(record1.failureCounters().transientErrorCount()).isEqualTo(0);
|
||||||
|
|
||||||
List<ProcessingAttempt> attempts1 = ctx.findAttempts(fp);
|
List<ProcessingAttempt> attempts1 = ctx.findAttempts(fp);
|
||||||
assertThat(attempts1).hasSize(1);
|
assertThat(attempts1).hasSize(1);
|
||||||
assertThat(attempts1.get(0).status()).isEqualTo(ProcessingStatus.FAILED_RETRYABLE);
|
assertThat(attempts1.get(0).status()).isEqualTo(ProcessingStatus.FAILED_FINAL);
|
||||||
assertThat(attempts1.get(0).retryable()).isTrue();
|
assertThat(attempts1.get(0).retryable()).isFalse();
|
||||||
|
|
||||||
// --- Run 2 ---
|
// --- Run 2: terminal FAILED_FINAL → skip ---
|
||||||
ctx.runBatch();
|
ctx.runBatch();
|
||||||
|
|
||||||
DocumentRecord record2 = ctx.findDocumentRecord(fp).orElseThrow();
|
DocumentRecord record2 = ctx.findDocumentRecord(fp).orElseThrow();
|
||||||
assertThat(record2.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL);
|
assertThat(record2.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL);
|
||||||
assertThat(record2.failureCounters().contentErrorCount()).isEqualTo(2);
|
assertThat(record2.failureCounters().contentErrorCount())
|
||||||
|
.as("Content error counter must not change after a skip")
|
||||||
|
.isEqualTo(1);
|
||||||
|
|
||||||
List<ProcessingAttempt> attempts2 = ctx.findAttempts(fp);
|
List<ProcessingAttempt> attempts2 = ctx.findAttempts(fp);
|
||||||
assertThat(attempts2).hasSize(2);
|
assertThat(attempts2).hasSize(2);
|
||||||
assertThat(attempts2.get(1).status()).isEqualTo(ProcessingStatus.FAILED_FINAL);
|
assertThat(attempts2.get(1).status()).isEqualTo(ProcessingStatus.SKIPPED_FINAL_FAILURE);
|
||||||
assertThat(attempts2.get(1).retryable()).isFalse();
|
|
||||||
|
|
||||||
// No target file should exist
|
// No target file should exist
|
||||||
assertThat(ctx.listTargetFiles()).isEmpty();
|
assertThat(ctx.listTargetFiles()).isEmpty();
|
||||||
@@ -277,40 +282,39 @@ class BatchRunEndToEndTest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies the skip-after-final-failure invariant:
|
* Verifies the skip-after-final-failure invariant:
|
||||||
* after a document reaches {@code FAILED_FINAL} (via two blank-PDF runs), a third run
|
* after a document reaches {@code FAILED_FINAL} (in a single blank-PDF run), a second run
|
||||||
* records a {@code SKIPPED_FINAL_FAILURE} attempt without changing the overall status
|
* records a {@code SKIPPED_FINAL_FAILURE} attempt without changing the overall status
|
||||||
* or failure counters.
|
* or failure counters.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void skipAfterFailedFinal_thirdRun_recordsSkip(@TempDir Path tempDir) throws Exception {
|
void skipAfterFailedFinal_secondRun_recordsSkip(@TempDir Path tempDir) throws Exception {
|
||||||
try (E2ETestContext ctx = E2ETestContext.initialize(tempDir)) {
|
try (E2ETestContext ctx = E2ETestContext.initialize(tempDir)) {
|
||||||
ctx.createBlankPdf("blank.pdf");
|
ctx.createBlankPdf("blank.pdf");
|
||||||
Path pdfPath = ctx.sourceFolder().resolve("blank.pdf");
|
Path pdfPath = ctx.sourceFolder().resolve("blank.pdf");
|
||||||
DocumentFingerprint fp = ctx.computeFingerprint(pdfPath);
|
DocumentFingerprint fp = ctx.computeFingerprint(pdfPath);
|
||||||
|
|
||||||
// Reach FAILED_FINAL via two blank-PDF runs
|
// Reach FAILED_FINAL in a single blank-PDF run (NO_USABLE_TEXT finalises immediately)
|
||||||
ctx.runBatch(); // → FAILED_RETRYABLE
|
|
||||||
ctx.runBatch(); // → FAILED_FINAL
|
ctx.runBatch(); // → FAILED_FINAL
|
||||||
|
|
||||||
DocumentRecord finalRecord = ctx.findDocumentRecord(fp).orElseThrow();
|
DocumentRecord finalRecord = ctx.findDocumentRecord(fp).orElseThrow();
|
||||||
assertThat(finalRecord.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL);
|
assertThat(finalRecord.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL);
|
||||||
int contentErrorsBefore = finalRecord.failureCounters().contentErrorCount();
|
int contentErrorsBefore = finalRecord.failureCounters().contentErrorCount();
|
||||||
|
|
||||||
// --- Run 3: should produce skip ---
|
// --- Run 2: should produce skip ---
|
||||||
ctx.runBatch();
|
ctx.runBatch();
|
||||||
|
|
||||||
DocumentRecord record3 = ctx.findDocumentRecord(fp).orElseThrow();
|
DocumentRecord record2 = ctx.findDocumentRecord(fp).orElseThrow();
|
||||||
assertThat(record3.overallStatus())
|
assertThat(record2.overallStatus())
|
||||||
.as("Overall status must remain FAILED_FINAL after a skip")
|
.as("Overall status must remain FAILED_FINAL after a skip")
|
||||||
.isEqualTo(ProcessingStatus.FAILED_FINAL);
|
.isEqualTo(ProcessingStatus.FAILED_FINAL);
|
||||||
assertThat(record3.failureCounters().contentErrorCount())
|
assertThat(record2.failureCounters().contentErrorCount())
|
||||||
.as("Failure counters must not change after a skip")
|
.as("Failure counters must not change after a skip")
|
||||||
.isEqualTo(contentErrorsBefore);
|
.isEqualTo(contentErrorsBefore);
|
||||||
|
|
||||||
List<ProcessingAttempt> attempts = ctx.findAttempts(fp);
|
List<ProcessingAttempt> attempts = ctx.findAttempts(fp);
|
||||||
assertThat(attempts).hasSize(3);
|
assertThat(attempts).hasSize(2);
|
||||||
assertThat(attempts.get(2).status()).isEqualTo(ProcessingStatus.SKIPPED_FINAL_FAILURE);
|
assertThat(attempts.get(1).status()).isEqualTo(ProcessingStatus.SKIPPED_FINAL_FAILURE);
|
||||||
assertThat(attempts.get(2).retryable()).isFalse();
|
assertThat(attempts.get(1).retryable()).isFalse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,10 +583,11 @@ class BatchRunEndToEndTest {
|
|||||||
* <ol>
|
* <ol>
|
||||||
* <li>Run 1: a searchable PDF reaches {@code SUCCESS} within the same run
|
* <li>Run 1: a searchable PDF reaches {@code SUCCESS} within the same run
|
||||||
* (PROPOSAL_READY → SUCCESS); a blank PDF (no extractable text) reaches
|
* (PROPOSAL_READY → SUCCESS); a blank PDF (no extractable text) reaches
|
||||||
* {@code FAILED_RETRYABLE}. {@link BatchRunOutcome#SUCCESS} is returned.</li>
|
* {@code FAILED_FINAL} immediately (no retry for {@code NO_USABLE_TEXT}).
|
||||||
|
* {@link BatchRunOutcome#SUCCESS} is returned.</li>
|
||||||
* <li>Run 2: the searchable PDF is skipped as {@code SKIPPED_ALREADY_PROCESSED};
|
* <li>Run 2: the searchable PDF is skipped as {@code SKIPPED_ALREADY_PROCESSED};
|
||||||
* the blank PDF reaches its second content error and is finalized to
|
* the blank PDF is skipped as {@code SKIPPED_FINAL_FAILURE} (already terminal).
|
||||||
* {@code FAILED_FINAL}. {@link BatchRunOutcome#SUCCESS} is returned.</li>
|
* {@link BatchRunOutcome#SUCCESS} is returned.</li>
|
||||||
* </ol>
|
* </ol>
|
||||||
* This confirms the exit-code contract: only hard bootstrap or infrastructure
|
* This confirms the exit-code contract: only hard bootstrap or infrastructure
|
||||||
* failures produce a non-zero exit code; document-level errors do not.
|
* failures produce a non-zero exit code; document-level errors do not.
|
||||||
@@ -609,7 +614,8 @@ class BatchRunEndToEndTest {
|
|||||||
.as("Searchable PDF must reach SUCCESS within the same single run")
|
.as("Searchable PDF must reach SUCCESS within the same single run")
|
||||||
.isEqualTo(ProcessingStatus.SUCCESS);
|
.isEqualTo(ProcessingStatus.SUCCESS);
|
||||||
assertThat(ctx.findDocumentRecord(fpBlank).orElseThrow().overallStatus())
|
assertThat(ctx.findDocumentRecord(fpBlank).orElseThrow().overallStatus())
|
||||||
.isEqualTo(ProcessingStatus.FAILED_RETRYABLE);
|
.as("Blank PDF (NO_USABLE_TEXT) must finalise immediately to FAILED_FINAL")
|
||||||
|
.isEqualTo(ProcessingStatus.FAILED_FINAL);
|
||||||
assertThat(ctx.findDocumentRecord(fpBlank).orElseThrow()
|
assertThat(ctx.findDocumentRecord(fpBlank).orElseThrow()
|
||||||
.failureCounters().contentErrorCount()).isEqualTo(1);
|
.failureCounters().contentErrorCount()).isEqualTo(1);
|
||||||
|
|
||||||
@@ -622,8 +628,7 @@ class BatchRunEndToEndTest {
|
|||||||
BatchRunOutcome run2 = ctx.runBatch();
|
BatchRunOutcome run2 = ctx.runBatch();
|
||||||
|
|
||||||
assertThat(run2)
|
assertThat(run2)
|
||||||
.as("Batch must complete with SUCCESS even when a document is finalised "
|
.as("Batch must complete with SUCCESS when all documents are skipped")
|
||||||
+ "to FAILED_FINAL")
|
|
||||||
.isEqualTo(BatchRunOutcome.SUCCESS);
|
.isEqualTo(BatchRunOutcome.SUCCESS);
|
||||||
|
|
||||||
DocumentRecord goodRecord = ctx.findDocumentRecord(fpGood).orElseThrow();
|
DocumentRecord goodRecord = ctx.findDocumentRecord(fpGood).orElseThrow();
|
||||||
@@ -632,8 +637,12 @@ class BatchRunEndToEndTest {
|
|||||||
.isEqualTo(ProcessingStatus.SUCCESS);
|
.isEqualTo(ProcessingStatus.SUCCESS);
|
||||||
|
|
||||||
DocumentRecord blankRecord = ctx.findDocumentRecord(fpBlank).orElseThrow();
|
DocumentRecord blankRecord = ctx.findDocumentRecord(fpBlank).orElseThrow();
|
||||||
assertThat(blankRecord.overallStatus()).isEqualTo(ProcessingStatus.FAILED_FINAL);
|
assertThat(blankRecord.overallStatus())
|
||||||
assertThat(blankRecord.failureCounters().contentErrorCount()).isEqualTo(2);
|
.as("Terminal FAILED_FINAL must remain unchanged after skip")
|
||||||
|
.isEqualTo(ProcessingStatus.FAILED_FINAL);
|
||||||
|
assertThat(blankRecord.failureCounters().contentErrorCount())
|
||||||
|
.as("Content error counter must not change after SKIPPED_FINAL_FAILURE")
|
||||||
|
.isEqualTo(1);
|
||||||
|
|
||||||
// Exactly one target file from the successfully processed document
|
// Exactly one target file from the successfully processed document
|
||||||
List<String> targetFiles = ctx.listTargetFiles();
|
List<String> targetFiles = ctx.listTargetFiles();
|
||||||
|
|||||||
+3
-2
@@ -21,9 +21,10 @@ public enum PreCheckFailureReason {
|
|||||||
* The extracted PDF text, after normalization, contains no letters or digits.
|
* The extracted PDF text, after normalization, contains no letters or digits.
|
||||||
* <p>
|
* <p>
|
||||||
* This is a deterministic content error: reprocessing the same file in a later run
|
* This is a deterministic content error: reprocessing the same file in a later run
|
||||||
* will have the same outcome unless the source file is changed.
|
* will have the same outcome unless the source file is changed (e.g. by adding OCR).
|
||||||
* <p>
|
* <p>
|
||||||
* Retry logic: exactly 1 retry in a later batch run.
|
* Retry logic: no retry — the document is immediately finalised to
|
||||||
|
* {@link ProcessingStatus#FAILED_FINAL}.
|
||||||
*/
|
*/
|
||||||
NO_USABLE_TEXT("No usable text in extracted PDF content"),
|
NO_USABLE_TEXT("No usable text in extracted PDF content"),
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
@@ -118,6 +118,11 @@
|
|||||||
java.desktop - JavaFX-Grafiksubsystem
|
java.desktop - JavaFX-Grafiksubsystem
|
||||||
java.logging - Log4j2-JUL-Bridge
|
java.logging - Log4j2-JUL-Bridge
|
||||||
java.xml - FXML/XML-Parsing
|
java.xml - FXML/XML-Parsing
|
||||||
|
jdk.crypto.ec - EC-Kryptographie (ECDH/ECDSA) fuer TLS 1.2/1.3;
|
||||||
|
ohne dieses Modul schlaegt der TLS-Handshake
|
||||||
|
mit modernen HTTPS-Endpunkten fehl (#92)
|
||||||
|
jdk.crypto.cryptoki - PKCS#11-Provider; vervollstaendigt den
|
||||||
|
JRE-Krypto-Stack analog zu einem Voll-JDK (#92)
|
||||||
Laufzeit-Verifikation ohne Entwicklungs-JDK erforderlich
|
Laufzeit-Verifikation ohne Entwicklungs-JDK erforderlich
|
||||||
(Anleitung in betrieb.md, Abschnitt MSI-Release-Checkliste).
|
(Anleitung in betrieb.md, Abschnitt MSI-Release-Checkliste).
|
||||||
-->
|
-->
|
||||||
@@ -134,6 +139,8 @@
|
|||||||
<module>java.scripting</module>
|
<module>java.scripting</module>
|
||||||
<module>java.sql</module>
|
<module>java.sql</module>
|
||||||
<module>java.xml</module>
|
<module>java.xml</module>
|
||||||
|
<module>jdk.crypto.ec</module>
|
||||||
|
<module>jdk.crypto.cryptoki</module>
|
||||||
<module>jdk.jfr</module>
|
<module>jdk.jfr</module>
|
||||||
<module>jdk.unsupported</module>
|
<module>jdk.unsupported</module>
|
||||||
<module>jdk.unsupported.desktop</module>
|
<module>jdk.unsupported.desktop</module>
|
||||||
|
|||||||
Reference in New Issue
Block a user