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:
+3
@@ -506,6 +506,8 @@ public final class GuiBatchRunCoordinator {
|
|||||||
? Optional.empty() : Optional.of(event.resolvedDate());
|
? Optional.empty() : Optional.of(event.resolvedDate());
|
||||||
Optional<String> reasoning = event.aiReasoning() == null || event.aiReasoning().isBlank()
|
Optional<String> reasoning = event.aiReasoning() == null || event.aiReasoning().isBlank()
|
||||||
? Optional.empty() : Optional.of(event.aiReasoning());
|
? Optional.empty() : Optional.of(event.aiReasoning());
|
||||||
|
Optional<String> failureMessage = event.failureMessage() == null || event.failureMessage().isBlank()
|
||||||
|
? Optional.empty() : Optional.of(event.failureMessage());
|
||||||
Duration duration = event.processingDuration();
|
Duration duration = event.processingDuration();
|
||||||
return new GuiBatchRunResultRow(
|
return new GuiBatchRunResultRow(
|
||||||
event.originalFileName(),
|
event.originalFileName(),
|
||||||
@@ -514,6 +516,7 @@ public final class GuiBatchRunCoordinator {
|
|||||||
finalName,
|
finalName,
|
||||||
date,
|
date,
|
||||||
reasoning,
|
reasoning,
|
||||||
|
failureMessage,
|
||||||
duration);
|
duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -34,6 +34,9 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
|||||||
* rename; empty otherwise
|
* rename; empty otherwise
|
||||||
* @param aiReasoning the AI reasoning shown in the side panel; empty when no
|
* @param aiReasoning the AI reasoning shown in the side panel; empty when no
|
||||||
* reasoning is available for this row
|
* reasoning is available for this row
|
||||||
|
* @param aiFailureMessage eine lesbare Fehlerbeschreibung, wenn der KI-Aufruf oder die
|
||||||
|
* Verarbeitung fehlgeschlagen ist; leer bei Erfolg und
|
||||||
|
* übersprungenen Dokumenten
|
||||||
* @param processingDuration wall-clock duration spent on the candidate in this run;
|
* @param processingDuration wall-clock duration spent on the candidate in this run;
|
||||||
* never {@code null} and never negative
|
* never {@code null} and never negative
|
||||||
* @param resetPending {@code true} when the document's persistence status has been
|
* @param resetPending {@code true} when the document's persistence status has been
|
||||||
@@ -46,6 +49,7 @@ public record GuiBatchRunResultRow(
|
|||||||
Optional<String> finalFileName,
|
Optional<String> finalFileName,
|
||||||
Optional<LocalDate> resolvedDate,
|
Optional<LocalDate> resolvedDate,
|
||||||
Optional<String> aiReasoning,
|
Optional<String> aiReasoning,
|
||||||
|
Optional<String> aiFailureMessage,
|
||||||
Duration processingDuration,
|
Duration processingDuration,
|
||||||
boolean resetPending) {
|
boolean resetPending) {
|
||||||
|
|
||||||
@@ -79,6 +83,7 @@ public record GuiBatchRunResultRow(
|
|||||||
finalFileName = finalFileName == null ? Optional.empty() : finalFileName;
|
finalFileName = finalFileName == null ? Optional.empty() : finalFileName;
|
||||||
resolvedDate = resolvedDate == null ? Optional.empty() : resolvedDate;
|
resolvedDate = resolvedDate == null ? Optional.empty() : resolvedDate;
|
||||||
aiReasoning = aiReasoning == null ? Optional.empty() : aiReasoning;
|
aiReasoning = aiReasoning == null ? Optional.empty() : aiReasoning;
|
||||||
|
aiFailureMessage = aiFailureMessage == null ? Optional.empty() : aiFailureMessage;
|
||||||
Objects.requireNonNull(processingDuration, "processingDuration must not be null");
|
Objects.requireNonNull(processingDuration, "processingDuration must not be null");
|
||||||
if (processingDuration.isNegative()) {
|
if (processingDuration.isNegative()) {
|
||||||
throw new IllegalArgumentException("processingDuration must not be negative");
|
throw new IllegalArgumentException("processingDuration must not be negative");
|
||||||
@@ -97,6 +102,8 @@ public record GuiBatchRunResultRow(
|
|||||||
* empty)
|
* empty)
|
||||||
* @param aiReasoning the AI reasoning text; may be {@code null} (treated as
|
* @param aiReasoning the AI reasoning text; may be {@code null} (treated as
|
||||||
* empty)
|
* empty)
|
||||||
|
* @param aiFailureMessage eine lesbare Fehlerbeschreibung bei Fehler; may be
|
||||||
|
* {@code null} (treated as empty)
|
||||||
* @param processingDuration the wall-clock processing duration; never {@code null}
|
* @param processingDuration the wall-clock processing duration; never {@code null}
|
||||||
*/
|
*/
|
||||||
public GuiBatchRunResultRow(
|
public GuiBatchRunResultRow(
|
||||||
@@ -106,9 +113,10 @@ public record GuiBatchRunResultRow(
|
|||||||
Optional<String> finalFileName,
|
Optional<String> finalFileName,
|
||||||
Optional<LocalDate> resolvedDate,
|
Optional<LocalDate> resolvedDate,
|
||||||
Optional<String> aiReasoning,
|
Optional<String> aiReasoning,
|
||||||
|
Optional<String> aiFailureMessage,
|
||||||
Duration processingDuration) {
|
Duration processingDuration) {
|
||||||
this(originalFileName, fingerprint, status, finalFileName, resolvedDate, aiReasoning,
|
this(originalFileName, fingerprint, status, finalFileName, resolvedDate, aiReasoning,
|
||||||
processingDuration, false);
|
aiFailureMessage, processingDuration, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,6 +139,7 @@ public record GuiBatchRunResultRow(
|
|||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
Duration.ZERO,
|
Duration.ZERO,
|
||||||
true);
|
true);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -899,7 +899,11 @@ public final class GuiBatchRunTab {
|
|||||||
builder.append('\n');
|
builder.append('\n');
|
||||||
row.aiReasoning().ifPresentOrElse(
|
row.aiReasoning().ifPresentOrElse(
|
||||||
reasoning -> builder.append(reasoning),
|
reasoning -> builder.append(reasoning),
|
||||||
() -> builder.append(NO_REASONING_TEXT));
|
() -> {
|
||||||
|
row.aiFailureMessage().ifPresent(msg ->
|
||||||
|
builder.append("\u26A0 Fehler: ").append(msg).append("\n\n"));
|
||||||
|
builder.append(NO_REASONING_TEXT);
|
||||||
|
});
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1012,6 +1016,7 @@ public final class GuiBatchRunTab {
|
|||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.of(message),
|
Optional.of(message),
|
||||||
|
Optional.empty(),
|
||||||
Duration.ZERO,
|
Duration.ZERO,
|
||||||
false);
|
false);
|
||||||
upsertResultRowByFingerprint(missingRow);
|
upsertResultRowByFingerprint(missingRow);
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@ class GuiBatchRunCoordinatorMiniRunTest {
|
|||||||
observer.onRunStarted(new RunId("mini-1"), 1);
|
observer.onRunStarted(new RunId("mini-1"), 1);
|
||||||
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
||||||
"a.pdf", FP1, DocumentCompletionStatus.SUCCESS,
|
"a.pdf", FP1, DocumentCompletionStatus.SUCCESS,
|
||||||
"2026-01-01 - Test.pdf", null, null, Duration.ofMillis(50)));
|
"2026-01-01 - Test.pdf", null, null, null, Duration.ofMillis(50)));
|
||||||
observer.onRunEnded(new RunSummary(1, 0, 0));
|
observer.onRunEnded(new RunSummary(1, 0, 0));
|
||||||
return GuiBatchRunLaunchOutcome.completed();
|
return GuiBatchRunLaunchOutcome.completed();
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-8
@@ -77,10 +77,10 @@ class GuiBatchRunCoordinatorTest {
|
|||||||
observer.onRunStarted(new RunId("run-1"), 2);
|
observer.onRunStarted(new RunId("run-1"), 2);
|
||||||
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
||||||
"a.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS,
|
"a.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS,
|
||||||
"2026-03-01 - Titel.pdf", LocalDate.of(2026, 3, 1), "gut", Duration.ofMillis(20)));
|
"2026-03-01 - Titel.pdf", LocalDate.of(2026, 3, 1), "gut", null, Duration.ofMillis(20)));
|
||||||
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
||||||
"b.pdf", DUMMY_FP, DocumentCompletionStatus.FAILED_PERMANENT,
|
"b.pdf", DUMMY_FP, DocumentCompletionStatus.FAILED_PERMANENT,
|
||||||
null, null, null, Duration.ofMillis(10)));
|
null, null, null, null, Duration.ofMillis(10)));
|
||||||
observer.onRunEnded(new RunSummary(1, 1, 0));
|
observer.onRunEnded(new RunSummary(1, 1, 0));
|
||||||
return GuiBatchRunLaunchOutcome.completed();
|
return GuiBatchRunLaunchOutcome.completed();
|
||||||
};
|
};
|
||||||
@@ -119,7 +119,7 @@ class GuiBatchRunCoordinatorTest {
|
|||||||
observer.onRunStarted(new RunId("run-skip"), 1);
|
observer.onRunStarted(new RunId("run-skip"), 1);
|
||||||
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
||||||
"c.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED,
|
"c.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED,
|
||||||
null, null, null, Duration.ofMillis(5)));
|
null, null, null, null, Duration.ofMillis(5)));
|
||||||
observer.onRunEnded(new RunSummary(0, 0, 1));
|
observer.onRunEnded(new RunSummary(0, 0, 1));
|
||||||
return GuiBatchRunLaunchOutcome.completed();
|
return GuiBatchRunLaunchOutcome.completed();
|
||||||
};
|
};
|
||||||
@@ -317,7 +317,7 @@ class GuiBatchRunCoordinatorTest {
|
|||||||
|
|
||||||
private static GuiBatchRunResultRow row(DocumentCompletionStatus status) {
|
private static GuiBatchRunResultRow row(DocumentCompletionStatus status) {
|
||||||
return new GuiBatchRunResultRow(
|
return new GuiBatchRunResultRow(
|
||||||
"x.pdf", DUMMY_FP, status, null, null, null, Duration.ofMillis(1));
|
"x.pdf", DUMMY_FP, status, null, null, null, null, Duration.ofMillis(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GuiBatchRunCoordinator.Listener noOpListener() {
|
private static GuiBatchRunCoordinator.Listener noOpListener() {
|
||||||
@@ -358,7 +358,7 @@ class GuiBatchRunCoordinatorTest {
|
|||||||
BatchRunProgressObserver noOp = BatchRunProgressObserver.noOp();
|
BatchRunProgressObserver noOp = BatchRunProgressObserver.noOp();
|
||||||
noOp.onRunStarted(new RunId("x"), 0);
|
noOp.onRunStarted(new RunId("x"), 0);
|
||||||
noOp.onDocumentCompleted(new DocumentCompletionEvent(
|
noOp.onDocumentCompleted(new DocumentCompletionEvent(
|
||||||
"a.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS, null, null, null, Duration.ZERO));
|
"a.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS, null, null, null, null, Duration.ZERO));
|
||||||
noOp.onRunEnded(new RunSummary(0, 0, 0));
|
noOp.onRunEnded(new RunSummary(0, 0, 0));
|
||||||
assertSame(noOp, BatchRunProgressObserver.noOp());
|
assertSame(noOp, BatchRunProgressObserver.noOp());
|
||||||
assertFalse(BatchRunCancellationToken.neverCancelled().isCancellationRequested());
|
assertFalse(BatchRunCancellationToken.neverCancelled().isCancellationRequested());
|
||||||
@@ -370,12 +370,12 @@ class GuiBatchRunCoordinatorTest {
|
|||||||
void resultRow_rejectsInvalidInput() {
|
void resultRow_rejectsInvalidInput() {
|
||||||
try {
|
try {
|
||||||
new GuiBatchRunResultRow(" ", DUMMY_FP, DocumentCompletionStatus.SUCCESS,
|
new GuiBatchRunResultRow(" ", DUMMY_FP, DocumentCompletionStatus.SUCCESS,
|
||||||
null, null, null, Duration.ZERO);
|
null, null, null, null, Duration.ZERO);
|
||||||
throw new AssertionError("expected IllegalArgumentException");
|
throw new AssertionError("expected IllegalArgumentException");
|
||||||
} catch (IllegalArgumentException expected) { /* ok */ }
|
} catch (IllegalArgumentException expected) { /* ok */ }
|
||||||
try {
|
try {
|
||||||
new GuiBatchRunResultRow("x.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS,
|
new GuiBatchRunResultRow("x.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS,
|
||||||
null, null, null, Duration.ofSeconds(-1));
|
null, null, null, null, Duration.ofSeconds(-1));
|
||||||
throw new AssertionError("expected IllegalArgumentException");
|
throw new AssertionError("expected IllegalArgumentException");
|
||||||
} catch (IllegalArgumentException expected) { /* ok */ }
|
} catch (IllegalArgumentException expected) { /* ok */ }
|
||||||
}
|
}
|
||||||
@@ -384,9 +384,10 @@ class GuiBatchRunCoordinatorTest {
|
|||||||
void resultRow_optionalHoldersNormaliseNullToEmpty() {
|
void resultRow_optionalHoldersNormaliseNullToEmpty() {
|
||||||
GuiBatchRunResultRow row = new GuiBatchRunResultRow(
|
GuiBatchRunResultRow row = new GuiBatchRunResultRow(
|
||||||
"x.pdf", DUMMY_FP, DocumentCompletionStatus.FAILED_PERMANENT,
|
"x.pdf", DUMMY_FP, DocumentCompletionStatus.FAILED_PERMANENT,
|
||||||
null, null, null, Duration.ZERO);
|
null, null, null, null, Duration.ZERO);
|
||||||
assertNull(row.finalFileName().orElse(null));
|
assertNull(row.finalFileName().orElse(null));
|
||||||
assertNull(row.resolvedDate().orElse(null));
|
assertNull(row.resolvedDate().orElse(null));
|
||||||
assertNull(row.aiReasoning().orElse(null));
|
assertNull(row.aiReasoning().orElse(null));
|
||||||
|
assertNull(row.aiFailureMessage().orElse(null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-8
@@ -35,6 +35,7 @@ class GuiBatchRunResultRowTest {
|
|||||||
Optional.of("2026-01-01 - Titel.pdf"),
|
Optional.of("2026-01-01 - Titel.pdf"),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
Duration.ofMillis(100));
|
Duration.ofMillis(100));
|
||||||
assertEquals("test.pdf", row.originalFileName());
|
assertEquals("test.pdf", row.originalFileName());
|
||||||
assertEquals(FP, row.fingerprint());
|
assertEquals(FP, row.fingerprint());
|
||||||
@@ -46,45 +47,46 @@ class GuiBatchRunResultRowTest {
|
|||||||
void construction_nullOriginalFileName_throws() {
|
void construction_nullOriginalFileName_throws() {
|
||||||
assertThrows(NullPointerException.class, () ->
|
assertThrows(NullPointerException.class, () ->
|
||||||
new GuiBatchRunResultRow(null, FP, DocumentCompletionStatus.SUCCESS,
|
new GuiBatchRunResultRow(null, FP, DocumentCompletionStatus.SUCCESS,
|
||||||
null, null, null, Duration.ZERO));
|
null, null, null, null, Duration.ZERO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void construction_blankOriginalFileName_throws() {
|
void construction_blankOriginalFileName_throws() {
|
||||||
assertThrows(IllegalArgumentException.class, () ->
|
assertThrows(IllegalArgumentException.class, () ->
|
||||||
new GuiBatchRunResultRow(" ", FP, DocumentCompletionStatus.SUCCESS,
|
new GuiBatchRunResultRow(" ", FP, DocumentCompletionStatus.SUCCESS,
|
||||||
null, null, null, Duration.ZERO));
|
null, null, null, null, Duration.ZERO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void construction_nullFingerprint_throws() {
|
void construction_nullFingerprint_throws() {
|
||||||
assertThrows(NullPointerException.class, () ->
|
assertThrows(NullPointerException.class, () ->
|
||||||
new GuiBatchRunResultRow("test.pdf", null, DocumentCompletionStatus.SUCCESS,
|
new GuiBatchRunResultRow("test.pdf", null, DocumentCompletionStatus.SUCCESS,
|
||||||
null, null, null, Duration.ZERO));
|
null, null, null, null, Duration.ZERO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void construction_nullStatus_throws() {
|
void construction_nullStatus_throws() {
|
||||||
assertThrows(NullPointerException.class, () ->
|
assertThrows(NullPointerException.class, () ->
|
||||||
new GuiBatchRunResultRow("test.pdf", FP, null,
|
new GuiBatchRunResultRow("test.pdf", FP, null,
|
||||||
null, null, null, Duration.ZERO));
|
null, null, null, null, Duration.ZERO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void construction_negativeDuration_throws() {
|
void construction_negativeDuration_throws() {
|
||||||
assertThrows(IllegalArgumentException.class, () ->
|
assertThrows(IllegalArgumentException.class, () ->
|
||||||
new GuiBatchRunResultRow("test.pdf", FP, DocumentCompletionStatus.SUCCESS,
|
new GuiBatchRunResultRow("test.pdf", FP, DocumentCompletionStatus.SUCCESS,
|
||||||
null, null, null, Duration.ofSeconds(-1)));
|
null, null, null, null, Duration.ofSeconds(-1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void construction_nullOptionals_normalisedToEmpty() {
|
void construction_nullOptionals_normalisedToEmpty() {
|
||||||
GuiBatchRunResultRow row = new GuiBatchRunResultRow(
|
GuiBatchRunResultRow row = new GuiBatchRunResultRow(
|
||||||
"test.pdf", FP, DocumentCompletionStatus.FAILED_PERMANENT,
|
"test.pdf", FP, DocumentCompletionStatus.FAILED_PERMANENT,
|
||||||
null, null, null, Duration.ZERO);
|
null, null, null, null, Duration.ZERO);
|
||||||
assertTrue(row.finalFileName().isEmpty());
|
assertTrue(row.finalFileName().isEmpty());
|
||||||
assertTrue(row.resolvedDate().isEmpty());
|
assertTrue(row.resolvedDate().isEmpty());
|
||||||
assertTrue(row.aiReasoning().isEmpty());
|
assertTrue(row.aiReasoning().isEmpty());
|
||||||
|
assertTrue(row.aiFailureMessage().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -144,12 +146,13 @@ class GuiBatchRunResultRowTest {
|
|||||||
GuiBatchRunResultRow original = new GuiBatchRunResultRow(
|
GuiBatchRunResultRow original = new GuiBatchRunResultRow(
|
||||||
"test.pdf", FP, DocumentCompletionStatus.SUCCESS,
|
"test.pdf", FP, DocumentCompletionStatus.SUCCESS,
|
||||||
Optional.of("2026-01-01 - Titel.pdf"), Optional.empty(), Optional.empty(),
|
Optional.of("2026-01-01 - Titel.pdf"), Optional.empty(), Optional.empty(),
|
||||||
Duration.ofMillis(42));
|
Optional.empty(), Duration.ofMillis(42));
|
||||||
GuiBatchRunResultRow marker = GuiBatchRunResultRow.resetMarker(original);
|
GuiBatchRunResultRow marker = GuiBatchRunResultRow.resetMarker(original);
|
||||||
|
|
||||||
assertTrue(marker.finalFileName().isEmpty());
|
assertTrue(marker.finalFileName().isEmpty());
|
||||||
assertTrue(marker.resolvedDate().isEmpty());
|
assertTrue(marker.resolvedDate().isEmpty());
|
||||||
assertTrue(marker.aiReasoning().isEmpty());
|
assertTrue(marker.aiReasoning().isEmpty());
|
||||||
|
assertTrue(marker.aiFailureMessage().isEmpty());
|
||||||
assertEquals(Duration.ZERO, marker.processingDuration());
|
assertEquals(Duration.ZERO, marker.processingDuration());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,12 +176,33 @@ class GuiBatchRunResultRowTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// aiFailureMessage
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void construction_withFailureMessage_isPresent() {
|
||||||
|
GuiBatchRunResultRow row = new GuiBatchRunResultRow(
|
||||||
|
"test.pdf", FP, DocumentCompletionStatus.FAILED_RETRYABLE,
|
||||||
|
null, null, null, Optional.of("KI-Timeout"), Duration.ofMillis(10));
|
||||||
|
assertTrue(row.aiFailureMessage().isPresent());
|
||||||
|
assertEquals("KI-Timeout", row.aiFailureMessage().get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void construction_withoutFailureMessage_isEmpty() {
|
||||||
|
GuiBatchRunResultRow row = new GuiBatchRunResultRow(
|
||||||
|
"test.pdf", FP, DocumentCompletionStatus.SUCCESS,
|
||||||
|
null, null, null, Optional.empty(), Duration.ofMillis(10));
|
||||||
|
assertTrue(row.aiFailureMessage().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Helper
|
// Helper
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
private static GuiBatchRunResultRow row(DocumentCompletionStatus status) {
|
private static GuiBatchRunResultRow row(DocumentCompletionStatus status) {
|
||||||
return new GuiBatchRunResultRow(
|
return new GuiBatchRunResultRow(
|
||||||
"file.pdf", FP, status, null, null, null, Duration.ofMillis(1));
|
"file.pdf", FP, status, null, null, null, null, Duration.ofMillis(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -188,6 +188,7 @@ class GuiBatchRunTabSelectionSmokeTest {
|
|||||||
"2026-01-01 - Titel.pdf",
|
"2026-01-01 - Titel.pdf",
|
||||||
java.time.LocalDate.of(2026, 1, 1),
|
java.time.LocalDate.of(2026, 1, 1),
|
||||||
"reasoning",
|
"reasoning",
|
||||||
|
null,
|
||||||
Duration.ofMillis(5)));
|
Duration.ofMillis(5)));
|
||||||
observer.onRunEnded(new RunSummary(1, 0, 0));
|
observer.onRunEnded(new RunSummary(1, 0, 0));
|
||||||
return GuiBatchRunLaunchOutcome.completed();
|
return GuiBatchRunLaunchOutcome.completed();
|
||||||
@@ -286,7 +287,7 @@ class GuiBatchRunTabSelectionSmokeTest {
|
|||||||
private static GuiBatchRunResultRow row(String name, DocumentFingerprint fp,
|
private static GuiBatchRunResultRow row(String name, DocumentFingerprint fp,
|
||||||
DocumentCompletionStatus status) {
|
DocumentCompletionStatus status) {
|
||||||
return new GuiBatchRunResultRow(name, fp, status,
|
return new GuiBatchRunResultRow(name, fp, status,
|
||||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ofMillis(1));
|
Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Duration.ofMillis(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void runOnFx(Runnable action) throws InterruptedException {
|
private void runOnFx(Runnable action) throws InterruptedException {
|
||||||
|
|||||||
+3
-2
@@ -101,13 +101,14 @@ class GuiBatchRunTabSmokeTest {
|
|||||||
"2026-03-01 - Titel.pdf",
|
"2026-03-01 - Titel.pdf",
|
||||||
LocalDate.of(2026, 3, 1),
|
LocalDate.of(2026, 3, 1),
|
||||||
"gut begründet",
|
"gut begründet",
|
||||||
|
null,
|
||||||
Duration.ofMillis(42)));
|
Duration.ofMillis(42)));
|
||||||
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
||||||
"b.pdf", DUMMY_FP, DocumentCompletionStatus.FAILED_RETRYABLE,
|
"b.pdf", DUMMY_FP, DocumentCompletionStatus.FAILED_RETRYABLE,
|
||||||
null, null, null, Duration.ofMillis(10)));
|
null, null, null, null, Duration.ofMillis(10)));
|
||||||
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
observer.onDocumentCompleted(new DocumentCompletionEvent(
|
||||||
"c.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED,
|
"c.pdf", DUMMY_FP, DocumentCompletionStatus.SKIPPED,
|
||||||
null, null, null, Duration.ofMillis(5)));
|
null, null, null, null, Duration.ofMillis(5)));
|
||||||
observer.onRunEnded(new RunSummary(1, 1, 1));
|
observer.onRunEnded(new RunSummary(1, 1, 1));
|
||||||
return GuiBatchRunLaunchOutcome.completed();
|
return GuiBatchRunLaunchOutcome.completed();
|
||||||
};
|
};
|
||||||
|
|||||||
+4
@@ -31,6 +31,9 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
|
|||||||
* any is available for this candidate (may be present on success
|
* any is available for this candidate (may be present on success
|
||||||
* and on some failure paths where an AI call had previously
|
* and on some failure paths where an AI call had previously
|
||||||
* produced a reasoning); {@code null} when no reasoning exists
|
* 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
|
* @param processingDuration the wall-clock duration spent on this candidate in the current
|
||||||
* run; never {@code null} and never negative
|
* run; never {@code null} and never negative
|
||||||
*/
|
*/
|
||||||
@@ -41,6 +44,7 @@ public record DocumentCompletionEvent(
|
|||||||
String finalFileName,
|
String finalFileName,
|
||||||
LocalDate resolvedDate,
|
LocalDate resolvedDate,
|
||||||
String aiReasoning,
|
String aiReasoning,
|
||||||
|
String failureMessage,
|
||||||
Duration processingDuration) {
|
Duration processingDuration) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+27
-17
@@ -618,6 +618,7 @@ public class DocumentProcessingCoordinator {
|
|||||||
resolvedFilename,
|
resolvedFilename,
|
||||||
proposalAttempt.resolvedDate(),
|
proposalAttempt.resolvedDate(),
|
||||||
proposalAttempt.aiReasoning(),
|
proposalAttempt.aiReasoning(),
|
||||||
|
null,
|
||||||
attemptStart, now);
|
attemptStart, now);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -701,7 +702,7 @@ public class DocumentProcessingCoordinator {
|
|||||||
publishCompletion(candidate, fingerprint,
|
publishCompletion(candidate, fingerprint,
|
||||||
retryable ? DocumentCompletionStatus.FAILED_RETRYABLE
|
retryable ? DocumentCompletionStatus.FAILED_RETRYABLE
|
||||||
: DocumentCompletionStatus.FAILED_PERMANENT,
|
: DocumentCompletionStatus.FAILED_PERMANENT,
|
||||||
null, null, null, attemptStart, now);
|
null, null, null, errorMessage, attemptStart, now);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (DocumentPersistenceException persistEx) {
|
} catch (DocumentPersistenceException persistEx) {
|
||||||
@@ -771,7 +772,7 @@ public class DocumentProcessingCoordinator {
|
|||||||
transition.retryable()
|
transition.retryable()
|
||||||
? DocumentCompletionStatus.FAILED_RETRYABLE
|
? DocumentCompletionStatus.FAILED_RETRYABLE
|
||||||
: DocumentCompletionStatus.FAILED_PERMANENT,
|
: DocumentCompletionStatus.FAILED_PERMANENT,
|
||||||
null, null, reasoning, attemptStart, now);
|
null, null, reasoning, errorMessage, attemptStart, now);
|
||||||
|
|
||||||
if (!secondaryPersisted) {
|
if (!secondaryPersisted) {
|
||||||
logger.debug("Completion for '{}' reported without secondary persistence record.",
|
logger.debug("Completion for '{}' reported without secondary persistence record.",
|
||||||
@@ -815,7 +816,7 @@ public class DocumentProcessingCoordinator {
|
|||||||
logger.debug("Skip attempt #{} persisted for '{}' with status {}.",
|
logger.debug("Skip attempt #{} persisted for '{}' with status {}.",
|
||||||
attemptNumber, candidate.uniqueIdentifier(), skipStatus);
|
attemptNumber, candidate.uniqueIdentifier(), skipStatus);
|
||||||
publishCompletion(candidate, fingerprint, DocumentCompletionStatus.SKIPPED,
|
publishCompletion(candidate, fingerprint, DocumentCompletionStatus.SKIPPED,
|
||||||
null, null, null, attemptStart, now);
|
null, null, null, null, attemptStart, now);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (DocumentPersistenceException e) {
|
} catch (DocumentPersistenceException e) {
|
||||||
@@ -1080,12 +1081,17 @@ public class DocumentProcessingCoordinator {
|
|||||||
outcome.counters().contentErrorCount(),
|
outcome.counters().contentErrorCount(),
|
||||||
outcome.counters().transientErrorCount());
|
outcome.counters().transientErrorCount());
|
||||||
}
|
}
|
||||||
// Pipeline-path terminal resolutions are reported to the progress observer.
|
// Pipeline-Pfad: terminale Auflösungen werden dem Progress-Observer gemeldet.
|
||||||
// PROPOSAL_READY is an intermediate state; the subsequent finalisation publishes
|
// PROPOSAL_READY ist ein Zwischenstatus; die Finalisierung publiziert das eigentliche
|
||||||
// the actual completion event (SUCCESS or transient-error failure).
|
// Completion-Event (SUCCESS oder Fehler).
|
||||||
if (outcome.overallStatus() != ProcessingStatus.PROPOSAL_READY) {
|
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),
|
publishCompletion(candidate, fingerprint, toCompletionStatus(outcome),
|
||||||
null, null, null, attemptStart, now);
|
null, null, null, pipelineFailureMessage, attemptStart, now);
|
||||||
}
|
}
|
||||||
return true;
|
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
|
* Any runtime exception thrown by the observer is caught and logged at warn level and must
|
||||||
* not affect persistence or batch flow.
|
* not affect persistence or batch flow.
|
||||||
*
|
*
|
||||||
* @param candidate the candidate being reported; must not be null
|
* @param candidate die Kandidatdatei, über die berichtet wird; darf nicht null sein
|
||||||
* @param fingerprint the content-based identity of the document; must not be null
|
* @param fingerprint der inhaltsbasierte Fingerprint des Dokuments; darf nicht null sein
|
||||||
* @param status the aggregated completion status; must not be null
|
* @param status der aggregierte Abschlussstatus; darf nicht null sein
|
||||||
* @param finalFileName the final target filename on success; {@code null} otherwise
|
* @param finalFileName der finale Zieldateiname bei Erfolg; sonst {@code null}
|
||||||
* @param resolvedDate the resolved date on success; may be {@code null} otherwise
|
* @param resolvedDate das aufgelöste Datum bei Erfolg; bei Fehlern kann es {@code null} sein
|
||||||
* @param aiReasoning the AI reasoning when one is available for this result;
|
* @param aiReasoning das KI-Reasoning, wenn für dieses Ergebnis eines vorliegt;
|
||||||
* {@code null} otherwise
|
* sonst {@code null}
|
||||||
* @param startInstant the moment processing of the candidate began in this run
|
* @param failureMessage eine lesbare Fehlerbeschreibung bei Fehler-Status; bei Erfolg
|
||||||
* @param endInstant the moment the terminal resolution was reached
|
* 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(
|
private void publishCompletion(
|
||||||
SourceDocumentCandidate candidate,
|
SourceDocumentCandidate candidate,
|
||||||
@@ -1233,6 +1241,7 @@ public class DocumentProcessingCoordinator {
|
|||||||
String finalFileName,
|
String finalFileName,
|
||||||
LocalDate resolvedDate,
|
LocalDate resolvedDate,
|
||||||
String aiReasoning,
|
String aiReasoning,
|
||||||
|
String failureMessage,
|
||||||
Instant startInstant,
|
Instant startInstant,
|
||||||
Instant endInstant) {
|
Instant endInstant) {
|
||||||
Consumer<DocumentCompletionEvent> forwarder = completionForwarder;
|
Consumer<DocumentCompletionEvent> forwarder = completionForwarder;
|
||||||
@@ -1251,9 +1260,10 @@ public class DocumentProcessingCoordinator {
|
|||||||
finalFileName,
|
finalFileName,
|
||||||
resolvedDate,
|
resolvedDate,
|
||||||
aiReasoning,
|
aiReasoning,
|
||||||
|
failureMessage,
|
||||||
duration));
|
duration));
|
||||||
} catch (RuntimeException forwarderFailure) {
|
} 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);
|
candidate.uniqueIdentifier(), forwarderFailure.getMessage(), forwarderFailure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -85,13 +85,13 @@ class BatchRunProgressObservationTest {
|
|||||||
@Test
|
@Test
|
||||||
void documentCompletionEvent_rejectsBlankFilename() {
|
void documentCompletionEvent_rejectsBlankFilename() {
|
||||||
assertThrows(IllegalArgumentException.class, () -> new DocumentCompletionEvent(
|
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
|
@Test
|
||||||
void documentCompletionEvent_rejectsNegativeDuration() {
|
void documentCompletionEvent_rejectsNegativeDuration() {
|
||||||
assertThrows(IllegalArgumentException.class, () -> new DocumentCompletionEvent(
|
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)));
|
Duration.ofSeconds(-1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ class BatchRunProgressObservationTest {
|
|||||||
void documentCompletionEvent_carriesOptionalFields() {
|
void documentCompletionEvent_carriesOptionalFields() {
|
||||||
DocumentCompletionEvent event = new DocumentCompletionEvent(
|
DocumentCompletionEvent event = new DocumentCompletionEvent(
|
||||||
"x.pdf", DUMMY_FP, DocumentCompletionStatus.SUCCESS, "2026-03-01 - Titel.pdf",
|
"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("x.pdf", event.originalFileName());
|
||||||
assertEquals(DocumentCompletionStatus.SUCCESS, event.status());
|
assertEquals(DocumentCompletionStatus.SUCCESS, event.status());
|
||||||
@@ -129,7 +129,7 @@ class BatchRunProgressObservationTest {
|
|||||||
assertSame(a, b);
|
assertSame(a, b);
|
||||||
a.onRunStarted(new RunId("r-1"), 5);
|
a.onRunStarted(new RunId("r-1"), 5);
|
||||||
a.onDocumentCompleted(new DocumentCompletionEvent(
|
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));
|
a.onRunEnded(new RunSummary(0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,7 +438,7 @@ class BatchRunProgressObservationTest {
|
|||||||
candidate.uniqueIdentifier(),
|
candidate.uniqueIdentifier(),
|
||||||
fingerprint,
|
fingerprint,
|
||||||
statuses.get(index),
|
statuses.get(index),
|
||||||
null, null, null, Duration.ofMillis(10)));
|
null, null, null, null, Duration.ofMillis(10)));
|
||||||
}
|
}
|
||||||
onBeforeReturn.run();
|
onBeforeReturn.run();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in New Issue
Block a user