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:
+168
-8
@@ -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}.
|
||||
*
|
||||
|
||||
+287
@@ -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);
|
||||
}
|
||||
}
|
||||
+99
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+268
@@ -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);
|
||||
}
|
||||
}
|
||||
+80
@@ -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();
|
||||
}
|
||||
}
|
||||
+2
@@ -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
|
||||
|
||||
+27
-1
@@ -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);
|
||||
|
||||
|
||||
+318
@@ -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;
|
||||
}
|
||||
}
|
||||
+27
-1
@@ -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();
|
||||
|
||||
|
||||
+54
-2
@@ -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);
|
||||
|
||||
+135
-5
@@ -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(() -> {
|
||||
|
||||
+54
-2
@@ -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<>();
|
||||
|
||||
+108
-4
@@ -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(() -> {
|
||||
|
||||
+27
-1
@@ -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") {
|
||||
|
||||
+432
@@ -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;
|
||||
}
|
||||
}
|
||||
+54
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+422
@@ -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;
|
||||
}
|
||||
}
|
||||
+77
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user