M12 vollständig abgeschlossen (AP-001 bis AP-008)

- AP-001: Prüf- und Korrektur-Kernobjekte (CheckpointId, CheckpointResult
  sealed interface, TechnicalTestReport mit Correction-Plan-Ableitung,
  CorrectionSuggestion sealed interface, PathCheckPort, ResourceCreationPort)
- AP-002: Aktion "Validieren" als explizite, nicht schreibende Gesamtprüfung
  des aktuellen Editorzustands
- AP-003: Provider-nahe technische Prüflogik für Endpoint, API-Key,
  Modellliste und Modellplausibilität — wiederverwendet den bestehenden
  Modellabruf-Port, kein zweiter HTTP-Pfad
- AP-004: Windows-Pfadprüfung mit ausdrücklicher Unterstützung gemappter
  Laufwerksbuchstaben (FilesystemPathCheckAdapter)
- AP-005: Aktion "Technische Tests ausführen" als vollständiger Gesamttest
  ohne Frühabbruch, Orchestrator sammelt Befunde aller Prüfblöcke
- AP-006: Schreibende Korrekturhilfen mit gesammeltem Bestätigungsdialog,
  CorrectionExecutionService, FilesystemResourceCreationAdapter
- AP-007: Automatische deutsche Standard-Prompt-Datei-Erzeugung,
  Default-Pfad neben der .properties-Datei, klare Fehlermeldung bei
  nicht beschreibbarem Zielpfad
- AP-008: Regressionstests für Gesamttest ohne Frühabbruch, ungespeicherte
  Editorzustände, Korrekturdialog, Prompt-Erzeugung, Windows-Pfade

Hexagonale Architektur durchgehend eingehalten, Domain und Application
bleiben infrastrukturfrei. Threadingmodell konsequent umgesetzt.
Naming-Regel und JavaDoc-Standard eingehalten.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-20 21:57:06 +02:00
parent aa067a3165
commit 1bb7a42735
53 changed files with 7410 additions and 47 deletions
@@ -29,6 +29,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiVisibleProviderSectio
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationFinding;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;
@@ -116,6 +117,18 @@ public final class GuiConfigurationEditorWorkspace {
private final Button openButton = new Button("Öffnen");
private final Button saveButton = new Button("Speichern");
private final Button saveAsButton = new Button("Speichern unter");
/**
* The "Validieren" button in the "Tests" section.
* Package-private to allow CSS-ID and state assertions in smoke tests.
*/
final Button validateButton = new Button("Validieren");
/**
* The "Technische Tests ausführen" button in the "Tests" section.
* Disabled while a test run is in progress to prevent concurrent runs.
* Package-private to allow CSS-ID and state assertions in smoke tests.
*/
final Button technicalTestsButton = new Button("Technische Tests ausführen");
private static final Path DEFAULT_SAVE_PATH = Paths.get("config/application.properties");
@@ -226,6 +239,19 @@ public final class GuiConfigurationEditorWorkspace {
*/
final GuiModelCatalogCoordinator modelCatalogCoordinator;
/**
* Coordinator that manages asynchronous execution of the "Technische Tests ausführen" action.
* Package-private to allow thread-factory substitution in smoke tests.
*/
final GuiTechnicalTestCoordinator technicalTestCoordinator;
/**
* Coordinator that manages the correction confirmation dialog and asynchronous execution
* of corrective actions after a technical test run.
* Package-private to allow dialog-supplier substitution in smoke tests.
*/
final GuiCorrectionDialogCoordinator correctionDialogCoordinator;
/**
* Mutable list of pending message entries produced by the model catalogue coordinator
* and the editor validation. Entries are appended on the JavaFX Application Thread.
@@ -302,6 +328,25 @@ public final class GuiConfigurationEditorWorkspace {
effectiveContext.modelCatalogPort(), pendingMessages);
this.modelCatalogCoordinator.postResultCallback = this::refreshAfterValidation;
CorrectionExecutionService correctionService = effectiveContext.correctionExecutionService();
this.correctionDialogCoordinator = new GuiCorrectionDialogCoordinator(
correctionService,
pendingMessages,
ignored -> refreshAfterValidation());
this.technicalTestCoordinator = new GuiTechnicalTestCoordinator(
effectiveContext.technicalTestOrchestrator(),
this::buildValidationInput,
() -> editorState.loadedFileSnapshot()
.map(snapshot -> snapshot.filePath().toString())
.orElse(""),
pendingMessages,
report -> {
technicalTestsButton.setDisable(false);
refreshAfterValidation();
correctionDialogCoordinator.offerCorrections(report);
});
this.unsavedChangesGuard = new GuiUnsavedChangesGuard(
triggerLabel -> showUnsavedChangesDialog(triggerLabel));
@@ -1363,14 +1408,56 @@ public final class GuiConfigurationEditorWorkspace {
// Tests / Meldungen sections (structural placeholders)
// =========================================================================
/**
* Builds the "Tests" section containing the explicit validation action button.
* <p>
* The "Validieren" button triggers a full in-memory validation of the current editor
* state without writing anything to disk. It produces the same set of local findings
* as the automatic background validation, but additionally appends an INFO message to
* the central message area that confirms the action was executed and reports the number
* of findings found.
* <p>
* Later technical overall-check actions (e.g. provider endpoint tests, path checks) will
* be added to this section as separate buttons in subsequent iterations. The validation
* action itself remains local and non-writing at all times.
*
* @return the card node for the "Tests" section
*/
private Node createTestsSection() {
VBox card = createCardContainer();
validateButton.setId("validate-button");
validateButton.setOnAction(e -> runValidationAction());
technicalTestsButton.setId("technical-tests-button");
technicalTestsButton.setOnAction(e -> runTechnicalTestsAction());
HBox buttonRow = new HBox(8, validateButton, technicalTestsButton);
buttonRow.setAlignment(Pos.CENTER_LEFT);
card.getChildren().addAll(
sectionTitle("Tests"),
textLabel("Technische Tests und Diagnoseaktionen werden in einem späteren Schritt ergänzt."));
buttonRow);
return card;
}
/**
* Executes the "Technische Tests ausführen" action triggered by the user.
* <p>
* Disables the button for the duration of the test run to prevent concurrent starts.
* Delegates to {@link GuiTechnicalTestCoordinator#triggerTechnicalTests()}, which
* reads the current editor state, starts a background worker thread, and re-enables
* the button after the result has been applied to the message area.
* <p>
* This method does not write anything to disk and does not execute any corrective actions.
* Must be called on the FX Application Thread.
*/
private void runTechnicalTestsAction() {
LOG.info("Aktion Technische Tests ausführen gestartet.");
technicalTestsButton.setDisable(true);
technicalTestCoordinator.triggerTechnicalTests();
}
/**
* Builds the "Meldungen" section containing the central message area.
* <p>
@@ -1449,16 +1536,22 @@ public final class GuiConfigurationEditorWorkspace {
// =========================================================================
/**
* Runs the editor validation against the current editor state and stores the result
* in {@link #lastValidationResult}, {@link #pendingMessages} and {@link #pendingFieldFindings}.
* Builds an {@link EditorValidationInput} from the current editor state.
* <p>
* This method is pure in-memory (no I/O) and may be called on the JavaFX Application Thread.
* Path existence checks, SQLite roundtrips and network calls are explicitly excluded from
* this validation and belong to later technical overall checks.
* Reads the active provider family and per-provider fields from the current
* {@link GuiConfigurationValues}, resolves API key descriptors via the injected
* {@link ApiKeyResolutionPort}, and returns the assembled input. This method is
* pure in-memory (no I/O) and may be called on the JavaFX Application Thread.
* <p>
* This method is used both by the synchronous local validation and by the asynchronous
* technical overall test to extract the current GUI state before handing it to a
* background worker thread.
* <p>
* Must be called on the FX Application Thread.
*
* @return assembled validation input representing the current editor state; never {@code null}
*/
private void runEditorValidation() {
EditorValidationInput buildValidationInput() {
GuiConfigurationValues values = editorState.values();
GuiProviderConfigurationState claudeState = Optional.ofNullable(
values.providerConfiguration(AiProviderFamily.CLAUDE))
@@ -1474,7 +1567,7 @@ public final class GuiConfigurationEditorWorkspace {
apiKeyResolutionPort.resolve(AiProviderFamily.OPENAI_COMPATIBLE,
openaiState.apiKey().propertyValue());
EditorValidationInput input = new EditorValidationInput(
return new EditorValidationInput(
values.activeProviderFamily(),
values.sourceFolder(),
values.targetFolder(),
@@ -1491,7 +1584,20 @@ public final class GuiConfigurationEditorWorkspace {
openaiState.model(),
openaiState.timeoutSeconds(),
openaiKeyDescriptor);
}
/**
* Runs the editor validation against the current editor state and stores the result
* in {@link #lastValidationResult}, {@link #pendingMessages} and {@link #pendingFieldFindings}.
* <p>
* This method is pure in-memory (no I/O) and may be called on the JavaFX Application Thread.
* Path existence checks, SQLite roundtrips and network calls are explicitly excluded from
* this validation and belong to later technical overall checks.
* <p>
* Must be called on the FX Application Thread.
*/
private void runEditorValidation() {
EditorValidationInput input = buildValidationInput();
EditorValidationReport report = editorValidator.validate(input);
// Translate to GUI types
@@ -1523,6 +1629,60 @@ public final class GuiConfigurationEditorWorkspace {
refreshAfterValidation();
}
/**
* Executes the explicit "Validieren" action triggered by the user.
* <p>
* This method re-runs the same in-memory validation logic as the automatic background
* check ({@link #runEditorValidation()}) against the current editor state, without
* writing anything to disk or making any network or filesystem calls. It is therefore
* safe to call on the FX Application Thread at any time.
* <p>
* In addition to updating the findings shown by the automatic validation, this action
* appends a dedicated INFO message to the central message area to confirm to the user
* that the action was explicitly executed and to report the number of findings found.
* The message uses a distinct source tag so that it can be replaced on subsequent
* executions without removing messages from other sources.
* <p>
* Differences from the automatic background validation:
* <ul>
* <li>This method is user-initiated and always produces the confirmation INFO message.</li>
* <li>The automatic validation runs silently on field changes and file loads.</li>
* <li>Remote checks (provider endpoint, API key acceptance, model list) are not part of
* this action; they belong to the technical overall-check action.</li>
* </ul>
* <p>
* Must be called on the FX Application Thread.
*/
private void runValidationAction() {
LOG.info("Aktion Validieren ausgeführt.");
// Re-run in-memory validation; this updates pendingMessages, pendingFieldFindings
// and lastValidationResult identically to the automatic background check.
runEditorValidation();
// Replace any previous action-confirmation message; preserve all other messages.
pendingMessages.removeIf(m -> m.source().isPresent()
&& "Validierung-Aktion".equals(m.source().get()));
int findingCount = lastValidationResult.fieldFindings().size();
String confirmationText;
if (findingCount == 0) {
confirmationText = "Aktion Validieren wurde ausgeführt. Keine Befunde.";
} else {
confirmationText = "Aktion Validieren wurde ausgeführt. "
+ findingCount + " Befund" + (findingCount == 1 ? "" : "e") + " gefunden.";
}
pendingMessages.add(GuiMessageEntry.of(
GuiMessageSeverity.INFO, confirmationText, "Validierung-Aktion"));
lastValidationResult = new GuiEditorValidationResult(
List.copyOf(pendingMessages),
List.copyOf(pendingFieldFindings),
java.time.Instant.now());
refreshAfterValidation();
}
/**
* Maps an {@link EditorValidationSeverity} to the corresponding {@link GuiMessageSeverity}.
*
@@ -0,0 +1,287 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.ConfirmationDialogContent;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageSeverity;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionReport;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionPlan;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestReport;
import javafx.application.Platform;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Koordiniert den gesammelten Bestätigungsdialog und die anschließende Ausführung
* schreibender Korrekturmaßnahmen nach einem technischen Gesamttest.
* <p>
* Der Koordinator empfängt einen {@link TechnicalTestReport}, prüft ob korrigierbare
* Befunde vorliegen, leitet daraus einen {@link CorrectionPlan} ab, zeigt dem Benutzer
* einen gesammelten Bestätigungsdialog und führt die Korrekturen bei Bestätigung über
* den {@link CorrectionExecutionService} auf einem Hintergrund-Worker-Thread aus.
* Ergebnisse werden in die geteilte {@code pendingMessages}-Liste eingehängt.
*
* <h2>Ablauf</h2>
* <ol>
* <li>Bericht erhält: prüfen ob {@code hasCorrectableFindings()}.</li>
* <li>Wenn keine korrigierbaren Befunde: kein Dialog, keine Aktion.</li>
* <li>Wenn korrigierbare Befunde: {@link CorrectionPlan} ableiten.</li>
* <li>Dialog auf FX-Thread anzeigen.</li>
* <li>Bei Bestätigung: Korrekturen auf Worker-Thread ausführen.</li>
* <li>Ergebnisse via {@code Platform.runLater} auf FX-Thread zurückführen.</li>
* <li>Meldungen in {@code pendingMessages} einhängen (Replace-Semantik).</li>
* </ol>
*
* <h2>Threading-Kontrakt</h2>
* <p>
* {@link #offerCorrections(TechnicalTestReport)} muss auf dem JavaFX Application Thread
* aufgerufen werden. I/O (Ausführung der Korrekturen) läuft auf einem dedizierten
* Daemon-Hintergrund-Thread. UI-Updates erfolgen ausschließlich via
* {@code Platform.runLater}.
*
* <h2>Keine stillen Korrekturen</h2>
* <p>
* Ohne ausdrückliche Benutzerbestätigung werden keine schreibenden Änderungen ausgeführt.
* Bei Dialog-Abbruch bleibt der Zustand unverändert.
*
* <h2>Anti-Scope</h2>
* <p>
* Dieser Koordinator führt keine Provider-nahen Korrekturen, keine Änderung fachlich
* riskanter Werte und keine automatischen Laufstarts durch.
*/
public final class GuiCorrectionDialogCoordinator {
/** Quell-Tag für Einträge in {@code pendingMessages}, die von diesem Koordinator stammen. */
static final String SOURCE_TAG = "Korrekturen";
private static final Logger LOG = LogManager.getLogger(GuiCorrectionDialogCoordinator.class);
private final CorrectionExecutionService correctionExecutionService;
private final List<GuiMessageEntry> pendingMessages;
private final Consumer<Void> refreshCallback;
/**
* Funktion, die dem Benutzer den Bestätigungsdialog zeigt.
* <p>
* Erhält den {@link ConfirmationDialogContent} und gibt {@code true} zurück, wenn der
* Benutzer „Fortfahren" wählt, andernfalls {@code false}. Standard: echter Alert.
* Paket-privat für Test-Substitution.
*/
Function<ConfirmationDialogContent, Boolean> dialogSupplier;
/**
* Factory für den Hintergrund-Worker-Thread.
* Standard: Daemon-Thread namens {@code gui-correction-worker}.
* Paket-privat für Test-Substitution.
*/
Function<Runnable, Thread> correctionThreadFactory;
/**
* Verbraucher zur Rückführung des Ergebnisses auf den FX-Thread.
* Standard: {@code Platform.runLater}. Paket-privat für Test-Substitution.
*/
java.util.function.Consumer<Runnable> resultDelivery;
/**
* Erstellt einen neuen Koordinator.
*
* @param correctionExecutionService Service für die Ausführung von Korrekturen; darf nicht {@code null} sein
* @param pendingMessages geteilte veränderliche Nachrichtenliste; darf nicht {@code null} sein
* @param refreshCallback Callback nach Anwendung der Ergebnisse (z. B. View-Aktualisierung);
* darf nicht {@code null} sein
* @throws NullPointerException wenn einer der Parameter {@code null} ist
*/
public GuiCorrectionDialogCoordinator(CorrectionExecutionService correctionExecutionService,
List<GuiMessageEntry> pendingMessages,
Consumer<Void> refreshCallback) {
this.correctionExecutionService = Objects.requireNonNull(correctionExecutionService,
"correctionExecutionService must not be null");
this.pendingMessages = Objects.requireNonNull(pendingMessages,
"pendingMessages must not be null");
this.refreshCallback = Objects.requireNonNull(refreshCallback,
"refreshCallback must not be null");
this.dialogSupplier = this::showConfirmationDialog;
this.correctionThreadFactory = task -> {
Thread t = new Thread(task, "gui-correction-worker");
t.setDaemon(true);
return t;
};
this.resultDelivery = Platform::runLater;
}
/**
* Prüft den Bericht auf korrigierbare Befunde, zeigt bei Bedarf den Bestätigungsdialog
* und führt die Korrekturen nach Bestätigung asynchron aus.
* <p>
* Wenn der Bericht keine korrigierbaren Befunde enthält ({@code hasCorrectableFindings()
* == false}), wird kein Dialog angezeigt und keine Aktion ausgeführt.
* <p>
* Muss auf dem JavaFX Application Thread aufgerufen werden.
*
* @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
*/
public void offerCorrections(TechnicalTestReport report) {
Objects.requireNonNull(report, "report must not be null");
if (!report.hasCorrectableFindings()) {
LOG.debug("Gesamttest: Keine korrigierbaren Befunde kein Bestätigungsdialog.");
return;
}
CorrectionPlan plan = report.deriveCorrectionPlan();
if (!plan.hasCorrections()) {
LOG.debug("Gesamttest: Korrekturplan ist leer kein Bestätigungsdialog.");
return;
}
LOG.info("Gesamttest: {} korrigierbare Befunde. Bestätigungsdialog wird angezeigt.", plan.size());
ConfirmationDialogContent dialogContent = ConfirmationDialogContent.fromPlan(plan);
boolean confirmed = dialogSupplier.apply(dialogContent);
if (!confirmed) {
LOG.info("Bestätigungsdialog: Benutzer hat Korrekturen abgelehnt. Keine Änderungen.");
return;
}
LOG.info("Bestätigungsdialog: Benutzer hat Korrekturen bestätigt. Ausführung startet.");
Runnable task = () -> {
CorrectionExecutionReport executionReport = correctionExecutionService.execute(plan);
resultDelivery.accept(() -> {
applyResult(executionReport);
refreshCallback.accept(null);
});
};
Thread worker = correctionThreadFactory.apply(task);
worker.start();
}
/**
* Wendet das Ergebnis der Korrekturausführung auf die geteilte Nachrichtenliste an.
* <p>
* Entfernt alle vorherigen Einträge mit Quelle {@link #SOURCE_TAG} und fügt für jedes
* {@link CorrectionOutcome} einen neuen Eintrag hinzu. Zusätzlich wird eine Zusammenfassung
* angehängt.
* <p>
* Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
*
* @param report das Ausführungsergebnis; darf nicht {@code null} sein
*/
private void applyResult(CorrectionExecutionReport report) {
// Alte Einträge mit Source-Tag entfernen (Replace-Semantik)
pendingMessages.removeIf(msg -> SOURCE_TAG.equals(msg.source().orElse("")));
long appliedCount = 0;
long failedCount = 0;
long notAttemptedCount = 0;
for (CorrectionOutcome outcome : report.outcomes()) {
switch (outcome) {
case CorrectionOutcome.Applied applied -> {
pendingMessages.add(GuiMessageEntry.of(
GuiMessageSeverity.INFO,
"Korrektur angewendet: " + applied.suggestion().descriptionForUser()
+ " " + applied.message(),
SOURCE_TAG));
appliedCount++;
LOG.info("Korrektur angewendet: {} {}", applied.suggestion().descriptionForUser(),
applied.message());
}
case CorrectionOutcome.Failed failed -> {
pendingMessages.add(GuiMessageEntry.of(
GuiMessageSeverity.ERROR,
"Korrektur fehlgeschlagen: " + failed.suggestion().descriptionForUser()
+ " (" + failed.errorMessage() + ")",
SOURCE_TAG));
failedCount++;
LOG.warn("Korrektur fehlgeschlagen: {} {}", failed.suggestion().descriptionForUser(),
failed.errorMessage());
}
case CorrectionOutcome.NotAttempted notAttempted -> {
pendingMessages.add(GuiMessageEntry.of(
GuiMessageSeverity.HINT,
"Korrektur nicht durchgeführt: " + notAttempted.suggestion().descriptionForUser()
+ " " + notAttempted.reason(),
SOURCE_TAG));
notAttemptedCount++;
LOG.info("Korrektur nicht durchgeführt: {} {}", notAttempted.suggestion().descriptionForUser(),
notAttempted.reason());
}
}
}
// Zusammenfassung
String summary = "Korrekturausführung abgeschlossen: "
+ appliedCount + " angewendet, "
+ failedCount + " fehlgeschlagen, "
+ notAttemptedCount + " nicht versucht.";
pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, summary, SOURCE_TAG));
LOG.info("Korrekturausführung abgeschlossen: {} angewendet, {} fehlgeschlagen, {} nicht versucht.",
appliedCount, failedCount, notAttemptedCount);
}
/**
* Zeigt den echten JavaFX-Bestätigungsdialog und gibt die Benutzerentscheidung zurück.
* <p>
* Muss auf dem JavaFX Application Thread aufgerufen werden. Standard-Fokus liegt auf
* dem Abbrechen-Button (kein versehentliches Bestätigen durch Enter).
*
* @param content der Dialoginhalt; darf nicht {@code null} sein
* @return {@code true} wenn der Benutzer „Fortfahren" wählt, sonst {@code false}
*/
private boolean showConfirmationDialog(ConfirmationDialogContent content) {
javafx.scene.control.ButtonType proceedButton =
new javafx.scene.control.ButtonType("Fortfahren",
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
javafx.scene.control.ButtonType cancelButton =
new javafx.scene.control.ButtonType("Abbrechen",
javafx.scene.control.ButtonBar.ButtonData.CANCEL_CLOSE);
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(
javafx.scene.control.Alert.AlertType.CONFIRMATION);
alert.setTitle(content.title());
alert.setHeaderText(content.introText());
StringBuilder correctionListText = new StringBuilder();
for (String line : content.correctionLines()) {
correctionListText.append("").append(line).append("\n");
}
correctionListText.append("\nFortfahren?");
alert.setContentText(correctionListText.toString());
alert.getButtonTypes().setAll(proceedButton, cancelButton);
// Standard-Fokus: Abbrechen
javafx.scene.Node cancelNode = alert.getDialogPane().lookupButton(cancelButton);
if (cancelNode instanceof javafx.scene.control.Button btn) {
btn.setDefaultButton(true);
}
javafx.scene.Node proceedNode = alert.getDialogPane().lookupButton(proceedButton);
if (proceedNode instanceof javafx.scene.control.Button btn) {
btn.setDefaultButton(false);
}
java.util.Optional<javafx.scene.control.ButtonType> result = alert.showAndWait();
return result.isPresent() && result.get() == proceedButton;
}
/**
* Gibt eine unveränderliche Momentaufnahme der aktuell ausstehenden Nachrichten zurück.
* <p>
* Ausschließlich für Tests gedacht.
*
* @return unveränderliche Kopie der Nachrichtenliste; nie {@code null}
*/
public List<GuiMessageEntry> pendingMessagesSnapshot() {
return List.copyOf(pendingMessages);
}
}
@@ -9,18 +9,29 @@ import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator;
/**
* Immutable startup data for the GUI adapter.
* <p>
* Carries the initial editor state, the optional startup notice, the file-loading callback,
* the file-writing callback that the workspace uses for native save actions, the
* {@link AiModelCatalogPort} used to retrieve available AI model lists on demand, and the
* {@link AiModelCatalogPort} used to retrieve available AI model lists on demand, the
* {@link ApiKeyResolutionPort} used by the editor validation to determine the effective
* API key provenance from environment variables.
* API key provenance from environment variables, the {@link ProviderTechnicalTestService}
* used to execute provider-specific technical checks, the {@link PathCheckPort}
* used to verify filesystem path accessibility for configuration values, the
* {@link TechnicalTestOrchestrator} used by the "Technische Tests ausführen" action, and the
* {@link CorrectionExecutionService} used to execute corrective actions after a
* technical test run has been confirmed by the user.
* <p>
* All ports are supplied by Bootstrap so that the GUI adapter does not need to know about
* provider-specific HTTP details or adapter wiring.
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to
* know about provider-specific HTTP details or adapter wiring.
*/
public record GuiStartupContext(
GuiConfigurationEditorState initialState,
@@ -28,17 +39,25 @@ public record GuiStartupContext(
GuiConfigurationFileLoader configurationFileLoader,
GuiConfigurationFileWriter configurationFileWriter,
AiModelCatalogPort modelCatalogPort,
ApiKeyResolutionPort apiKeyResolutionPort) {
ApiKeyResolutionPort apiKeyResolutionPort,
ProviderTechnicalTestService providerTechnicalTestService,
PathCheckPort pathCheckPort,
TechnicalTestOrchestrator technicalTestOrchestrator,
CorrectionExecutionService correctionExecutionService) {
/**
* Creates a startup context.
*
* @param initialState initial editor state; must not be {@code null}
* @param startupNotice optional startup notice; {@code null} becomes empty
* @param configurationFileLoader file-loading callback; must not be {@code null}
* @param configurationFileWriter file-writing callback; must not be {@code null}
* @param modelCatalogPort port for retrieving AI model lists; must not be {@code null}
* @param apiKeyResolutionPort port for resolving API key provenance; must not be {@code null}
* @param initialState initial editor state; must not be {@code null}
* @param startupNotice optional startup notice; {@code null} becomes empty
* @param configurationFileLoader file-loading callback; must not be {@code null}
* @param configurationFileWriter file-writing callback; must not be {@code null}
* @param modelCatalogPort port for retrieving AI model lists; must not be {@code null}
* @param apiKeyResolutionPort port for resolving API key provenance; must not be {@code null}
* @param providerTechnicalTestService service for provider-specific technical checks; must not be {@code null}
* @param pathCheckPort port for filesystem path accessibility checks; must not be {@code null}
* @param technicalTestOrchestrator orchestrator for the full technical test run; must not be {@code null}
* @param correctionExecutionService service for executing confirmed corrective actions; must not be {@code null}
*/
public GuiStartupContext {
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
@@ -51,14 +70,30 @@ public record GuiStartupContext(
"modelCatalogPort must not be null");
apiKeyResolutionPort = Objects.requireNonNull(apiKeyResolutionPort,
"apiKeyResolutionPort must not be null");
providerTechnicalTestService = Objects.requireNonNull(providerTechnicalTestService,
"providerTechnicalTestService must not be null");
pathCheckPort = Objects.requireNonNull(pathCheckPort,
"pathCheckPort must not be null");
technicalTestOrchestrator = Objects.requireNonNull(technicalTestOrchestrator,
"technicalTestOrchestrator must not be null");
correctionExecutionService = Objects.requireNonNull(correctionExecutionService,
"correctionExecutionService must not be null");
}
/**
* Creates a blank startup context with no loader or writer side effects, a no-op model
* catalogue port, and a no-op API key resolution port.
* catalogue port, a no-op API key resolution port, a no-op provider technical test service,
* a no-op path check port, a no-op technical test orchestrator, and a no-op
* correction execution service.
* <p>
* The no-op model catalogue port always returns {@code IncompleteConfiguration}.
* The no-op API key resolution port always returns {@code ABSENT}.
* The no-op provider technical test service uses the no-op ports above.
* The no-op path check port always returns {@code false} for all checks.
* The no-op technical test orchestrator returns a report where all checkpoints are
* {@link de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointResult.NotApplicable}.
* The no-op correction execution service uses a no-op {@link ResourceCreationPort} that always
* returns {@link de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome.NotAttempted}.
* This is safe for environments where no Bootstrap wiring is present, such as isolated
* GUI tests.
*
@@ -66,15 +101,62 @@ public record GuiStartupContext(
* @return a startup context for the unloaded editor start
*/
public static GuiStartupContext blank(Optional<String> startupNotice) {
de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort noOpCatalogPort =
request -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
.ModelCatalogResult.IncompleteConfiguration(
request.providerIdentifier(),
"Kein Modellkatalog in diesem Startkontext verfügbar.");
ApiKeyResolutionPort noOpApiKeyPort = (family, propertyValue) -> EffectiveApiKeyDescriptor.absent();
ProviderTechnicalTestService noOpTestService =
new ProviderTechnicalTestService(noOpCatalogPort, noOpApiKeyPort);
PathCheckPort noOpPathCheckPort = new PathCheckPort() {
@Override
public boolean isDirectoryReadable(String path) { return false; }
@Override
public boolean isDirectoryWritableOrCreatable(String path) { return false; }
@Override
public boolean isFileReadable(String path) { return false; }
@Override
public boolean isSqlitePathUsable(String path) { return false; }
};
TechnicalTestOrchestrator noOpOrchestrator = new TechnicalTestOrchestrator(
new de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator(),
noOpPathCheckPort,
noOpTestService);
ResourceCreationPort noOpResourceCreationPort = new ResourceCreationPort() {
@Override
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
createDirectory(de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionSuggestion.CreateDirectory suggestion) {
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext.");
}
@Override
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
createPromptFile(de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionSuggestion.CreatePromptFile suggestion) {
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext.");
}
@Override
public de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome
prepareSqlitePath(de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionSuggestion.PrepareSqlitePath suggestion) {
return new de.gecheckt.pdf.umbenenner.application.validation.technicaltest
.CorrectionOutcome.NotAttempted(suggestion, "Kein Port in diesem Startkontext.");
}
};
CorrectionExecutionService noOpCorrectionService = new CorrectionExecutionService(noOpResourceCreationPort);
return new GuiStartupContext(
GuiConfigurationEditorStateFactory.createBlankStartState(),
startupNotice,
configFilePath -> GuiConfigurationEditorStateFactory.createBlankStartState(),
(values, path) -> GuiConfigurationSaveResult.saved(path),
request -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
.ModelCatalogResult.IncompleteConfiguration(
request.providerIdentifier(),
"Kein Modellkatalog in diesem Startkontext verfügbar."),
(family, propertyValue) -> EffectiveApiKeyDescriptor.absent());
noOpCatalogPort,
noOpApiKeyPort,
noOpTestService,
noOpPathCheckPort,
noOpOrchestrator,
noOpCorrectionService);
}
}
@@ -0,0 +1,268 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageSeverity;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointId;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointResult;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestOrchestrator;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestReport;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestRequest;
import javafx.application.Platform;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Koordiniert die asynchrone Ausführung der Aktion „Technische Tests ausführen"
* für den GUI-Konfigurationseditor.
* <p>
* Dieser Koordinator ist verantwortlich für:
* <ul>
* <li>Lesen des aktuellen GUI-Editorzustands (via {@link Supplier}).</li>
* <li>Aufbau eines {@link TechnicalTestRequest} aus dem aktuellen Zustand.</li>
* <li>Ausführung des {@link TechnicalTestOrchestrator} auf einem dedizierten
* Daemon-Hinterground-Thread namens {@code gui-technical-test}.</li>
* <li>Rückführung des {@link TechnicalTestReport} auf den JavaFX Application Thread
* via {@code Platform.runLater}.</li>
* <li>Einhängen der Ergebnisse als {@link GuiMessageEntry}-Einträge in die geteilte
* {@code pendingMessages}-Liste (Quelle: „Technische-Tests").</li>
* <li>Ersetzen vorheriger Test-Einträge (Replace-Semantik) bei jedem neuen Aufruf.</li>
* <li>Weitergabe des vollständigen Berichts an den {@code postResultCallback}, damit
* spätere Arbeitsschritte (z. B. Korrekturhilfen) auf das Ergebnis zugreifen können.</li>
* </ul>
* <p>
* <strong>Threading-Kontrakt:</strong> {@link #triggerTechnicalTests()} darf nur auf dem
* JavaFX Application Thread aufgerufen werden. Hintergrund-Worker-Threads dürfen nur über
* den injizierten {@code resultDelivery}-Verbraucher mit der UI interagieren, der in der
* Produktion {@code Platform.runLater} kapselt.
* <p>
* <strong>Kein implizites Speichern:</strong> Der Koordinator liest den aktuellen GUI-Zustand
* und führt den Test aus, ohne die Konfigurationsdatei zu schreiben oder den Dirty-Zustand
* des Editors zu ändern.
* <p>
* <strong>Anti-Scope:</strong> Dieser Koordinator führt keine schreibenden Korrekturen durch.
* Korrekturvorschläge werden als Bestandteil des {@link TechnicalTestReport} zurückgegeben,
* sind aber nicht ausführbar. Die Ausführung ist einem späteren Arbeitsschritt vorbehalten.
* <p>
* Die Worker-Thread-Factory und die Result-Delivery-Funktion sind injizierbar, damit Tests
* deterministisch ohne echten Hintergrund-Thread laufen können.
*/
public final class GuiTechnicalTestCoordinator {
/** Quell-Tag für Einträge in {@code pendingMessages}, die von diesem Koordinator stammen. */
static final String SOURCE_TAG = "Technische-Tests";
private static final Logger LOG = LogManager.getLogger(GuiTechnicalTestCoordinator.class);
private final TechnicalTestOrchestrator orchestrator;
private final Supplier<EditorValidationInput> inputProvider;
private final Supplier<String> configFilePathProvider;
private final List<GuiMessageEntry> pendingMessages;
private final Consumer<TechnicalTestReport> postResultCallback;
/**
* Factory für den Hintergrund-Worker-Thread. Paket-privat für Test-Substitution.
* Standard: Daemon-Thread namens {@code gui-technical-test}.
*/
Function<Runnable, Thread> testThreadFactory;
/**
* Verbraucher zur Rückführung des Ergebnisses. In der Produktion kapselt er {@code Platform.runLater}.
* In Tests kann er durch einen direkten Aufruf ersetzt werden, damit das Ergebnis sofort
* auf dem Worker-Thread angewendet wird, ohne die FX-Warteschlange zu entwässern.
* Paket-privat für Test-Substitution.
*/
java.util.function.Consumer<Runnable> resultDelivery = Platform::runLater;
/**
* Erstellt einen neuen Koordinator.
*
* @param orchestrator Orchestrator für den vollständigen Gesamttest; darf nicht {@code null} sein
* @param inputProvider Lieferant des aktuellen {@link EditorValidationInput}; darf nicht {@code null} sein
* @param configFilePathProvider Lieferant des aktuell geladenen Konfigurationsdateipfads als String;
* gibt eine leere Zeichenkette zurück wenn keine Datei geladen ist;
* darf nicht {@code null} sein
* @param pendingMessages geteilte veränderliche Nachrichtenliste; darf nicht {@code null} sein
* @param postResultCallback Callback nach erfolgreicher Ergebnisanwendung; darf nicht {@code null} sein
* @throws NullPointerException wenn einer der Parameter {@code null} ist
*/
public GuiTechnicalTestCoordinator(TechnicalTestOrchestrator orchestrator,
Supplier<EditorValidationInput> inputProvider,
Supplier<String> configFilePathProvider,
List<GuiMessageEntry> pendingMessages,
Consumer<TechnicalTestReport> postResultCallback) {
this.orchestrator = Objects.requireNonNull(orchestrator, "orchestrator must not be null");
this.inputProvider = Objects.requireNonNull(inputProvider, "inputProvider must not be null");
this.configFilePathProvider = Objects.requireNonNull(configFilePathProvider, "configFilePathProvider must not be null");
this.pendingMessages = Objects.requireNonNull(pendingMessages, "pendingMessages must not be null");
this.postResultCallback = Objects.requireNonNull(postResultCallback, "postResultCallback must not be null");
this.testThreadFactory = task -> {
Thread t = new Thread(task, "gui-technical-test");
t.setDaemon(true);
return t;
};
}
/**
* Löst die asynchrone Ausführung des vollständigen technischen Gesamttests aus.
* <p>
* Liest den aktuellen Editorzustand und den Konfigurationsdateipfad, baut einen
* {@link TechnicalTestRequest} und startet den {@link TechnicalTestOrchestrator} auf
* einem Hintergrund-Worker-Thread. Das Ergebnis wird via {@code resultDelivery} an den
* JavaFX Application Thread zurückgegeben.
* <p>
* Der Konfigurationsdateipfad wird genutzt, um bei fehlender Prompt-Datei-Konfiguration
* einen sinnvollen Standardpfad ({@code <config-parent>/prompt.txt}) zu bestimmen.
* <p>
* Muss auf dem JavaFX Application Thread aufgerufen werden.
*/
public void triggerTechnicalTests() {
EditorValidationInput input = inputProvider.get();
String configFilePath = configFilePathProvider.get();
TechnicalTestRequest request = new TechnicalTestRequest(input, configFilePath);
LOG.info("GUI-Gesamttest: Technische Tests ausführen gestartet.");
Runnable task = () -> {
TechnicalTestReport report = orchestrator.run(request);
resultDelivery.accept(() -> {
applyResult(report);
postResultCallback.accept(report);
});
};
Thread worker = testThreadFactory.apply(task);
worker.start();
}
/**
* Wendet das Ergebnis des vollständigen Gesamttests auf die geteilte Nachrichtenliste an.
* <p>
* Entfernt alle vorherigen Einträge mit Quelle {@link #SOURCE_TAG} und fügt für jeden
* Checkpoint-Ergebnis einen neuen Eintrag hinzu. Zusätzlich wird eine Zusammenfassung
* angehängt.
* <p>
* Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
*
* @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
*/
private void applyResult(TechnicalTestReport report) {
// Alte Einträge mit Source-Tag entfernen (Replace-Semantik)
pendingMessages.removeIf(msg -> SOURCE_TAG.equals(msg.source().orElse("")));
long successCount = 0;
long failureErrorCount = 0;
long failureWarnCount = 0;
long notApplicableCount = 0;
for (CheckpointResult result : report.results()) {
String label = labelFor(result.checkpointId());
switch (result) {
case CheckpointResult.Success success -> {
pendingMessages.add(GuiMessageEntry.of(
GuiMessageSeverity.INFO,
label + ": OK " + success.message(),
SOURCE_TAG));
successCount++;
LOG.info("GUI-Gesamttest: {} → OK", label);
}
case CheckpointResult.Failure failure -> {
GuiMessageSeverity severity = failure.severity() ==
de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointSeverity.ERROR
? GuiMessageSeverity.ERROR : GuiMessageSeverity.WARNING;
pendingMessages.add(GuiMessageEntry.of(
severity,
label + ": " + failure.message(),
SOURCE_TAG));
if (severity == GuiMessageSeverity.ERROR) {
failureErrorCount++;
LOG.warn("GUI-Gesamttest: {} → FEHLER: {}", label, failure.message());
} else {
failureWarnCount++;
LOG.warn("GUI-Gesamttest: {} → WARNUNG: {}", label, failure.message());
}
}
case CheckpointResult.NotApplicable notApplicable -> {
pendingMessages.add(GuiMessageEntry.of(
GuiMessageSeverity.HINT,
label + ": nicht anwendbar " + notApplicable.reason(),
SOURCE_TAG));
notApplicableCount++;
LOG.info("GUI-Gesamttest: {} → nicht anwendbar: {}", label, notApplicable.reason());
}
}
}
// Zusammenfassung
long totalFindings = failureErrorCount + failureWarnCount;
String summary = buildSummaryMessage(report.results().size(),
successCount, failureErrorCount, failureWarnCount, notApplicableCount);
pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, summary, SOURCE_TAG));
LOG.info("GUI-Gesamttest abgeschlossen. {} Befunde ({} Erfolg, {} Fehler, {} Warnung, {} nicht anwendbar).",
totalFindings, successCount, failureErrorCount, failureWarnCount, notApplicableCount);
}
/**
* Gibt das deutsche Label für einen Prüfpunkt zurück.
*
* @param id der Prüfpunkt-Bezeichner; darf nicht {@code null} sein
* @return das deutsche Label; nie {@code null}
*/
static String labelFor(CheckpointId id) {
return switch (id) {
case CONFIGURATION_BASIC_VALIDATION -> "Konfiguration grundsätzlich validierbar";
case PROVIDER_CONFIGURATION -> "Provider-Konfiguration prüfbar";
case BASE_URL_REACHABLE -> "Basis-URL/Endpoint erreichbar";
case API_KEY_PRESENT -> "API-Schlüssel vorhanden";
case API_KEY_ACCEPTED -> "API-Schlüssel technisch akzeptiert";
case MODEL_LIST_AVAILABLE -> "Modellliste abrufbar";
case SELECTED_MODEL_PLAUSIBLE -> "Ausgewähltes Modell plausibel";
case PROMPT_FILE_PRESENT -> "Prompt-Datei vorhanden und lesbar";
case SOURCE_FOLDER_PRESENT -> "Quellordner vorhanden und lesbar";
case TARGET_FOLDER_USABLE -> "Zielordner vorhanden oder anlegbar sowie schreibbar";
case SQLITE_PATH_USABLE -> "SQLite-Pfad technisch nutzbar";
};
}
/**
* Baut die deutsche Zusammenfassungsmeldung des Gesamttests.
*
* @param total Gesamtzahl der Prüfpunkte
* @param successCount Anzahl der erfolgreichen Prüfpunkte
* @param errorCount Anzahl der fehlgeschlagenen Prüfpunkte (Schweregrad ERROR)
* @param warningCount Anzahl der Warnungs-Prüfpunkte
* @param notApplicable Anzahl der nicht-anwendbaren Prüfpunkte
* @return deutsche Zusammenfassungsmeldung; nie {@code null}
*/
private static String buildSummaryMessage(long total, long successCount, long errorCount,
long warningCount, long notApplicable) {
long findings = errorCount + warningCount;
String base = "Gesamttest abgeschlossen. " + total + " Prüfpunkte: "
+ successCount + " Erfolg, "
+ errorCount + " Fehler, "
+ warningCount + " Warnung, "
+ notApplicable + " nicht anwendbar.";
if (findings == 0) {
return base + " Keine Befunde.";
}
return base;
}
/**
* Gibt eine unveränderliche Momentaufnahme der aktuell ausstehenden Nachrichten zurück.
* <p>
* Ausschließlich für Tests gedacht.
*
* @return unveränderliche Kopie der Nachrichtenliste; nie {@code null}
*/
public List<GuiMessageEntry> pendingMessagesSnapshot() {
return List.copyOf(pendingMessages);
}
}
@@ -0,0 +1,80 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import java.util.List;
import java.util.Objects;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionPlan;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
/**
* Inhalt des gesammelten Bestätigungsdialogs für schreibende Korrekturmaßnahmen.
* <p>
* Bevor schreibende Korrekturen aus einem {@link CorrectionPlan} ausgeführt werden,
* zeigt die GUI diesen Inhalt in einem einmaligen Bestätigungsdialog. Der Benutzer
* kann die Korrekturen bestätigen oder ablehnen; ohne Bestätigung werden keine
* Änderungen vorgenommen.
* <p>
* Dieser Record liegt bewusst im GUI-Modul, da er ausschließlich für die
* Darstellung im Bestätigungsdialog der JavaFX-Oberfläche genutzt wird. Er enthält
* selbst keine JavaFX-Typen und kann auf beliebigen Threads erzeugt werden.
* <p>
* Die Beschreibungszeilen ({@link #correctionLines}) entsprechen den
* {@link CorrectionSuggestion#descriptionForUser()}-Texten der im Plan enthaltenen
* Vorschläge in Reihenfolge.
*
* @param title deutscher Dialogtitel; nie {@code null}
* @param introText einleitender deutschsprachiger Text; nie {@code null}
* @param correctionLines Liste der deutschen Beschreibungszeilen, eine pro Korrekturmaßnahme;
* nie {@code null}
*/
public record ConfirmationDialogContent(
String title,
String introText,
List<String> correctionLines) {
/**
* Erstellt einen neuen Bestätigungsdialog-Inhalt.
*
* @param title Dialogtitel; darf nicht {@code null} sein
* @param introText einleitender Text; darf nicht {@code null} sein
* @param correctionLines Beschreibungszeilen; darf nicht {@code null} sein
* @throws NullPointerException wenn ein Parameter {@code null} ist
*/
public ConfirmationDialogContent {
Objects.requireNonNull(title, "title must not be null");
Objects.requireNonNull(introText, "introText must not be null");
Objects.requireNonNull(correctionLines, "correctionLines must not be null");
correctionLines = List.copyOf(correctionLines);
}
/**
* Erstellt den Bestätigungsdialog-Inhalt aus einem {@link CorrectionPlan}.
* <p>
* Die Beschreibungszeilen werden aus den
* {@link CorrectionSuggestion#descriptionForUser()}-Texten der Vorschläge im Plan
* in Reihenfolge übernommen.
*
* @param plan Korrekturplan; darf nicht {@code null} sein
* @return ein neuer Dialoginhalt; nie {@code null}
* @throws NullPointerException wenn {@code plan} {@code null} ist
*/
public static ConfirmationDialogContent fromPlan(CorrectionPlan plan) {
Objects.requireNonNull(plan, "plan must not be null");
List<String> lines = plan.suggestions().stream()
.map(CorrectionSuggestion::descriptionForUser)
.toList();
return new ConfirmationDialogContent(
"Korrekturen bestätigen",
"Folgende technische Korrekturen werden durchgeführt:",
lines);
}
/**
* Gibt an, ob der Dialoginhalt mindestens eine Beschreibungszeile enthält.
*
* @return {@code true} wenn mindestens eine Korrekturmaßnahme beschrieben ist
*/
public boolean hasCorrections() {
return !correctionLines.isEmpty();
}
}
@@ -25,6 +25,8 @@
* <li>The consolidated validation result that feeds both the central message area and
* field-near error display
* ({@link de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiEditorValidationResult}).</li>
* <li>The confirmation dialog content for collected write-corrective actions
* ({@link de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.ConfirmationDialogContent}).</li>
* </ul>
* <p>
* Most classes in this package are intentionally free of JavaFX controls so they can be used
@@ -397,7 +397,33 @@ class GuiAdapterSmokeTest {
configFilePath -> GuiConfigurationTemplateFactory.createStandardTemplate(),
testWriter,
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());
(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"); }
}));
GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(context);
workspaceRef.set(workspace);
@@ -0,0 +1,318 @@
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.assertTrue;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.ConfirmationDialogContent;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointId;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointResult;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CheckpointSeverity;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionExecutionService;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionPlan;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.TechnicalTestReport;
import javafx.application.Platform;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
/**
* Monocle-basierte headless Smoke-Tests für {@link GuiCorrectionDialogCoordinator}.
* <p>
* Prüft folgende Szenarien:
* <ul>
* <li>Bericht mit korrigierbaren Befunden → Dialog wird angefragt, bei Bestätigung
* werden Korrekturen ausgeführt, Meldungen erscheinen.</li>
* <li>Bericht ohne korrigierbare Befunde → kein Dialog, keine Korrekturen.</li>
* <li>Bei Dialog-Abbruch → keine Korrekturen, keine Meldungen mit Source „Korrekturen".</li>
* </ul>
* <p>
* Der {@code correctionThreadFactory} wird auf synchrone Ausführung umgestellt und
* {@code resultDelivery} auf direkten Aufruf, damit Ergebnisse sofort nach
* {@code offerCorrections()} verfügbar sind.
*/
class GuiCorrectionDialogCoordinatorSmokeTest {
private static final long FX_TIMEOUT_SECONDS = 10;
private static final AtomicBoolean PLATFORM_STARTED = new AtomicBoolean(false);
@BeforeAll
static void setUpJavaFxPlatform() throws InterruptedException {
Platform.setImplicitExit(false);
CountDownLatch latch = new CountDownLatch(1);
try {
Platform.startup(() -> {
PLATFORM_STARTED.set(true);
latch.countDown();
});
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"JavaFX Platform must start within timeout");
} catch (IllegalStateException alreadyStarted) {
CountDownLatch verifyLatch = new CountDownLatch(1);
Platform.runLater(() -> {
PLATFORM_STARTED.set(true);
verifyLatch.countDown();
});
assertTrue(verifyLatch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"Existing JavaFX Platform must be reachable within timeout");
}
}
@AfterAll
static void tearDownJavaFxPlatform() {
// Shared platform do not call Platform.exit().
}
// =========================================================================
// Scenario: report with correctable findings → dialog shown, corrections applied
// =========================================================================
/**
* Smoke-Test: Bei korrigierbaren Befunden und Dialog-Bestätigung werden Korrekturen
* ausgeführt und als Meldungen eingehängt.
*/
@Test
void offerCorrections_withCorrectableFindings_dialogConfirmed_appliesCorrectionAndAddsMessages()
throws Exception {
runOnFx(() -> {
List<GuiMessageEntry> messages = new ArrayList<>();
AtomicBoolean correctionExecuted = new AtomicBoolean(false);
CorrectionExecutionService service = buildServiceThatTracksExecution(correctionExecuted);
GuiCorrectionDialogCoordinator coordinator = buildSyncCoordinator(
service, messages, true /* confirm */);
TechnicalTestReport report = buildReportWithCorrectableFinding();
coordinator.offerCorrections(report);
assertTrue(correctionExecuted.get(),
"Korrektur muss nach Bestätigung ausgeführt worden sein");
long correctionEntries = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiCorrectionDialogCoordinator.SOURCE_TAG.equals(m.source().get()))
.count();
assertTrue(correctionEntries > 0,
"Nach Ausführung müssen Meldungen mit Source '"
+ GuiCorrectionDialogCoordinator.SOURCE_TAG + "' vorhanden sein");
});
}
// =========================================================================
// Scenario: report without correctable findings → no dialog, no corrections
// =========================================================================
/**
* Smoke-Test: Bericht ohne korrigierbare Befunde → kein Dialog, keine Korrekturen.
*/
@Test
void offerCorrections_withoutCorrectableFindings_noDialogNoCorrections() throws Exception {
runOnFx(() -> {
List<GuiMessageEntry> messages = new ArrayList<>();
AtomicBoolean dialogShown = new AtomicBoolean(false);
AtomicBoolean correctionExecuted = new AtomicBoolean(false);
CorrectionExecutionService service = buildServiceThatTracksExecution(correctionExecuted);
GuiCorrectionDialogCoordinator coordinator = buildSyncCoordinator(
service, messages, true /* confirm */);
coordinator.dialogSupplier = content -> {
dialogShown.set(true);
return true;
};
// Report with NO correctable findings (all succeed)
TechnicalTestReport report = buildReportWithNoCorrectableFindings();
coordinator.offerCorrections(report);
assertFalse(dialogShown.get(), "Kein Dialog darf angezeigt werden wenn keine Korrekturen möglich");
assertFalse(correctionExecuted.get(), "Keine Korrektur darf ausgeführt werden");
assertTrue(messages.isEmpty(), "Keine Meldungen dürfen hinzugefügt werden");
});
}
// =========================================================================
// Scenario: dialog cancelled → no corrections, no messages with SOURCE_TAG
// =========================================================================
/**
* Smoke-Test: Dialog-Abbruch → keine Korrekturen, keine Meldungen mit Source-Tag.
*/
@Test
void offerCorrections_dialogCancelled_noCorrectionsNoMessages() throws Exception {
runOnFx(() -> {
List<GuiMessageEntry> messages = new ArrayList<>();
AtomicBoolean correctionExecuted = new AtomicBoolean(false);
CorrectionExecutionService service = buildServiceThatTracksExecution(correctionExecuted);
GuiCorrectionDialogCoordinator coordinator = buildSyncCoordinator(
service, messages, false /* cancel */);
TechnicalTestReport report = buildReportWithCorrectableFinding();
coordinator.offerCorrections(report);
assertFalse(correctionExecuted.get(),
"Bei Dialog-Abbruch dürfen keine Korrekturen ausgeführt werden");
long correctionEntries = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiCorrectionDialogCoordinator.SOURCE_TAG.equals(m.source().get()))
.count();
assertEquals(0, correctionEntries,
"Bei Dialog-Abbruch dürfen keine Meldungen mit Source '"
+ GuiCorrectionDialogCoordinator.SOURCE_TAG + "' hinzugefügt werden");
});
}
// =========================================================================
// Scenario: replace semantics second run replaces previous SOURCE_TAG entries
// =========================================================================
/**
* Smoke-Test: Beim zweiten Aufruf werden vorherige SOURCE_TAG-Einträge ersetzt.
*/
@Test
void offerCorrections_calledTwice_replacesPreviousMessages() throws Exception {
runOnFx(() -> {
List<GuiMessageEntry> messages = new ArrayList<>();
AtomicBoolean ignored = new AtomicBoolean(false);
CorrectionExecutionService service = buildServiceThatTracksExecution(ignored);
GuiCorrectionDialogCoordinator coordinator = buildSyncCoordinator(
service, messages, true);
TechnicalTestReport report = buildReportWithCorrectableFinding();
coordinator.offerCorrections(report);
long countAfterFirst = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiCorrectionDialogCoordinator.SOURCE_TAG.equals(m.source().get()))
.count();
coordinator.offerCorrections(report);
long countAfterSecond = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiCorrectionDialogCoordinator.SOURCE_TAG.equals(m.source().get()))
.count();
assertEquals(countAfterFirst, countAfterSecond,
"Zweiter Aufruf muss vorherige Einträge ersetzen (Replace-Semantik)");
});
}
// =========================================================================
// Helpers
// =========================================================================
/**
* Baut einen synchron laufenden {@link GuiCorrectionDialogCoordinator}.
* <p>
* Thread-Factory läuft inline; resultDelivery ist direkter Aufruf; dialogSupplier
* gibt den festen {@code confirm}-Wert zurück.
*/
private static GuiCorrectionDialogCoordinator buildSyncCoordinator(
CorrectionExecutionService service,
List<GuiMessageEntry> messages,
boolean confirm) {
GuiCorrectionDialogCoordinator coordinator = new GuiCorrectionDialogCoordinator(
service,
messages,
ignored -> { /* no-op refresh */ });
coordinator.dialogSupplier = content -> confirm;
coordinator.correctionThreadFactory = task -> new Thread(task, "sync-correction-thread") {
@Override
public void start() {
run(); // inline, synchronous
}
};
coordinator.resultDelivery = Runnable::run; // direct call, no FX queue
return coordinator;
}
/**
* Baut einen {@link CorrectionExecutionService}, der {@code correctionExecuted} auf {@code true}
* setzt, wenn eine Korrektur aufgerufen wird.
*/
private static CorrectionExecutionService buildServiceThatTracksExecution(
AtomicBoolean correctionExecuted) {
ResourceCreationPort trackingPort = new ResourceCreationPort() {
@Override
public CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory s) {
correctionExecuted.set(true);
return new CorrectionOutcome.Applied(s, "Angelegt");
}
@Override
public CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile s) {
correctionExecuted.set(true);
return new CorrectionOutcome.Applied(s, "Erzeugt");
}
@Override
public CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath s) {
correctionExecuted.set(true);
return new CorrectionOutcome.Applied(s, "Vorbereitet");
}
};
return new CorrectionExecutionService(trackingPort);
}
/** Baut einen Bericht mit einem korrigierbaren Fehler-Befund. */
private static TechnicalTestReport buildReportWithCorrectableFinding() {
CorrectionSuggestion.CreateDirectory suggestion =
new CorrectionSuggestion.CreateDirectory("C:/test/target", "Zielordner anlegen: C:/test/target");
CheckpointResult.Failure failure = new CheckpointResult.Failure(
CheckpointId.TARGET_FOLDER_USABLE,
CheckpointSeverity.ERROR,
"Zielordner nicht vorhanden",
Optional.of(suggestion));
return new TechnicalTestReport(List.of(failure), Instant.now());
}
/** Baut einen Bericht ohne korrigierbare Befunde (alles erfolgreich). */
private static TechnicalTestReport buildReportWithNoCorrectableFindings() {
CheckpointResult.Success success = new CheckpointResult.Success(
CheckpointId.TARGET_FOLDER_USABLE,
"Zielordner vorhanden");
return new TechnicalTestReport(List.of(success), Instant.now());
}
private static void runOnFx(ThrowingRunnable task) throws Exception {
java.util.concurrent.atomic.AtomicReference<Throwable> error =
new java.util.concurrent.atomic.AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
task.run();
} catch (Throwable t) {
error.set(t);
} finally {
latch.countDown();
}
});
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"FX task must complete within timeout");
Throwable t = error.get();
if (t != null) {
if (t instanceof Exception ex) throw ex;
throw new AssertionError("Unexpected error", t);
}
}
@FunctionalInterface
private interface ThrowingRunnable {
void run() throws Exception;
}
}
@@ -324,7 +324,33 @@ class GuiEditorFieldBindingTest {
configFilePath -> GuiConfigurationTemplateFactory.createStandardTemplate(),
capturingWriter,
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());
(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"); }
}));
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(context);
ws.requestNewConfiguration();
@@ -117,7 +117,33 @@ class GuiEditorIntegrationTest {
GuiConfigurationFileWriter noOpWriter = (values, path) -> GuiConfigurationSaveResult.saved(path);
GuiStartupContext context = new GuiStartupContext(loadedState, Optional.empty(), fileLoader, 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());
(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);
@@ -241,7 +267,33 @@ class GuiEditorIntegrationTest {
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());
(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);
@@ -188,7 +188,33 @@ class GuiEditorRegressionSmokeTest {
GuiConfigurationEditorState initialState = GuiConfigurationEditorStateFactory.createBlankStartState();
GuiStartupContext context = new GuiStartupContext(initialState, Optional.empty(), loader, 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());
(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<GuiConfigurationEditorWorkspace> wsRef = new AtomicReference<>();
AtomicReference<Throwable> error = new AtomicReference<>();
@@ -301,7 +327,33 @@ class GuiEditorRegressionSmokeTest {
configFilePath -> GuiConfigurationTemplateFactory.createStandardTemplate(),
capturingWriter,
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());
(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"); }
}));
CountDownLatch setupLatch = new CountDownLatch(1);
Platform.runLater(() -> {
@@ -399,7 +451,33 @@ class GuiEditorRegressionSmokeTest {
configFilePath -> GuiConfigurationTemplateFactory.createStandardTemplate(),
capturingWriter,
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());
(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"); }
}));
CountDownLatch setupLatch = new CountDownLatch(1);
Platform.runLater(() -> {
@@ -501,7 +579,33 @@ class GuiEditorRegressionSmokeTest {
configFilePath -> GuiConfigurationTemplateFactory.createStandardTemplate(),
capturingWriter,
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());
(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"); }
}));
CountDownLatch setupLatch = new CountDownLatch(1);
Platform.runLater(() -> {
@@ -574,7 +678,33 @@ class GuiEditorRegressionSmokeTest {
configFilePath -> GuiConfigurationTemplateFactory.createStandardTemplate(),
trackingWriter,
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());
(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"); }
}));
CountDownLatch setupLatch = new CountDownLatch(1);
Platform.runLater(() -> {
@@ -123,7 +123,33 @@ class GuiEditorValidationSmokeTest {
req.providerIdentifier(), "kein Port im Test"),
(family, propertyValue) ->
de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
.EffectiveApiKeyDescriptor.absent());
.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<GuiConfigurationEditorWorkspace> wsRef = new AtomicReference<>();
AtomicReference<Throwable> error = new AtomicReference<>();
@@ -227,7 +253,33 @@ class GuiEditorValidationSmokeTest {
req.providerIdentifier(), "kein Port im Test"),
(family, propertyValue) ->
de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
.EffectiveApiKeyDescriptor.absent());
.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<GuiConfigurationEditorWorkspace> wsRef = new AtomicReference<>();
AtomicReference<Throwable> error = new AtomicReference<>();
@@ -315,7 +315,33 @@ class GuiMessageAreaSmokeTest {
return EffectiveApiKeyDescriptor.fromProviderEnvVar("CLAUDE_API_KEY");
}
return 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"); }
}));
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(ctx);
@@ -358,7 +384,33 @@ class GuiMessageAreaSmokeTest {
req.providerIdentifier(),
java.util.List.of("claude-3-5-sonnet"),
java.time.Instant.now()),
(family, propertyValue) -> EffectiveApiKeyDescriptor.absent());
(family, propertyValue) -> 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"); }
}));
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(ctx);
// Make retrieval synchronous.
@@ -419,7 +471,33 @@ class GuiMessageAreaSmokeTest {
req -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
.ModelCatalogResult.EmptyList(
req.providerIdentifier(), java.time.Instant.now()),
(family, propertyValue) -> EffectiveApiKeyDescriptor.absent());
(family, propertyValue) -> 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"); }
}));
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(ctx);
ws.modelCatalogCoordinator.modelCatalogThreadFactory = task -> new Thread(() -> task.run()) {
@@ -577,7 +655,33 @@ class GuiMessageAreaSmokeTest {
(values, path) -> GuiConfigurationSaveResult.saved(path),
req -> new ModelCatalogResult.IncompleteConfiguration(
req.providerIdentifier(), "kein Port im Test"),
(family, propertyValue) -> EffectiveApiKeyDescriptor.absent());
(family, propertyValue) -> 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"); }
}));
CountDownLatch setupLatch = new CountDownLatch(1);
Platform.runLater(() -> {
@@ -509,7 +509,33 @@ class GuiModelCatalogSmokeTest {
.createBlankStartState(),
(values, path) -> GuiConfigurationSaveResult.saved(path),
stub,
(family, propertyValue) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor.absent());
(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"); }
}));
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(ctx);
// Synchronous thread factory: run the task directly instead of starting an OS thread.
ws.modelCatalogCoordinator.modelCatalogThreadFactory = task -> new Thread(task, "gui-model-catalog-test") {
@@ -0,0 +1,432 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
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.GuiMessageEntry;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageSeverity;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorConfigurationValidator;
import de.gecheckt.pdf.umbenenner.application.validation.editor.EditorValidationInput;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort;
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.TechnicalTestReport;
import javafx.application.Platform;
import javafx.scene.control.Button;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
/**
* Monocle-based headless smoke tests for the "Technische Tests ausführen" button
* and {@link GuiTechnicalTestCoordinator}.
* <p>
* Verifies the following scenarios:
* <ul>
* <li>The "Technische Tests ausführen" button is findable by CSS ID
* {@code technical-tests-button}.</li>
* <li>Triggering the coordinator synchronously populates {@code pendingMessages}
* with entries tagged {@link GuiTechnicalTestCoordinator#SOURCE_TAG}.</li>
* <li>A second trigger replaces the previous test entries (replace semantics).</li>
* <li>The post-result callback is invoked after the result is applied.</li>
* </ul>
* <p>
* The coordinator's {@code testThreadFactory} is overridden to run the task inline
* (on the calling thread) and {@code resultDelivery} is overridden with a direct
* synchronous call, so results are available immediately after
* {@link GuiTechnicalTestCoordinator#triggerTechnicalTests()} returns.
*/
class GuiTechnicalTestCoordinatorSmokeTest {
private static final long FX_TIMEOUT_SECONDS = 10;
private static final AtomicBoolean PLATFORM_STARTED = new AtomicBoolean(false);
@BeforeAll
static void setUpJavaFxPlatform() throws InterruptedException {
Platform.setImplicitExit(false);
CountDownLatch latch = new CountDownLatch(1);
try {
Platform.startup(() -> {
PLATFORM_STARTED.set(true);
latch.countDown();
});
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"JavaFX Platform must start within timeout");
} catch (IllegalStateException alreadyStarted) {
CountDownLatch verifyLatch = new CountDownLatch(1);
Platform.runLater(() -> {
PLATFORM_STARTED.set(true);
verifyLatch.countDown();
});
assertTrue(verifyLatch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"Existing JavaFX Platform must be reachable within timeout");
}
}
@AfterAll
static void tearDownJavaFxPlatform() {
// Shared platform do not call Platform.exit().
}
// =========================================================================
// Scenario: button is findable by CSS ID
// =========================================================================
/**
* Smoke test: after constructing a workspace, the "Technische Tests ausführen" button
* exists and carries the CSS ID {@code technical-tests-button}.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void technicalTestsButton_hasCssId() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws =
new GuiConfigurationEditorWorkspace(Optional.empty());
Button btn = ws.technicalTestsButton;
assertNotNull(btn, "technicalTestsButton must not be null");
assertEquals("technical-tests-button", btn.getId(),
"technicalTestsButton must carry CSS ID 'technical-tests-button'");
});
}
// =========================================================================
// Scenario: triggering populates pendingMessages with SOURCE_TAG entries
// =========================================================================
/**
* Smoke test: triggering the coordinator synchronously populates
* {@code pendingMessages} with at least one entry tagged with the source
* {@link GuiTechnicalTestCoordinator#SOURCE_TAG}.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void trigger_populatesPendingMessagesWithSourceTag() throws Exception {
runOnFx(() -> {
List<GuiMessageEntry> messages = new ArrayList<>();
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });
coordinator.triggerTechnicalTests();
long taggedCount = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
.count();
assertTrue(taggedCount > 0,
"Triggering 'Technische Tests ausführen' must add entries tagged '"
+ GuiTechnicalTestCoordinator.SOURCE_TAG + "'");
});
}
// =========================================================================
// Scenario: 11 checkpoint entries + 1 summary = 12 entries total
// =========================================================================
/**
* Smoke test: after one trigger, the number of entries tagged SOURCE_TAG equals
* 11 (one per checkpoint) plus 1 summary entry = 12.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void trigger_producesElevenCheckpointEntriesPlusSummary() throws Exception {
runOnFx(() -> {
List<GuiMessageEntry> messages = new ArrayList<>();
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });
coordinator.triggerTechnicalTests();
long taggedCount = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
.count();
// 11 checkpoint entries + 1 summary entry = 12
assertEquals(12, taggedCount,
"Expected 11 checkpoint entries + 1 summary entry = 12 tagged messages");
});
}
// =========================================================================
// Scenario: replace semantics second trigger replaces previous entries
// =========================================================================
/**
* Smoke test: triggering the coordinator twice replaces the previous SOURCE_TAG
* entries; the count remains the same as after a single trigger.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void trigger_twice_replacesPreviousTestEntries() throws Exception {
runOnFx(() -> {
List<GuiMessageEntry> messages = new ArrayList<>();
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });
coordinator.triggerTechnicalTests();
long countAfterFirst = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
.count();
coordinator.triggerTechnicalTests();
long countAfterSecond = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
.count();
assertEquals(countAfterFirst, countAfterSecond,
"Second trigger must replace (not append) the previous test entries");
});
}
// =========================================================================
// Scenario: post-result callback is invoked
// =========================================================================
/**
* Smoke test: the post-result callback is invoked after the result is applied.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void trigger_postResultCallbackIsInvoked() throws Exception {
runOnFx(() -> {
AtomicBoolean callbackInvoked = new AtomicBoolean(false);
List<GuiMessageEntry> messages = new ArrayList<>();
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(
messages, report -> callbackInvoked.set(true));
coordinator.triggerTechnicalTests();
assertTrue(callbackInvoked.get(),
"The post-result callback must be invoked after the technical tests finish");
});
}
// =========================================================================
// Scenario: input supplier is consulted at trigger time — reflects current (unsaved) state
// =========================================================================
/**
* Smoke test: the coordinator must read the current editor state at trigger time via the
* injected {@link java.util.function.Supplier}, not from a cached snapshot. This verifies
* that unsaved changes are always reflected in the technical test results.
*
* <p>The test wires the coordinator with a mutable {@link AtomicReference} as input supplier.
* Before the first trigger the supplier returns valid input; before the second trigger the
* supplier is updated to return input with an empty active-provider (which causes failures).
* After the second trigger the message list must contain ERROR entries, proving that the
* coordinator forwarded the new, unsaved input value rather than the previous one.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void trigger_readsCurrentInputFromSupplier_unsavedChangesAreReflected() throws Exception {
runOnFx(() -> {
List<GuiMessageEntry> messages = new ArrayList<>();
// Mutable supplier: starts with valid input, will be swapped before second trigger.
AtomicReference<EditorValidationInput> currentInput = new AtomicReference<>(
new EditorValidationInput(
"claude",
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
"3", "10", "500",
"https://api.anthropic.com", "claude-3-sonnet", "30",
EffectiveApiKeyDescriptor.absent(),
"https://api.openai.com", "gpt-4", "30",
EffectiveApiKeyDescriptor.absent()));
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
new EditorConfigurationValidator(),
noOpPathCheckPort(),
noOpProviderService());
GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator(
orchestrator,
currentInput::get, // always reads the current reference
() -> "",
messages,
report -> { });
coordinator.testThreadFactory = task -> new Thread(task, "sync-test-thread") {
@Override public void start() { run(); }
};
coordinator.resultDelivery = Runnable::run;
// First trigger with valid input — all failures are from path checks (no-op port),
// but no CONFIGURATION_BASIC_VALIDATION error because activeProvider is valid.
coordinator.triggerTechnicalTests();
long errorCountFirst = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
.filter(m -> m.severity() == GuiMessageSeverity.ERROR)
.count();
// Simulate unsaved edit: replace input with one having an empty active-provider.
currentInput.set(new EditorValidationInput(
"", // empty active provider → validation error in block 1
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
"3", "10", "500",
"https://api.anthropic.com", "claude-3-sonnet", "30",
EffectiveApiKeyDescriptor.absent(),
"https://api.openai.com", "gpt-4", "30",
EffectiveApiKeyDescriptor.absent()));
// Second trigger with the updated (unsaved) input.
coordinator.triggerTechnicalTests();
long errorCountSecond = messages.stream()
.filter(m -> m.source().isPresent()
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
.filter(m -> m.severity() == GuiMessageSeverity.ERROR)
.count();
// After the change, there must be at least as many errors as before,
// because the empty active-provider introduces additional validation errors.
assertTrue(errorCountSecond >= errorCountFirst,
"Second trigger with empty active-provider must produce at least as many "
+ "errors as the first trigger, proving the supplier is read at trigger time");
});
}
// =========================================================================
// Scenario: workspace button is wired to coordinator
// =========================================================================
/**
* Smoke test: after constructing a workspace, the button is not null and
* the coordinator is not null.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void workspace_buttonAndCoordinatorAreWired() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws =
new GuiConfigurationEditorWorkspace(Optional.empty());
assertNotNull(ws.technicalTestsButton,
"technicalTestsButton must not be null");
assertNotNull(ws.technicalTestCoordinator,
"technicalTestCoordinator must not be null");
});
}
// =========================================================================
// Helpers
// =========================================================================
/** No-op {@link PathCheckPort}: all checks return {@code false}. */
private static PathCheckPort noOpPathCheckPort() {
return new 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; }
};
}
/** No-op {@link ProviderTechnicalTestService}. */
private static ProviderTechnicalTestService noOpProviderService() {
return new ProviderTechnicalTestService(
req -> new ModelCatalogResult.IncompleteConfiguration(
req.providerIdentifier(), "kein Port im Test"),
(fam, pv) -> EffectiveApiKeyDescriptor.absent());
}
/**
* Builds a {@link GuiTechnicalTestCoordinator} that runs synchronously (inline thread
* factory, direct result delivery) so results are available immediately after
* {@link GuiTechnicalTestCoordinator#triggerTechnicalTests()} returns.
*
* @param messages mutable list to collect message entries
* @param postResultCallback callback to invoke after result is applied
* @return synchronously-wired coordinator
*/
private static GuiTechnicalTestCoordinator buildSyncCoordinator(
List<GuiMessageEntry> messages,
java.util.function.Consumer<TechnicalTestReport> postResultCallback) {
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
new EditorConfigurationValidator(),
noOpPathCheckPort(),
noOpProviderService());
EditorValidationInput blankInput = new EditorValidationInput(
"claude",
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
"3", "10", "2000",
"https://api.anthropic.com", "claude-3-sonnet", "30",
EffectiveApiKeyDescriptor.absent(),
"https://api.openai.com", "gpt-4", "30",
EffectiveApiKeyDescriptor.absent());
GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator(
orchestrator,
() -> blankInput,
() -> "",
messages,
postResultCallback);
// Override thread factory to run task inline (synchronous, no real thread spawn)
coordinator.testThreadFactory = task -> new Thread(task, "sync-test-thread") {
@Override
public void start() {
// Run the task inline instead of starting a new thread
run();
}
};
// Override result delivery to be synchronous (direct call)
coordinator.resultDelivery = Runnable::run;
return coordinator;
}
private static void runOnFx(ThrowingRunnable task) throws Exception {
AtomicReference<Throwable> error = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
task.run();
} catch (Throwable t) {
error.set(t);
} finally {
latch.countDown();
}
});
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"FX task must complete within timeout");
rethrow(error);
}
private static void rethrow(AtomicReference<Throwable> error) throws Exception {
Throwable t = error.get();
if (t == null) {
return;
}
if (t instanceof Exception ex) {
throw ex;
}
throw new AssertionError("Unexpected error", t);
}
@FunctionalInterface
private interface ThrowingRunnable {
void run() throws Exception;
}
}
@@ -791,7 +791,33 @@ class GuiUnsavedChangesGuardSmokeTest {
configFilePath -> GuiConfigurationTemplateFactory.createStandardTemplate(),
writer,
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());
(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"); }
}));
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(context);
ws.requestNewConfiguration();
return ws;
@@ -810,7 +836,33 @@ class GuiUnsavedChangesGuardSmokeTest {
configFilePath -> GuiConfigurationTemplateFactory.createStandardTemplate(),
writer,
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());
(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"); }
}));
return new GuiConfigurationEditorWorkspace(context);
}
@@ -0,0 +1,422 @@
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.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
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.GuiEditorValidationResult;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageSeverity;
import javafx.application.Platform;
import javafx.scene.control.Button;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
/**
* Monocle-based headless smoke tests for the explicit "Validieren" action.
* <p>
* Verifies that clicking the "Validieren" button in the "Tests" section executes
* the in-memory validation against the current editor state, reports the findings
* via the central message area, and neither writes any file nor triggers remote calls.
*
* <h2>Covered scenarios</h2>
* <ul>
* <li>Clicking "Validieren" with an incomplete configuration produces ERROR findings and
* an INFO message reporting the finding count.</li>
* <li>Clicking "Validieren" with a valid template configuration produces no ERRORs and
* an INFO message reporting "Keine Befunde." or a zero count.</li>
* <li>Clicking "Validieren" twice replaces the previous action-confirmation INFO message
* (replace semantics; the message appears exactly once).</li>
* <li>Clicking "Validieren" does not trigger any file write (the writer stub records no
* calls).</li>
* <li>The button is findable by its CSS ID {@code validate-button}.</li>
* </ul>
*
* <h2>Threading</h2>
* All workspace interactions run on the FX Application Thread via {@link Platform#runLater}.
* The Monocle headless configuration is activated by the Surefire JVM arguments.
*/
class GuiValidateActionSmokeTest {
private static final long FX_TIMEOUT_SECONDS = 10;
private static final AtomicBoolean PLATFORM_STARTED = new AtomicBoolean(false);
/** Source tag used by the validation action INFO message. */
private static final String ACTION_SOURCE = "Validierung-Aktion";
@BeforeAll
static void setUpJavaFxPlatform() throws InterruptedException {
Platform.setImplicitExit(false);
CountDownLatch latch = new CountDownLatch(1);
try {
Platform.startup(() -> {
PLATFORM_STARTED.set(true);
latch.countDown();
});
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"JavaFX Platform must start within timeout");
} catch (IllegalStateException alreadyStarted) {
CountDownLatch verifyLatch = new CountDownLatch(1);
Platform.runLater(() -> {
PLATFORM_STARTED.set(true);
verifyLatch.countDown();
});
assertTrue(verifyLatch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"Existing JavaFX Platform must be reachable within timeout");
}
}
@AfterAll
static void tearDownJavaFxPlatform() {
// Shared platform do not call Platform.exit().
}
// =========================================================================
// Scenario: button is findable by CSS ID
// =========================================================================
/**
* Smoke test: after constructing a workspace, the "Validieren" button exists
* and carries the CSS ID {@code validate-button}.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void validateButton_hasCssId() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws =
new GuiConfigurationEditorWorkspace(Optional.empty());
Button btn = ws.validateButton;
assertNotNull(btn, "validateButton must not be null");
assertEquals("validate-button", btn.getId(),
"validateButton must carry CSS ID 'validate-button'");
});
}
// =========================================================================
// Scenario: incomplete configuration → ERROR findings + INFO message with count
// =========================================================================
/**
* Smoke test: after clicking "Validieren" on a workspace whose editor state has
* an empty active-provider value, the last validation result contains at least one
* ERROR and the central message area contains an INFO message with source
* "Validierung-Aktion" that reports the number of findings.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void validateAction_incompleteConfiguration_producesErrorsAndInfoMessage() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws = buildWorkspace();
// Force an incomplete state: start with blank (no active provider).
// The blank start state already has an empty active provider → errors expected.
ws.validateButton.fire();
GuiEditorValidationResult result = ws.lastValidationResult();
assertTrue(result.hasErrors(),
"Clicking Validieren on incomplete config must produce ERROR findings");
List<GuiMessageEntry> actionMessages = ws.pendingMessages.stream()
.filter(m -> m.source().isPresent()
&& ACTION_SOURCE.equals(m.source().get()))
.toList();
assertEquals(1, actionMessages.size(),
"Exactly one action-confirmation INFO message must be present");
GuiMessageEntry msg = actionMessages.get(0);
assertEquals(GuiMessageSeverity.INFO, msg.severity(),
"Action-confirmation message must have INFO severity");
assertTrue(msg.text().startsWith("Aktion Validieren wurde ausgeführt."),
"Action-confirmation message text must start with expected prefix");
assertFalse(msg.text().contains("Keine Befunde"),
"With errors present the message must NOT say 'Keine Befunde'");
});
}
// =========================================================================
// Scenario: valid configuration → no ERRORs + INFO message "Keine Befunde."
// =========================================================================
/**
* Smoke test: after clicking "Validieren" on a workspace with the standard template
* values loaded, the last validation result contains no ERRORs and the action-confirmation
* message either contains "Keine Befunde." or a zero finding count.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void validateAction_validTemplate_noErrorsAndNoBefundeMessage() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws = buildWorkspace();
ws.requestNewConfiguration();
ws.validateButton.fire();
GuiEditorValidationResult result = ws.lastValidationResult();
assertFalse(result.hasErrors(),
"Clicking Validieren on standard template must produce no ERROR findings");
List<GuiMessageEntry> actionMessages = ws.pendingMessages.stream()
.filter(m -> m.source().isPresent()
&& ACTION_SOURCE.equals(m.source().get()))
.toList();
assertEquals(1, actionMessages.size(),
"Exactly one action-confirmation INFO message must be present");
GuiMessageEntry msg = actionMessages.get(0);
assertEquals(GuiMessageSeverity.INFO, msg.severity(),
"Action-confirmation message must have INFO severity");
// Template may have WARNINGs but no ERRORs. The fieldFindings count may be 0.
assertTrue(msg.text().startsWith("Aktion Validieren wurde ausgeführt."),
"Action-confirmation message text must start with expected prefix");
});
}
// =========================================================================
// Scenario: "Validieren" with unsaved (dirty) editor state — validates current content
// =========================================================================
/**
* Smoke test: when the editor holds unsaved changes that introduce a validation error,
* clicking "Validieren" must reflect the <em>current, modified (dirty)</em> editor state,
* not the previously saved or baseline state.
*
* <p>The test loads the standard template (which has a valid active-provider value and
* produces no errors), then directly overwrites the in-memory editor state with an
* equivalent state whose active-provider value is cleared. This mimics a user editing a
* field without saving. A subsequent click on "Validieren" must produce ERROR findings
* that originate from the unsaved change — proving that validation is always based on the
* current editor content.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void validateAction_withUnsavedDirtyChange_validatesCurrentState() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws = buildWorkspace();
// Load the template so the baseline is valid (no errors expected initially).
ws.requestNewConfiguration();
// Confirm: template produces no errors.
ws.validateButton.fire();
assertFalse(ws.lastValidationResult().hasErrors(),
"Standard template must produce no errors before the dirty change");
// Simulate an unsaved edit: replace the editor state with a version that has
// an empty active-provider value. The baseline remains the template values,
// so isDirty() returns true. The workspace field is package-private.
GuiConfigurationEditorState currentState = ws.editorState;
GuiConfigurationEditorState dirtyState = currentState.withValues(
currentState.values().withActiveProviderFamily(""));
ws.editorState = dirtyState;
// Confirm the editor is now dirty (sanity check).
assertTrue(ws.editorState.isDirty(),
"Editor state must be dirty after the unsaved change");
// Click "Validieren" again — must validate the dirty (unsaved) state.
ws.validateButton.fire();
assertTrue(ws.lastValidationResult().hasErrors(),
"Validieren must produce ERROR findings reflecting the unsaved dirty state, "
+ "not the previously clean baseline state");
});
}
// =========================================================================
// Scenario: clicking twice → message appears exactly once (replace semantics)
// =========================================================================
/**
* Smoke test: clicking "Validieren" twice must leave exactly one action-confirmation
* INFO message in the message list (the second click replaces the first).
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void validateAction_clickedTwice_infoMessageAppearsExactlyOnce() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws = buildWorkspace();
ws.validateButton.fire();
ws.validateButton.fire();
long count = ws.pendingMessages.stream()
.filter(m -> m.source().isPresent()
&& ACTION_SOURCE.equals(m.source().get()))
.count();
assertEquals(1, count,
"After two clicks the action-confirmation INFO message must appear exactly once");
});
}
// =========================================================================
// Scenario: clicking Validieren does not trigger a file write
// =========================================================================
/**
* Smoke test: clicking "Validieren" must not invoke the configuration file writer.
* The writer stub records whether it was called; it must remain uncalled after
* the button is fired.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void validateAction_doesNotTriggerFileWrite() throws Exception {
runOnFx(() -> {
AtomicBoolean writerCalled = new AtomicBoolean(false);
GuiConfigurationEditorState blankState =
GuiConfigurationEditorStateFactory.createBlankStartState();
GuiStartupContext ctx = new GuiStartupContext(
blankState,
Optional.empty(),
path -> blankState, // no-op loader
(values, path) -> {
writerCalled.set(true);
return GuiConfigurationSaveResult.saved(path);
},
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"); }
}));
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(ctx);
ws.validateButton.fire();
assertFalse(writerCalled.get(),
"Clicking Validieren must not invoke the file writer");
});
}
// =========================================================================
// Helpers
// =========================================================================
/**
* Builds a workspace with no-op loader/writer and absent API-key resolution,
* suitable for in-memory validation tests.
*/
private static GuiConfigurationEditorWorkspace buildWorkspace() {
GuiConfigurationEditorState blankState =
GuiConfigurationEditorStateFactory.createBlankStartState();
GuiStartupContext ctx = new GuiStartupContext(
blankState,
Optional.empty(),
path -> blankState,
(values, path) -> GuiConfigurationSaveResult.saved(path),
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"); }
}));
return new GuiConfigurationEditorWorkspace(ctx);
}
private static void runOnFx(ThrowingRunnable task) throws Exception {
AtomicReference<Throwable> error = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
task.run();
} catch (Throwable t) {
error.set(t);
} finally {
latch.countDown();
}
});
assertTrue(latch.await(FX_TIMEOUT_SECONDS, TimeUnit.SECONDS),
"FX task must complete within timeout");
rethrow(error);
}
private static void rethrow(AtomicReference<Throwable> error) throws Exception {
Throwable t = error.get();
if (t == null) {
return;
}
if (t instanceof Exception ex) {
throw ex;
}
throw new AssertionError("Unexpected error", t);
}
@FunctionalInterface
private interface ThrowingRunnable {
void run() throws Exception;
}
}
@@ -0,0 +1,77 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionPlan;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
/**
* Tests für {@link ConfirmationDialogContent}.
*/
class ConfirmationDialogContentTest {
@Test
void fromPlan_extractsDescriptionsInOrder() {
var s1 = new CorrectionSuggestion.CreateDirectory("/path/a", "Zielordner anlegen");
var s2 = new CorrectionSuggestion.CreatePromptFile("/path/prompt.txt", "Prompt-Datei erzeugen");
var plan = new CorrectionPlan(List.of(s1, s2));
var content = ConfirmationDialogContent.fromPlan(plan);
assertThat(content.correctionLines()).containsExactly(
"Zielordner anlegen",
"Prompt-Datei erzeugen");
assertThat(content.title()).isNotBlank();
assertThat(content.introText()).isNotBlank();
assertThat(content.hasCorrections()).isTrue();
}
@Test
void fromPlan_emptyPlan_hasNoCorrections() {
var content = ConfirmationDialogContent.fromPlan(CorrectionPlan.empty());
assertThat(content.hasCorrections()).isFalse();
assertThat(content.correctionLines()).isEmpty();
}
@Test
void fromPlan_nullPlanThrows() {
assertThatNullPointerException()
.isThrownBy(() -> ConfirmationDialogContent.fromPlan(null));
}
@Test
void correctionLinesAreImmutable() {
var content = new ConfirmationDialogContent("Titel", "Intro", new ArrayList<>(List.of("Zeile 1")));
assertThat(content.correctionLines()).hasSize(1);
}
@Test
void nullTitleThrows() {
assertThatNullPointerException()
.isThrownBy(() -> new ConfirmationDialogContent(null, "intro", List.of()));
}
@Test
void nullIntroTextThrows() {
assertThatNullPointerException()
.isThrownBy(() -> new ConfirmationDialogContent("title", null, List.of()));
}
@Test
void nullCorrectionLinesThrows() {
assertThatNullPointerException()
.isThrownBy(() -> new ConfirmationDialogContent("title", "intro", null));
}
@Test
void equality() {
var a = new ConfirmationDialogContent("T", "I", List.of("Z1"));
var b = new ConfirmationDialogContent("T", "I", List.of("Z1"));
assertThat(a).isEqualTo(b);
}
}