V2.8 Fix: „Erneut verarbeiten" setzt DB-Status vor Mini-Lauf zurück

Das Problem: Der „Erneut verarbeiten"-Button startete einen Mini-Lauf,
ohne den DB-Status der selektierten Dateien zurückzusetzen. Dateien mit
FAILED_FINAL-Status wurden daher vom Use Case übersprungen.

Die Lösung:
1. Neue Methode startReprocessing() in GuiBatchRunCoordinator, die
   resetPort.reset() SYNCHRON vor dem Mini-Lauf aufruft.
2. handleReprocessSelected() in GuiBatchRunTab nutzt jetzt
   startReprocessing() statt startMiniRun() direkt.
3. Test-Fix: noOpReset muss die Fingerprints in der erfolgreich-zurückgesetzt-
   Liste enthalten, damit successCount() > 0 ist.

Spec-Konformität:
- Reset erfolgt synchron vor dem Worker-Thread-Start
- Keine neue Architektur-Verletzung
- Hexagonale Architektur bleibt sauber (Port/Adapter)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 14:02:45 +02:00
parent 9fd5bd5a52
commit b41b4112c4
12 changed files with 55 additions and 21 deletions
@@ -43,6 +43,8 @@ import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidation
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationSeverity; import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationSeverity;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService; import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
@@ -51,8 +53,6 @@ import javafx.scene.control.Alert;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label; import javafx.scene.control.Label;
@@ -70,7 +70,6 @@ import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyCombination;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
@@ -268,6 +268,48 @@ public final class GuiBatchRunCoordinator {
return startWorker(task); return startWorker(task);
} }
/**
* Starts a reprocessing operation: resets the database status of the specified
* fingerprints and immediately launches a targeted mini-run for them.
* <p>
* This method is the preferred entry point for "Erneut verarbeiten" (reprocess)
* actions in the GUI. It ensures that documents marked as FAILED_FINAL or otherwise
* ineligible for processing are reset before the mini-run begins, so they are
* reprocessed rather than skipped.
* <p>
* The reset executes synchronously on the caller's thread before the worker thread
* is started. This guarantees that the mini-run sees the documents in a
* reprocessable state.
*
* @param configFilePath the configuration file; must not be {@code null}
* @param fingerprintFilter the set of document fingerprints to reset and process;
* must not be {@code null}
* @return {@code true} when a new worker thread was started, {@code false} when a run
* was already in progress or when the reset failed for all fingerprints
* @throws NullPointerException if any argument is {@code null}
*/
public boolean startReprocessing(Path configFilePath,
Set<DocumentFingerprint> fingerprintFilter) {
Objects.requireNonNull(configFilePath, "configFilePath must not be null");
Objects.requireNonNull(fingerprintFilter, "fingerprintFilter must not be null");
if (isRunning()) {
return false;
}
// Reset the database status synchronously before starting the mini-run.
// This ensures that documents are not skipped due to FAILED_FINAL or other
// terminal states.
ResetDocumentStatusResult resetResult = resetPort.reset(configFilePath, fingerprintFilter);
if (resetResult.successCount() == 0) {
LOG.warn("GUI-Reprocessing: Reset für alle {} Dokumente fehlgeschlagen; "
+ "Mini-Lauf wird nicht gestartet.", fingerprintFilter.size());
return false;
}
LOG.info("GUI-Reprocessing: {} von {} Dokumenten erfolgreich zurückgesetzt.",
resetResult.successCount(), resetResult.requestedCount());
// Now start the mini-run with the reset fingerprints.
return startMiniRun(configFilePath, fingerprintFilter);
}
/** /**
* Starts a reset-only operation for the supplied fingerprint set. * Starts a reset-only operation for the supplied fingerprint set.
* <p> * <p>
@@ -4,7 +4,6 @@ import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -707,7 +706,10 @@ public final class GuiBatchRunTab {
// Mark selected rows as reset-pending immediately for visual feedback. // Mark selected rows as reset-pending immediately for visual feedback.
markSelectedRowsAsResetPending(); markSelectedRowsAsResetPending();
boolean started = coordinator.startMiniRun(configPath, snapshot); // Reset database status and start mini-run. The reset executes synchronously
// before the mini-run worker thread is started, ensuring documents are not
// skipped due to FAILED_FINAL status.
boolean started = coordinator.startReprocessing(configPath, snapshot);
if (!started) { if (!started) {
showMessage(ALREADY_RUNNING_HINT); showMessage(ALREADY_RUNNING_HINT);
return; return;
@@ -18,8 +18,6 @@ import java.util.function.Function;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent; import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus; import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult; import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
@@ -196,7 +196,10 @@ class GuiBatchRunTabSelectionSmokeTest {
GuiBatchRunLauncher noOpLauncher = (p, o, t) -> GuiBatchRunLauncher noOpLauncher = (p, o, t) ->
GuiBatchRunLaunchOutcome.rejected("not used"); GuiBatchRunLaunchOutcome.rejected("not used");
GuiResetDocumentStatusPort noOpReset = (p, fps) -> GuiResetDocumentStatusPort noOpReset = (p, fps) ->
new ResetDocumentStatusResult(fps.size(), Set.of(), Map.of()); // Return a successful reset for all fingerprints. The reset doesn't actually
// change anything in this test, but successCount() returns successfullyReset.size(),
// so we must return the fingerprints in the successfully reset set.
new ResetDocumentStatusResult(fps.size(), fps, Map.of());
CountDownLatch tabReady = new CountDownLatch(1); CountDownLatch tabReady = new CountDownLatch(1);
AtomicReferenceCapture<GuiBatchRunTab> tabRef = new AtomicReferenceCapture<>(); AtomicReferenceCapture<GuiBatchRunTab> tabRef = new AtomicReferenceCapture<>();
@@ -2,7 +2,6 @@ package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Path; import java.nio.file.Path;
@@ -6,6 +6,7 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort; import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
import de.gecheckt.pdf.umbenenner.application.service.AiResponseValidator.AiValidationResult;
import de.gecheckt.pdf.umbenenner.domain.model.AiErrorClassification; import de.gecheckt.pdf.umbenenner.domain.model.AiErrorClassification;
import de.gecheckt.pdf.umbenenner.domain.model.DateSource; import de.gecheckt.pdf.umbenenner.domain.model.DateSource;
import de.gecheckt.pdf.umbenenner.domain.model.NamingProposal; import de.gecheckt.pdf.umbenenner.domain.model.NamingProposal;
@@ -7,6 +7,7 @@ import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent; import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus; import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentKnownProcessable; import de.gecheckt.pdf.umbenenner.application.port.out.DocumentKnownProcessable;
@@ -12,14 +12,10 @@ import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionEvent;
import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
import de.gecheckt.pdf.umbenenner.application.port.in.RunSummary;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintPort; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintPort;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintResult; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintResult;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintSuccess; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintTechnicalError; import de.gecheckt.pdf.umbenenner.application.port.out.FingerprintTechnicalError;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort; import de.gecheckt.pdf.umbenenner.application.port.out.PdfTextExtractionPort;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger; import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingLogger;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort; import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
@@ -2,14 +2,11 @@ package de.gecheckt.pdf.umbenenner.application.usecase;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
@@ -17,7 +14,6 @@ import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -6,7 +6,6 @@ import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -24,8 +24,6 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationLoadException;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext; import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiStartupContext;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome; import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLaunchOutcome;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher; import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiBatchRunLauncher;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiMiniRunLauncher;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun.GuiResetDocumentStatusPort;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState; import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory; import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationFileSnapshot; import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationFileSnapshot;
@@ -56,7 +54,6 @@ import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult; import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResetDocumentStatusUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity; import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort; import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort; import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
@@ -77,6 +74,7 @@ import de.gecheckt.pdf.umbenenner.application.service.AiNamingService;
import de.gecheckt.pdf.umbenenner.application.service.AiResponseValidator; import de.gecheckt.pdf.umbenenner.application.service.AiResponseValidator;
import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator; import de.gecheckt.pdf.umbenenner.application.service.DocumentProcessingCoordinator;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase; import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultResetDocumentStatusUseCase;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator; import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService; import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator; import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator;