Fix #19: Fehlergrund bei fehlgeschlagenem KI-Aufruf im Begründungsbereich anzeigen
- DocumentCompletionEvent um optionales Feld failureMessage erweitert - DocumentProcessingCoordinator leitet Fehlermeldung bei Fehler-Status durch - GuiBatchRunResultRow um aiFailureMessage (Optional<String>) ergänzt - GuiBatchRunCoordinator.toRow() befüllt aiFailureMessage aus dem Event - GuiBatchRunTab.buildDetailText() zeigt bei fehlendem Reasoning und vorhandenem Fehlergrund: "⚠ Fehler: <Meldung>" vor dem Hinweistext - Alle Tests angepasst und neue Unit-Tests für aiFailureMessage ergänzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
@@ -31,6 +31,9 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
||||
* any is available for this candidate (may be present on success
|
||||
* and on some failure paths where an AI call had previously
|
||||
* produced a reasoning); {@code null} when no reasoning exists
|
||||
* @param failureMessage a human-readable description of the failure when the status
|
||||
* indicates an error; {@code null} for successful or skipped
|
||||
* candidates
|
||||
* @param processingDuration the wall-clock duration spent on this candidate in the current
|
||||
* run; never {@code null} and never negative
|
||||
*/
|
||||
@@ -41,6 +44,7 @@ public record DocumentCompletionEvent(
|
||||
String finalFileName,
|
||||
LocalDate resolvedDate,
|
||||
String aiReasoning,
|
||||
String failureMessage,
|
||||
Duration processingDuration) {
|
||||
|
||||
/**
|
||||
|
||||
+27
-17
@@ -618,6 +618,7 @@ public class DocumentProcessingCoordinator {
|
||||
resolvedFilename,
|
||||
proposalAttempt.resolvedDate(),
|
||||
proposalAttempt.aiReasoning(),
|
||||
null,
|
||||
attemptStart, now);
|
||||
return true;
|
||||
|
||||
@@ -701,7 +702,7 @@ public class DocumentProcessingCoordinator {
|
||||
publishCompletion(candidate, fingerprint,
|
||||
retryable ? DocumentCompletionStatus.FAILED_RETRYABLE
|
||||
: DocumentCompletionStatus.FAILED_PERMANENT,
|
||||
null, null, null, attemptStart, now);
|
||||
null, null, null, errorMessage, attemptStart, now);
|
||||
return true;
|
||||
|
||||
} catch (DocumentPersistenceException persistEx) {
|
||||
@@ -771,7 +772,7 @@ public class DocumentProcessingCoordinator {
|
||||
transition.retryable()
|
||||
? DocumentCompletionStatus.FAILED_RETRYABLE
|
||||
: DocumentCompletionStatus.FAILED_PERMANENT,
|
||||
null, null, reasoning, attemptStart, now);
|
||||
null, null, reasoning, errorMessage, attemptStart, now);
|
||||
|
||||
if (!secondaryPersisted) {
|
||||
logger.debug("Completion for '{}' reported without secondary persistence record.",
|
||||
@@ -815,7 +816,7 @@ public class DocumentProcessingCoordinator {
|
||||
logger.debug("Skip attempt #{} persisted for '{}' with status {}.",
|
||||
attemptNumber, candidate.uniqueIdentifier(), skipStatus);
|
||||
publishCompletion(candidate, fingerprint, DocumentCompletionStatus.SKIPPED,
|
||||
null, null, null, attemptStart, now);
|
||||
null, null, null, null, attemptStart, now);
|
||||
return true;
|
||||
|
||||
} catch (DocumentPersistenceException e) {
|
||||
@@ -1080,12 +1081,17 @@ public class DocumentProcessingCoordinator {
|
||||
outcome.counters().contentErrorCount(),
|
||||
outcome.counters().transientErrorCount());
|
||||
}
|
||||
// Pipeline-path terminal resolutions are reported to the progress observer.
|
||||
// PROPOSAL_READY is an intermediate state; the subsequent finalisation publishes
|
||||
// the actual completion event (SUCCESS or transient-error failure).
|
||||
// Pipeline-Pfad: terminale Auflösungen werden dem Progress-Observer gemeldet.
|
||||
// PROPOSAL_READY ist ein Zwischenstatus; die Finalisierung publiziert das eigentliche
|
||||
// Completion-Event (SUCCESS oder Fehler).
|
||||
if (outcome.overallStatus() != ProcessingStatus.PROPOSAL_READY) {
|
||||
String pipelineFailureMessage = null;
|
||||
if (outcome.overallStatus() == ProcessingStatus.FAILED_RETRYABLE
|
||||
|| outcome.overallStatus() == ProcessingStatus.FAILED_FINAL) {
|
||||
pipelineFailureMessage = buildFailureMessage(pipelineOutcome, outcome);
|
||||
}
|
||||
publishCompletion(candidate, fingerprint, toCompletionStatus(outcome),
|
||||
null, null, null, attemptStart, now);
|
||||
null, null, null, pipelineFailureMessage, attemptStart, now);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1216,15 +1222,17 @@ public class DocumentProcessingCoordinator {
|
||||
* Any runtime exception thrown by the observer is caught and logged at warn level and must
|
||||
* not affect persistence or batch flow.
|
||||
*
|
||||
* @param candidate the candidate being reported; must not be null
|
||||
* @param fingerprint the content-based identity of the document; must not be null
|
||||
* @param status the aggregated completion status; must not be null
|
||||
* @param finalFileName the final target filename on success; {@code null} otherwise
|
||||
* @param resolvedDate the resolved date on success; may be {@code null} otherwise
|
||||
* @param aiReasoning the AI reasoning when one is available for this result;
|
||||
* {@code null} otherwise
|
||||
* @param startInstant the moment processing of the candidate began in this run
|
||||
* @param endInstant the moment the terminal resolution was reached
|
||||
* @param candidate die Kandidatdatei, über die berichtet wird; darf nicht null sein
|
||||
* @param fingerprint der inhaltsbasierte Fingerprint des Dokuments; darf nicht null sein
|
||||
* @param status der aggregierte Abschlussstatus; darf nicht null sein
|
||||
* @param finalFileName der finale Zieldateiname bei Erfolg; sonst {@code null}
|
||||
* @param resolvedDate das aufgelöste Datum bei Erfolg; bei Fehlern kann es {@code null} sein
|
||||
* @param aiReasoning das KI-Reasoning, wenn für dieses Ergebnis eines vorliegt;
|
||||
* sonst {@code null}
|
||||
* @param failureMessage eine lesbare Fehlerbeschreibung bei Fehler-Status; bei Erfolg
|
||||
* und Überspringen {@code null}
|
||||
* @param startInstant der Zeitpunkt, zu dem die Verarbeitung des Kandidaten begann
|
||||
* @param endInstant der Zeitpunkt der terminalen Auflösung
|
||||
*/
|
||||
private void publishCompletion(
|
||||
SourceDocumentCandidate candidate,
|
||||
@@ -1233,6 +1241,7 @@ public class DocumentProcessingCoordinator {
|
||||
String finalFileName,
|
||||
LocalDate resolvedDate,
|
||||
String aiReasoning,
|
||||
String failureMessage,
|
||||
Instant startInstant,
|
||||
Instant endInstant) {
|
||||
Consumer<DocumentCompletionEvent> forwarder = completionForwarder;
|
||||
@@ -1251,9 +1260,10 @@ public class DocumentProcessingCoordinator {
|
||||
finalFileName,
|
||||
resolvedDate,
|
||||
aiReasoning,
|
||||
failureMessage,
|
||||
duration));
|
||||
} catch (RuntimeException forwarderFailure) {
|
||||
logger.warn("Progress forwarder threw while reporting completion for '{}': {}",
|
||||
logger.warn("Progress forwarder beim Melden des Abschlusses für '{}' aufgetreten: {}",
|
||||
candidate.uniqueIdentifier(), forwarderFailure.getMessage(), forwarderFailure);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -85,13 +85,13 @@ class BatchRunProgressObservationTest {
|
||||
@Test
|
||||
void documentCompletionEvent_rejectsBlankFilename() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new DocumentCompletionEvent(
|
||||
" ", DUMMY_FP, DocumentCompletionStatus.SUCCESS, null, null, null, Duration.ZERO));
|
||||
" ", DUMMY_FP, DocumentCompletionStatus.SUCCESS, null, null, null, null, Duration.ZERO));
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentCompletionEvent_rejectsNegativeDuration() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new DocumentCompletionEvent(
|
||||
"x.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS, null, null, null,
|
||||
"x.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS, null, null, null, null,
|
||||
Duration.ofSeconds(-1)));
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class BatchRunProgressObservationTest {
|
||||
void documentCompletionEvent_carriesOptionalFields() {
|
||||
DocumentCompletionEvent event = new DocumentCompletionEvent(
|
||||
"x.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS, "2026-03-01 - Titel.pdf",
|
||||
LocalDate.of(2026, 3, 1), "weil wichtig", Duration.ofMillis(123));
|
||||
LocalDate.of(2026, 3, 1), "weil wichtig", null, Duration.ofMillis(123));
|
||||
|
||||
assertEquals("x.pdf", event.originalFileName());
|
||||
assertEquals(DocumentCompletionStatus.SUCCESS, event.status());
|
||||
@@ -129,7 +129,7 @@ class BatchRunProgressObservationTest {
|
||||
assertSame(a, b);
|
||||
a.onRunStarted(new RunId("r-1"), 5);
|
||||
a.onDocumentCompleted(new DocumentCompletionEvent(
|
||||
"x.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED, null, null, null, Duration.ZERO));
|
||||
"x.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED, null, null, null, null, Duration.ZERO));
|
||||
a.onRunEnded(new RunSummary(0, 0, 0));
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@ class BatchRunProgressObservationTest {
|
||||
candidate.uniqueIdentifier(),
|
||||
fingerprint,
|
||||
statuses.get(index),
|
||||
null, null, null, Duration.ofMillis(10)));
|
||||
null, null, null, null, Duration.ofMillis(10)));
|
||||
}
|
||||
onBeforeReturn.run();
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user