M13 vollständig abgeschlossen: V2.0-Freigabe (AP-001 bis AP-009)

- AP-001: Betriebs- und Startdokumentation für GUI und headless
  konsolidiert (betrieb.md, README.md)
- AP-002: Endbenutzer-Bedienanleitung gui-bedienanleitung.md angelegt
  (deskriptiv, 13 Kapitel, deutsch, Windows-Hinweise)
- AP-003: Konfigurationsbeispiele docs/examples/application.properties
  und docs/examples/prompt.txt konsolidiert, konsistent mit Standardvorlage
- AP-004: Regressionstests für headless Abwärtskompatibilität
  (JAR-Smoke-IT mit --config-Varianten und JavaFX-Freiheit)
- AP-005: GUI-Smoke-Tests für V2.0-Kernumfang vervollständigt
  (Startup-Notice-Sichtbarkeit im Header)
- AP-006: Build- und Packaging-Dokumentation im Abschnitt
  "Build und Packaging" in betrieb.md, README-Artefaktnamen korrigiert
- AP-007: Integrierte Gesamtprüfung durchgeführt, V2.0-Abschnitt in
  befundliste.md — keine Release-Blocker, zwei nicht blockierende
  Restpunkte (R1 ByteBuddy-Warning, R2 fehlender visueller GUI-Render-Test)
- AP-008: entfiel (keine Release-Blocker zu beheben)
- AP-009: Finale Gesamtprüfung, Freigabedokument docs/freigabe-v2_0.md
  mit Git-HEAD, Build-/Test-Ergebnissen, Freigabeaussage. Ein während
  der Stichprobe entdeckter Doku-Defekt (R3: API-Key-Legacy-Variable)
  wurde unmittelbar in gui-bedienanleitung.md korrigiert.

V2.0 ist freigabefähig. 1.403 Tests grün, 0 Failures, 0 Errors.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 22:50:51 +02:00
parent 1bb7a42735
commit 016da8318d
10 changed files with 1399 additions and 21 deletions
@@ -108,6 +108,13 @@ public final class GuiConfigurationEditorWorkspace {
private final Label configurationPathValueLabel = new Label();
/** Package-private to allow visibility assertions in smoke tests. */
final Label dirtyMarkerLabel = new Label("geändert");
/**
* Package-private to allow startup-notice visibility assertions in smoke tests.
* Returns the status label used to display startup notices and status messages in the header.
*/
Label statusNoticeLabel() {
return statusLabel;
}
private final Label welcomeTitleLabel = new Label("Willkommen");
private final Label welcomeTextLabel = new Label(WELCOME_TEXT);
/** Package-private to allow node lookups in smoke tests. */
@@ -2,6 +2,7 @@ package de.gecheckt.pdf.umbenenner.adapter.in.gui;
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;
import java.io.IOException;
@@ -323,6 +324,95 @@ class GuiEditorIntegrationTest {
}
}
// =========================================================================
// GUI startup with a non-existent --config path: startup notice rendered in header
// =========================================================================
/**
* Verifies that when the workspace is constructed with a startup notice (as Bootstrap does
* when {@code --config} points to a non-existent file), the notice text is rendered in the
* visible header status label.
* <p>
* This complements {@link #guiStartup_withNonExistentConfigPath_usesBlankStateAndCarriesStartupNotice}
* which only verifies the blank editor state. This test verifies the user-visible side: the
* notice must be rendered so the user can read it.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void guiStartup_withNonExistentConfigPath_noticeIsRenderedInHeaderStatusLabel()
throws Exception {
String notice = "Konfigurationsdatei nicht gefunden: /no/such/file.properties\n"
+ "Die GUI startet ohne Konfigurationsdatei.";
GuiConfigurationEditorState blankState = GuiConfigurationEditorStateFactory.createBlankStartState();
GuiConfigurationFileWriter noOpWriter = (values, path) -> GuiConfigurationSaveResult.saved(path);
GuiStartupContext context = new GuiStartupContext(
blankState,
Optional.of(notice),
configFilePath -> GuiConfigurationEditorStateFactory.createBlankStartState(),
noOpWriter,
req -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req.providerIdentifier(), "kein Port im Test"),
(family, propertyValue) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent(),
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
req2 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req2.providerIdentifier(), "kein Port im Test"),
(fam2, pv2) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent()),
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort() {
@Override public boolean isDirectoryReadable(String p) { return false; }
@Override public boolean isDirectoryWritableOrCreatable(String p) { return false; }
@Override public boolean isFileReadable(String p) { return false; }
@Override public boolean isSqlitePathUsable(String p) { return false; }
},
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator(
new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(),
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort() {
@Override public boolean isDirectoryReadable(String p) { return false; }
@Override public boolean isDirectoryWritableOrCreatable(String p) { return false; }
@Override public boolean isFileReadable(String p) { return false; }
@Override public boolean isSqlitePathUsable(String p) { return false; }
},
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
req99 -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult.IncompleteConfiguration(req99.providerIdentifier(), "kein Port im Test"),
(fam99, pv99) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent())),
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService(
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
@Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreateDirectory s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
@Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.CreatePromptFile s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
@Override public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion.PrepareSqlitePath s) { return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted(s, "kein Port im Test"); }
}));
AtomicReference<Throwable> error = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(context);
// The startup notice must be rendered in the visible header status label.
javafx.scene.control.Label noticeLabel = workspace.statusNoticeLabel();
assertNotNull(noticeLabel, "Header status label must not be null");
assertTrue(noticeLabel.isVisible(),
"Header status label must be visible when a startup notice is present");
assertTrue(noticeLabel.isManaged(),
"Header status label must be managed when a startup notice is present");
assertFalse(noticeLabel.getText().isBlank(),
"Header status label text must not be blank when startup notice is present");
assertTrue(noticeLabel.getText().contains("Konfigurationsdatei nicht gefunden"),
"Header status label must contain the notice text; got: " + noticeLabel.getText());
} catch (Throwable t) {
error.set(t);
} finally {
latch.countDown();
}
});
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"FX task must complete within timeout");
if (error.get() != null) {
throw new AssertionError("FX thread threw an exception", error.get());
}
}
// =========================================================================
// --config path resolution: static helper (no FX thread needed)
// =========================================================================