diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslator.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslator.java index fe48077..aa2fc61 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslator.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslator.java @@ -1,13 +1,13 @@ package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun; /** - * Übersetzt technische KI-Fehlermeldungen in benutzerfreundliche deutsche Texte. + * Übersetzt strukturierte Fehlermeldungen aus der Anwendungsschicht in + * benutzerfreundliche deutsche Texte für den Detailbereich des Verarbeitungslauf-Tabs. *

- * Die Klasse wertet die rohe, englischsprachige Fehlermeldung aus dem - * Verarbeitungsversuch aus und liefert eine für den Endbenutzer lesbare - * Beschreibung des Fehlergrunds. Das ursprüngliche Datenmodell bleibt - * unverändert; die Übersetzung findet ausschließlich in der Darstellungsschicht - * statt. + * Die Klasse wertet die englischsprachige Fehlermeldung aus dem Verarbeitungsversuch + * musterbasiert aus und liefert eine für den Endbenutzer lesbare Beschreibung des + * Fehlergrunds. Das ursprüngliche Datenmodell bleibt unverändert; die Übersetzung + * findet ausschließlich in der Darstellungsschicht statt. *

* Die Mustererkennung erfolgt ohne Berücksichtigung der Groß-/Kleinschreibung * und prüft die definierten Schlüsselbegriffe in festgelegter Reihenfolge, @@ -26,14 +26,35 @@ final class AiFailureMessageTranslator { * Fallback-Text zurückgegeben. * * @param technicalMessage die rohe technische Fehlermeldung; darf {@code null} sein - * @return eine nicht-leere deutsche Benutzerfehlermeldung + * @return eine nicht-leere deutsche Benutzerfehlermeldung ohne führendes Warnsymbol */ static String translate(String technicalMessage) { if (technicalMessage == null || technicalMessage.isBlank()) { - return "KI-Aufruf fehlgeschlagen. Details im Anwendungslog."; + return "Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten."; } String lower = technicalMessage.toLowerCase(java.util.Locale.ROOT); + // Pre-Check-Fehler: kein lesbarer Text im PDF + if (lower.contains("no usable text")) { + return "PDF enthält keinen lesbaren Text. Möglicherweise handelt es sich um einen Scan" + + " ohne Texterkennung (OCR). Eine automatische Benennung ist nicht möglich."; + } + + // KI-Validierungsfehler: Titel überschreitet die konfigurierte Maximallänge + if (lower.contains("title exceeds")) { + return buildTitleExceedsMessage(technicalMessage); + } + + // Defekte oder strukturell nicht lesbare PDF-Datei + if (lower.contains("content not extractable") + || lower.contains("ioexception") + || lower.contains("end of file") + || lower.contains("endoffileexception") + || lower.contains("eof")) { + return "Die PDF-Datei ist ungültig oder beschädigt und kann nicht verarbeitet werden."; + } + + // HTTP-Authentifizierungsfehler if (lower.contains("http_401")) { return "KI-Dienst: Ungültiger API-Schlüssel. Bitte in den Einstellungen prüfen."; } @@ -46,9 +67,51 @@ final class AiFailureMessageTranslator { if (lower.contains("http_5")) { return "KI-Dienst vorübergehend nicht erreichbar. Bitte später erneut versuchen."; } + + // Netzwerk- und Verbindungsfehler if (lower.contains("connection") || lower.contains("timeout") || lower.contains("refused")) { - return "KI-Dienst nicht erreichbar. Bitte Netzwerkverbindung prüfen."; + return "KI-Dienst nicht erreichbar. Bitte Verbindung und Konfiguration prüfen."; } - return "KI-Aufruf fehlgeschlagen. Details im Anwendungslog."; + + return "Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten."; + } + + /** + * Baut aus einer „Title exceeds"-Fehlermeldung einen benutzerfreundlichen Text, + * der Titel, tatsächliche Länge und konfiguriertes Limit nennt. + *

+ * Erwartet wird das Format: + * {@code … Title exceeds N characters (base title): 'Titel' …} + *

+ * Kann das Format nicht geparst werden, wird ein generischer Hinweis zurückgegeben. + * + * @param technicalMessage die vollständige technische Fehlermeldung + * @return benutzerfreundlicher Hinweis auf den zu langen Titel + */ + private static String buildTitleExceedsMessage(String technicalMessage) { + try { + int exceedsIdx = technicalMessage.indexOf("Title exceeds "); + if (exceedsIdx >= 0) { + String afterExceeds = technicalMessage.substring(exceedsIdx + "Title exceeds ".length()); + int charIdx = afterExceeds.indexOf(" characters"); + if (charIdx > 0) { + int limit = Integer.parseInt(afterExceeds.substring(0, charIdx).trim()); + int colonQuote = technicalMessage.indexOf(": '", exceedsIdx); + if (colonQuote >= 0) { + String afterQuote = technicalMessage.substring(colonQuote + 3); + int closingQuote = afterQuote.lastIndexOf("'"); + if (closingQuote > 0) { + String title = afterQuote.substring(0, closingQuote); + return "KI-Vorschlag abgelehnt: '" + title + "' ist zu lang (" + + title.length() + " Zeichen, Limit: " + limit + + "). Bitte Dateinamen manuell kürzen."; + } + } + } + } + } catch (NumberFormatException | StringIndexOutOfBoundsException ignored) { + // Fallback unten + } + return "KI-Vorschlag abgelehnt: Titel überschreitet die maximale Länge. Bitte Dateinamen manuell kürzen."; } } diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java index 2420037..b52ac9e 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/GuiBatchRunTab.java @@ -1277,13 +1277,9 @@ public final class GuiBatchRunTab { builder.append('\n'); row.aiReasoning().ifPresentOrElse( reasoning -> builder.append(reasoning), - () -> { - row.aiFailureMessage().ifPresent(msg -> - builder.append("\u26A0 Fehler: ") - .append(AiFailureMessageTranslator.translate(msg)) - .append("\n\n")); - builder.append(NO_REASONING_TEXT); - }); + () -> row.aiFailureMessage().ifPresentOrElse( + msg -> builder.append("\u26A0 ").append(AiFailureMessageTranslator.translate(msg)), + () -> builder.append(NO_REASONING_TEXT))); return builder.toString(); } diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslatorTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslatorTest.java index 98ce3b3..804ec0d 100644 --- a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslatorTest.java +++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/AiFailureMessageTranslatorTest.java @@ -1,6 +1,7 @@ package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -10,10 +11,91 @@ import org.junit.jupiter.api.Test; * Unit-Tests für {@link AiFailureMessageTranslator}. *

* Prüft alle definierten Mapping-Fälle sowie Randbedingungen wie - * {@code null}-Eingaben und unbekannte Fehlertexte. + * {@code null}-Eingaben und unbekannte Fehlertexte. Die zurückgegebenen Meldungen + * dürfen keinen Verweis auf interne Logdateien enthalten. */ class AiFailureMessageTranslatorTest { + // ------------------------------------------------------------------------- + // Pre-Check-Fehler: kein lesbarer Text + // ------------------------------------------------------------------------- + + @Test + void noUsableText_liefertLesbarkeitsMeldung() { + String result = AiFailureMessageTranslator.translate( + "Processing failed (retryable). Reason: No usable text in extracted PDF content"); + assertTrue(result.contains("keinen lesbaren Text"), result); + assertTrue(result.contains("OCR"), result); + } + + @Test + void noUsableText_grossKleinschreibung_wirdErkannt() { + String result = AiFailureMessageTranslator.translate("NO USABLE TEXT found"); + assertTrue(result.contains("keinen lesbaren Text"), result); + } + + // ------------------------------------------------------------------------- + // KI-Validierungsfehler: Titel zu lang + // ------------------------------------------------------------------------- + + @Test + void titleExceeds_liefertTitelZuLangMeldung() { + String result = AiFailureMessageTranslator.translate( + "Processing failed finally (not retryable). AI functional error: " + + "Title exceeds 60 characters (base title): 'Sehr langer Titel der das Limit ueberschreitet'"); + assertTrue(result.contains("ist zu lang"), result); + assertTrue(result.contains("Sehr langer Titel der das Limit ueberschreitet"), result); + assertTrue(result.contains("Limit: 60"), result); + assertTrue(result.contains("manuell kürzen"), result); + } + + @Test + void titleExceeds_mitTitellaengeInMeldung() { + String result = AiFailureMessageTranslator.translate( + "Title exceeds 60 characters (base title): 'Ein langer Dokumenttitel'"); + assertTrue(result.contains("Ein langer Dokumenttitel"), result); + assertTrue(result.contains("Limit: 60"), result); + } + + @Test + void titleExceeds_ohneParsebaresFormat_liefertGenerischenHinweis() { + String result = AiFailureMessageTranslator.translate("title exceeds some limit"); + assertTrue(result.contains("Titel"), result); + assertTrue(result.contains("manuell kürzen"), result); + } + + // ------------------------------------------------------------------------- + // Defekte oder strukturell nicht lesbare PDF-Datei + // ------------------------------------------------------------------------- + + @Test + void contentNotExtractable_liefertDefektePdfMeldung() { + String result = AiFailureMessageTranslator.translate( + "Processing failed (retryable). Reason: PDF content not extractable"); + assertTrue(result.contains("ungültig oder beschädigt"), result); + } + + @Test + void ioException_liefertDefektePdfMeldung() { + String result = AiFailureMessageTranslator.translate( + "Processing failed (retryable). Technical: IOException reading file: test.pdf"); + assertTrue(result.contains("ungültig oder beschädigt"), result); + } + + @Test + void endOfFile_liefertDefektePdfMeldung() { + String result = AiFailureMessageTranslator.translate( + "Processing failed (retryable). Technical: Unexpected end of file"); + assertTrue(result.contains("ungültig oder beschädigt"), result); + } + + @Test + void eof_liefertDefektePdfMeldung() { + String result = AiFailureMessageTranslator.translate( + "Technical error: EOF while reading PDF structure"); + assertTrue(result.contains("ungültig oder beschädigt"), result); + } + // ------------------------------------------------------------------------- // HTTP-Statuscodes // ------------------------------------------------------------------------- @@ -56,59 +138,74 @@ class AiFailureMessageTranslatorTest { } // ------------------------------------------------------------------------- - // Verbindungsfehler + // Netzwerk- und Verbindungsfehler // ------------------------------------------------------------------------- @Test - void connectionFehler_liefertNetzwerkMeldung() { + void connectionFehler_liefertVerbindungsMeldung() { String result = AiFailureMessageTranslator.translate("Connection refused to https://api.example.com"); - assertEquals("KI-Dienst nicht erreichbar. Bitte Netzwerkverbindung prüfen.", result); + assertEquals("KI-Dienst nicht erreichbar. Bitte Verbindung und Konfiguration prüfen.", result); } @Test - void timeoutFehler_liefertNetzwerkMeldung() { + void timeoutFehler_liefertVerbindungsMeldung() { String result = AiFailureMessageTranslator.translate("AI technical error: timeout after 30s"); - assertEquals("KI-Dienst nicht erreichbar. Bitte Netzwerkverbindung prüfen.", result); + assertEquals("KI-Dienst nicht erreichbar. Bitte Verbindung und Konfiguration prüfen.", result); } @Test - void refusedFehler_liefertNetzwerkMeldung() { + void refusedFehler_liefertVerbindungsMeldung() { String result = AiFailureMessageTranslator.translate("connect refused"); - assertEquals("KI-Dienst nicht erreichbar. Bitte Netzwerkverbindung prüfen.", result); + assertEquals("KI-Dienst nicht erreichbar. Bitte Verbindung und Konfiguration prüfen.", result); } // ------------------------------------------------------------------------- - // Fallback + // Unbekannter Fehler (Fallback) // ------------------------------------------------------------------------- @Test void unbekannterFehler_liefertFallback() { String result = AiFailureMessageTranslator.translate("some completely unknown error text"); - assertEquals("KI-Aufruf fehlgeschlagen. Details im Anwendungslog.", result); + assertEquals("Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.", result); } @Test void null_liefertFallback() { String result = AiFailureMessageTranslator.translate(null); - assertEquals("KI-Aufruf fehlgeschlagen. Details im Anwendungslog.", result); + assertEquals("Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.", result); } @Test void leerstring_liefertFallback() { String result = AiFailureMessageTranslator.translate(""); - assertEquals("KI-Aufruf fehlgeschlagen. Details im Anwendungslog.", result); + assertEquals("Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.", result); } @Test void nurLeerzeichen_liefertFallback() { String result = AiFailureMessageTranslator.translate(" "); - assertEquals("KI-Aufruf fehlgeschlagen. Details im Anwendungslog.", result); + assertEquals("Verarbeitung fehlgeschlagen. Bitte Konfiguration prüfen und ggf. erneut verarbeiten.", result); } // ------------------------------------------------------------------------- - // Allgemeine Qualität der Rückgabe + // Qualitätsregeln: kein Verweis auf interne Logs // ------------------------------------------------------------------------- + @Test + void keineMeldungEnthältAnwendungslogVerweis() { + String[] eingaben = { + null, "", " ", "HTTP_401", "HTTP_403", "HTTP_429", "HTTP_500", + "connection error", "timeout", "refused", "unbekannt", + "no usable text", "content not extractable", "IOException", + "Title exceeds 60 characters (base title): 'Titel'" + }; + for (String eingabe : eingaben) { + String result = AiFailureMessageTranslator.translate(eingabe); + assertFalse(result.toLowerCase().contains("anwendungslog"), + "Meldung darf keinen Anwendungslog-Verweis enthalten für: " + eingabe); + } + } + @Test void alleEingaben_liefernNichtLeereTexte() { String[] eingaben = { @@ -118,7 +215,7 @@ class AiFailureMessageTranslatorTest { for (String eingabe : eingaben) { String result = AiFailureMessageTranslator.translate(eingabe); assertNotNull(result, "Ergebnis darf nicht null sein für: " + eingabe); - assertTrue(!result.isBlank(), "Ergebnis darf nicht leer sein für: " + eingabe); + assertFalse(result.isBlank(), "Ergebnis darf nicht leer sein für: " + eingabe); } }