diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/BatchRunSummaryBanner.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/BatchRunSummaryBanner.java
new file mode 100644
index 0000000..b8961aa
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/BatchRunSummaryBanner.java
@@ -0,0 +1,202 @@
+package de.gecheckt.pdf.umbenenner.adapter.in.gui.batchrun;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
+import javafx.application.Platform;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+
+/**
+ * Einzeilige Zusammenfassungsleiste, die nach Abschluss eines Verarbeitungslaufs
+ * die aggregierten Ergebnisse anzeigt.
+ *
+ *
Das Banner erscheint nach Laufabschluss unterhalb des Fortschrittsbalkens und
+ * oberhalb der Ergebnistabelle. Es zeigt nur Kategorien, deren Zähler größer als null
+ * ist. Folgende Status werden nicht gezählt und tauchen nie im Banner auf:
+ * {@code READY_FOR_AI}, {@code PROPOSAL_READY} und {@code PROCESSING} sind im
+ * Enum {@link DocumentCompletionStatus} nicht enthalten – alle enthaltenen Werte
+ * werden gezählt, außer Einträgen mit {@code resetPending=true}, da diese keinen
+ * abgeschlossenen Zustand darstellen.
+ *
+ *
Farbe ist niemals das einzige Unterscheidungsmerkmal: Jedes Segment enthält
+ * ein Icon und einen Text.
+ *
+ *
Die öffentlichen Methoden {@link #clear()} und {@link #update(Map)} sind
+ * thread-agnostisch definiert, aber müssen auf dem JavaFX Application Thread aufgerufen
+ * werden (oder das Banner muss via {@code Platform.runLater} aktualisiert werden).
+ * Die Aggregations-Hilfsmethode {@link #aggregateCounts(Iterable)} ist vollständig
+ * unabhängig von JavaFX und kann auf jedem Thread aufgerufen werden.
+ */
+public final class BatchRunSummaryBanner {
+
+ /** Trennzeichen zwischen den Kategoriesegmenten. */
+ private static final String SEGMENT_SEPARATOR = " · ";
+
+ /** Abstand zwischen den Label-Segmenten in Pixeln. */
+ private static final int SPACING = 0;
+
+ /** Innerer Abstand des Containers in Pixeln (oben/unten). */
+ private static final double PADDING_V = 4.0;
+
+ /** Standardfarbe für den Summentext. */
+ private static final String STYLE_DEFAULT = "-fx-font-size: 12;";
+
+ /**
+ * Alle {@link DocumentCompletionStatus}-Werte, die im Banner angezeigt werden,
+ * in der verbindlichen Anzeigereihenfolge gemäß Spezifikation.
+ */
+ private static final List DISPLAYED_ORDER = List.of(
+ DocumentCompletionStatus.SUCCESS,
+ DocumentCompletionStatus.FAILED_RETRYABLE,
+ DocumentCompletionStatus.FAILED_PERMANENT,
+ DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED,
+ DocumentCompletionStatus.SKIPPED_FINAL_FAILURE
+ );
+
+ /** Wurzel-Container des Banners – wird in das Tab-Layout eingebettet. */
+ private final HBox container;
+
+ /** Label, das den kompletten Bannertext als Inline-Segmente trägt. */
+ private final Label contentLabel;
+
+ /**
+ * Erstellt ein neues, initial unsichtbares Summary-Banner.
+ */
+ public BatchRunSummaryBanner() {
+ contentLabel = new Label();
+ contentLabel.setStyle(STYLE_DEFAULT);
+ contentLabel.setWrapText(false);
+
+ container = new HBox(SPACING, contentLabel);
+ container.setAlignment(Pos.CENTER_LEFT);
+ container.setStyle("-fx-padding: " + PADDING_V + " 0 " + PADDING_V + " 0;");
+
+ // Initial unsichtbar, nimmt keinen Platz ein
+ container.setVisible(false);
+ container.setManaged(false);
+ }
+
+ // -------------------------------------------------------------------------
+ // Öffentliche API
+ // -------------------------------------------------------------------------
+
+ /**
+ * Versteckt das Banner und leert seinen Inhalt.
+ *
+ * Muss auf dem JavaFX Application Thread aufgerufen werden.
+ */
+ public void clear() {
+ contentLabel.setText("");
+ container.setVisible(false);
+ container.setManaged(false);
+ }
+
+ /**
+ * Aktualisiert das Banner mit den aggregierten Zählern und macht es sichtbar.
+ *
+ *
Zeigt nur Kategorien mit Anzahl > 0. Wenn alle Zähler null sind (leerer Lauf),
+ * wird das Banner versteckt.
+ *
+ *
Muss auf dem JavaFX Application Thread aufgerufen werden.
+ *
+ * @param counts Zuordnung von Verarbeitungsstatus zu Anzahl;
+ * fehlende Status werden als 0 interpretiert; darf nicht null sein
+ */
+ public void update(Map counts) {
+ Objects.requireNonNull(counts, "counts darf nicht null sein");
+
+ String text = buildBannerText(counts);
+ if (text.isEmpty()) {
+ clear();
+ return;
+ }
+
+ contentLabel.setText(text);
+ container.setVisible(true);
+ container.setManaged(true);
+ }
+
+ /**
+ * Liefert den JavaFX-Container-Knoten zum Einbetten in das Tab-Layout.
+ *
+ * @return der Container-Knoten; nie null
+ */
+ public HBox getNode() {
+ return container;
+ }
+
+ // -------------------------------------------------------------------------
+ // Aggregations-Hilfe (thread-agnostisch, testbar ohne JavaFX)
+ // -------------------------------------------------------------------------
+
+ /**
+ * Zählt die Anzahl jedes {@link DocumentCompletionStatus} in der übergebenen
+ * Iterable. Einträge mit {@code resetPending=true} werden ignoriert, da sie
+ * keinen abgeschlossenen Verarbeitungszustand darstellen.
+ *
+ * Diese Methode ist vollständig unabhängig von JavaFX und kann auf jedem
+ * Thread aufgerufen werden.
+ *
+ * @param rows die Ergebniszeilen des Laufs; darf nicht null sein;
+ * null-Elemente werden übersprungen
+ * @return eine Map mit der Anzahl je Status; enthält alle anzuzeigenden
+ * Status (fehlende haben Wert 0); nie null
+ */
+ public static Map aggregateCounts(
+ Iterable extends GuiBatchRunResultRow> rows) {
+ Objects.requireNonNull(rows, "rows darf nicht null sein");
+
+ Map counts = new EnumMap<>(DocumentCompletionStatus.class);
+ // Alle anzuzeigenden Status mit 0 vorbelegen
+ for (DocumentCompletionStatus status : DISPLAYED_ORDER) {
+ counts.put(status, 0);
+ }
+
+ for (GuiBatchRunResultRow row : rows) {
+ if (row == null) {
+ continue;
+ }
+ // Reset-Pending-Zeilen zählen nicht – sie haben noch keinen abgeschlossenen Status
+ if (row.resetPending()) {
+ continue;
+ }
+ DocumentCompletionStatus status = row.status();
+ // Nur anzuzeigende Status zählen (entspricht dem Ausschluss von
+ // Übergangszuständen wie READY_FOR_AI, PROPOSAL_READY, PROCESSING)
+ if (counts.containsKey(status)) {
+ counts.merge(status, 1, Integer::sum);
+ }
+ }
+ return counts;
+ }
+
+ // -------------------------------------------------------------------------
+ // Interne Hilfsmethoden
+ // -------------------------------------------------------------------------
+
+ /**
+ * Erzeugt den angezeigten Bannertext aus den Zählern.
+ * Liefert einen leeren String wenn alle Zähler null sind.
+ *
+ * @param counts die Zähler je Status; darf nicht null sein
+ * @return der fertige Bannertext oder ein leerer String
+ */
+ static String buildBannerText(Map counts) {
+ List segments = new ArrayList<>();
+ for (DocumentCompletionStatus status : DISPLAYED_ORDER) {
+ int count = counts.getOrDefault(status, 0);
+ if (count > 0) {
+ String icon = ProcessingStatusPresentation.iconFor(status);
+ String category = ProcessingStatusPresentation.summaryCategoryFor(status);
+ segments.add(icon + " " + count + " " + category);
+ }
+ }
+ return String.join(SEGMENT_SEPARATOR, segments);
+ }
+}
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 c2ed63e..dc47c96 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
@@ -199,6 +199,9 @@ public final class GuiBatchRunTab {
/** PDF-Vorschau-Komponente im Detailbereich. */
private final PdfPreviewPane pdfPreview = new PdfPreviewPane();
+ /** Summary-Banner unterhalb des Fortschrittsbalkens – sichtbar nach Laufabschluss. */
+ private final BatchRunSummaryBanner summaryBanner = new BatchRunSummaryBanner();
+
private final Supplier configPathSupplier;
private final BooleanSupplier savedConfigurationReadyCheck;
private final Runnable onRunStateChanged;
@@ -501,8 +504,14 @@ public final class GuiBatchRunTab {
HBox.setHgrow(progressBar, Priority.ALWAYS);
counterLabel.setId("batch-run-counter");
- HBox header = new HBox(SECONDARY_SPACING, progressBar, counterLabel);
- header.setAlignment(Pos.CENTER_LEFT);
+ HBox progressRow = new HBox(SECONDARY_SPACING, progressBar, counterLabel);
+ progressRow.setAlignment(Pos.CENTER_LEFT);
+
+ // Summary-Banner unterhalb des Fortschrittsbalkens, oberhalb der Tabelle
+ HBox bannerNode = summaryBanner.getNode();
+ bannerNode.setId("batch-run-summary-banner");
+
+ VBox header = new VBox(0, progressRow, bannerNode);
header.setPadding(new Insets(0, 0, SECONDARY_SPACING, 0));
return header;
}
@@ -1187,6 +1196,7 @@ public final class GuiBatchRunTab {
messageArea.setVisible(false);
messageArea.setManaged(false);
messageArea.setStyle(null);
+ summaryBanner.clear();
resetMetrics();
updateCounterLabel();
progressBar.setProgress(0);
@@ -1580,6 +1590,10 @@ public final class GuiBatchRunTab {
miniRunCompletedFingerprints = new HashSet<>();
}
selectedRows.clear();
+ // Summary-Banner aus der aktuellen Ergebnisliste aggregieren und anzeigen
+ Map counts =
+ BatchRunSummaryBanner.aggregateCounts(resultItems);
+ summaryBanner.update(counts);
appendSummary(outcome);
updateButtonStates();
notifyRunStateChanged();
diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/BatchRunSummaryBannerTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/BatchRunSummaryBannerTest.java
new file mode 100644
index 0000000..777c865
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/batchrun/BatchRunSummaryBannerTest.java
@@ -0,0 +1,233 @@
+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.assertTrue;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import de.gecheckt.pdf.umbenenner.application.port.in.DocumentCompletionStatus;
+import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
+
+/**
+ * Unit-Tests für {@link BatchRunSummaryBanner}.
+ *
+ * Geprüft werden die Aggregationslogik und die Textgenerierung unabhängig von JavaFX.
+ * Die GUI-Integrationsmethoden ({@code clear()}, {@code update()}, {@code getNode()})
+ * erfordern eine JavaFX-Runtime und werden durch Smoke-Tests abgedeckt.
+ */
+class BatchRunSummaryBannerTest {
+
+ // -------------------------------------------------------------------------
+ // Hilfsmethoden für Testdaten
+ // -------------------------------------------------------------------------
+
+ private static GuiBatchRunResultRow row(DocumentCompletionStatus status) {
+ return new GuiBatchRunResultRow(
+ "test.pdf",
+ new DocumentFingerprint("a".repeat(64)),
+ status,
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ Duration.ZERO,
+ false,
+ Optional.empty());
+ }
+
+ private static GuiBatchRunResultRow resetPendingRow() {
+ GuiBatchRunResultRow base = row(DocumentCompletionStatus.SUCCESS);
+ return GuiBatchRunResultRow.resetMarker(base);
+ }
+
+ // -------------------------------------------------------------------------
+ // aggregateCounts
+ // -------------------------------------------------------------------------
+
+ @Test
+ void aggregateCounts_leereListe_alleZaehlerNull() {
+ Map counts =
+ BatchRunSummaryBanner.aggregateCounts(Collections.emptyList());
+
+ assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.SUCCESS, 0));
+ assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.FAILED_RETRYABLE, 0));
+ assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.FAILED_PERMANENT, 0));
+ assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0));
+ assertEquals(0, counts.getOrDefault(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 0));
+ }
+
+ @Test
+ void aggregateCounts_nurErfolgreiche_zaehltNurSuccess() {
+ List rows = List.of(
+ row(DocumentCompletionStatus.SUCCESS),
+ row(DocumentCompletionStatus.SUCCESS),
+ row(DocumentCompletionStatus.SUCCESS));
+
+ Map counts =
+ BatchRunSummaryBanner.aggregateCounts(rows);
+
+ assertEquals(3, counts.get(DocumentCompletionStatus.SUCCESS));
+ assertEquals(0, counts.get(DocumentCompletionStatus.FAILED_RETRYABLE));
+ assertEquals(0, counts.get(DocumentCompletionStatus.FAILED_PERMANENT));
+ assertEquals(0, counts.get(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED));
+ assertEquals(0, counts.get(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE));
+ }
+
+ @Test
+ void aggregateCounts_gemischterLauf_alleKategorienKorrekt() {
+ List rows = List.of(
+ row(DocumentCompletionStatus.SUCCESS),
+ row(DocumentCompletionStatus.SUCCESS),
+ row(DocumentCompletionStatus.FAILED_RETRYABLE),
+ row(DocumentCompletionStatus.FAILED_PERMANENT),
+ row(DocumentCompletionStatus.FAILED_PERMANENT),
+ row(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED),
+ row(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED),
+ row(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED),
+ row(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE));
+
+ Map counts =
+ BatchRunSummaryBanner.aggregateCounts(rows);
+
+ assertEquals(2, counts.get(DocumentCompletionStatus.SUCCESS));
+ assertEquals(1, counts.get(DocumentCompletionStatus.FAILED_RETRYABLE));
+ assertEquals(2, counts.get(DocumentCompletionStatus.FAILED_PERMANENT));
+ assertEquals(3, counts.get(DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED));
+ assertEquals(1, counts.get(DocumentCompletionStatus.SKIPPED_FINAL_FAILURE));
+ }
+
+ @Test
+ void aggregateCounts_resetPendingZeilenWerdenNichtGezaehlt() {
+ // Reset-Pending-Zeilen haben noch keinen abgeschlossenen Status und
+ // dürfen nicht ins Summary einfließen
+ List rows = List.of(
+ row(DocumentCompletionStatus.SUCCESS),
+ resetPendingRow(),
+ resetPendingRow());
+
+ Map counts =
+ BatchRunSummaryBanner.aggregateCounts(rows);
+
+ assertEquals(1, counts.get(DocumentCompletionStatus.SUCCESS));
+ assertEquals(0, counts.get(DocumentCompletionStatus.FAILED_RETRYABLE));
+ assertEquals(0, counts.get(DocumentCompletionStatus.FAILED_PERMANENT));
+ }
+
+ // -------------------------------------------------------------------------
+ // buildBannerText
+ // -------------------------------------------------------------------------
+
+ @Test
+ void buildBannerText_alleZaehlerNull_leerString() {
+ Map counts = Map.of(
+ DocumentCompletionStatus.SUCCESS, 0,
+ DocumentCompletionStatus.FAILED_RETRYABLE, 0,
+ DocumentCompletionStatus.FAILED_PERMANENT, 0,
+ DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0,
+ DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 0);
+
+ String text = BatchRunSummaryBanner.buildBannerText(counts);
+
+ assertTrue(text.isEmpty(), "Leere Zähler ergeben leeren Text: '" + text + "'");
+ }
+
+ @Test
+ void buildBannerText_nurErfolgreiche_nurSuccessSegment() {
+ Map counts = Map.of(
+ DocumentCompletionStatus.SUCCESS, 17,
+ DocumentCompletionStatus.FAILED_RETRYABLE, 0,
+ DocumentCompletionStatus.FAILED_PERMANENT, 0,
+ DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0,
+ DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 0);
+
+ String text = BatchRunSummaryBanner.buildBannerText(counts);
+
+ assertTrue(text.contains("17"), "Anzahl 17 muss im Text erscheinen: " + text);
+ assertTrue(text.contains("erfolgreich"), "Kategorie 'erfolgreich' muss erscheinen: " + text);
+ assertTrue(text.contains("✓"), "Icon ✓ muss erscheinen: " + text);
+ assertFalse(text.contains("↻"), "Kein ↻ wenn FAILED_RETRYABLE = 0: " + text);
+ assertFalse(text.contains("×"), "Kein × wenn FAILED_PERMANENT = 0: " + text);
+ assertFalse(text.contains("≡"), "Kein ≡ wenn SKIPPED_ALREADY_PROCESSED = 0: " + text);
+ assertFalse(text.contains("⊘"), "Kein ⊘ wenn SKIPPED_FINAL_FAILURE = 0: " + text);
+ }
+
+ @Test
+ void buildBannerText_vollerLauf_alleSegmenteEnthalten() {
+ Map counts = Map.of(
+ DocumentCompletionStatus.SUCCESS, 14,
+ DocumentCompletionStatus.FAILED_RETRYABLE, 1,
+ DocumentCompletionStatus.FAILED_PERMANENT, 2,
+ DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 3,
+ DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 1);
+
+ String text = BatchRunSummaryBanner.buildBannerText(counts);
+
+ // Jedes Segment enthält Icon + Anzahl + Kategorie
+ assertTrue(text.contains("✓ 14 erfolgreich"), "SUCCESS-Segment: " + text);
+ assertTrue(text.contains("↻ 1 wird wiederholt"), "FAILED_RETRYABLE-Segment: " + text);
+ assertTrue(text.contains("× 2 fehlgeschlagen"), "FAILED_PERMANENT-Segment: " + text);
+ assertTrue(text.contains("≡ 3 übersprungen"), "SKIPPED_ALREADY_PROCESSED-Segment: " + text);
+ assertTrue(text.contains("⊘ 1 endgültig übersprungen"), "SKIPPED_FINAL_FAILURE-Segment: " + text);
+ }
+
+ @Test
+ void buildBannerText_nurSkippedFinalFailure_erscheintImBanner() {
+ // Sicherstellung: ⊘ erscheint auch wenn > 0, obwohl es die seltenste Kategorie ist
+ Map counts = Map.of(
+ DocumentCompletionStatus.SUCCESS, 0,
+ DocumentCompletionStatus.FAILED_RETRYABLE, 0,
+ DocumentCompletionStatus.FAILED_PERMANENT, 0,
+ DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0,
+ DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 2);
+
+ String text = BatchRunSummaryBanner.buildBannerText(counts);
+
+ assertTrue(text.contains("⊘"), "Icon ⊘ muss erscheinen: " + text);
+ assertTrue(text.contains("2"), "Anzahl 2 muss erscheinen: " + text);
+ assertTrue(text.contains("endgültig übersprungen"), "Kategorie muss erscheinen: " + text);
+ }
+
+ @Test
+ void buildBannerText_nurKategorienMitAnzahlGroesserNull_erscheinen() {
+ // Nur SUCCESS=5 ist gesetzt; alle anderen 0 → kein anderes Segment
+ Map counts = Map.of(
+ DocumentCompletionStatus.SUCCESS, 5,
+ DocumentCompletionStatus.FAILED_RETRYABLE, 0,
+ DocumentCompletionStatus.FAILED_PERMANENT, 0,
+ DocumentCompletionStatus.SKIPPED_ALREADY_PROCESSED, 0,
+ DocumentCompletionStatus.SKIPPED_FINAL_FAILURE, 0);
+
+ String text = BatchRunSummaryBanner.buildBannerText(counts);
+
+ // Kein Trennzeichen (·) darf erscheinen, wenn nur ein Segment vorhanden ist
+ assertFalse(text.contains("·"), "Kein Trenner bei einzelnem Segment: " + text);
+ assertTrue(text.contains("✓ 5 erfolgreich"), "Nur SUCCESS-Segment: " + text);
+ }
+
+ @Test
+ void aggregateCounts_kombinationMitResetPending_nurEchtAbgeschlosseneGezaehlt() {
+ // 2 SUCCESS + 1 FAILED_PERMANENT + 1 resetPending(SUCCESS) → nur 2+1 gezählt
+ List rows = List.of(
+ row(DocumentCompletionStatus.SUCCESS),
+ row(DocumentCompletionStatus.SUCCESS),
+ row(DocumentCompletionStatus.FAILED_PERMANENT),
+ resetPendingRow());
+
+ Map counts =
+ BatchRunSummaryBanner.aggregateCounts(rows);
+
+ assertEquals(2, counts.get(DocumentCompletionStatus.SUCCESS));
+ assertEquals(1, counts.get(DocumentCompletionStatus.FAILED_PERMANENT));
+ // Summe aller gezählten Einträge = 3, nicht 4
+ int total = counts.values().stream().mapToInt(Integer::intValue).sum();
+ assertEquals(3, total, "Reset-Pending darf nicht mitgezählt werden");
+ }
+}