GUI-Bugfixes: Defaults beim Start, kopierbare Meldungen mit Zeitstempel, Befundauflistung, Modell-ComboBox links, effektiver API-Key für Modellabruf
- Blank-Startzustand zeigt jetzt die Standardvorlage (wie nach "Neu"), neue Factory createEmptyStartState für Tests - Meldungsbereich ist per Kontextmenü bzw. Strg+C kopierbar - Jede Meldung trägt ein führendes [HH:mm:ss]-Präfix - Validieren- und Tests-Aktionen akkumulieren Meldungen, automatische Validierung ersetzt still ihre Einträge - Validieren-Meldung listet alle konkreten Befunde einzeln auf - Modell-ComboBox und manuelles Modellfeld sind linksbündig - ApiKeyResolutionPort liefert jetzt den effektiven API-Schlüsselwert (Default + Env-Adapter-Override), so dass der Modellliste-Test in den technischen Tests nicht mehr "API-Schlüssel fehlt" meldet, obwohl er gesetzt ist
This commit is contained in:
+129
-24
@@ -48,12 +48,16 @@ import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -328,7 +332,7 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
this.configurationFileLoader = effectiveContext.configurationFileLoader();
|
||||
this.configurationFileWriter = effectiveContext.configurationFileWriter();
|
||||
this.editorState = effectiveContext.initialState();
|
||||
this.welcomeGuidanceVisible = editorState.isNewConfiguration();
|
||||
this.welcomeGuidanceVisible = false;
|
||||
this.apiKeyResolutionPort = effectiveContext.apiKeyResolutionPort();
|
||||
|
||||
this.modelCatalogCoordinator = new GuiModelCatalogCoordinator(
|
||||
@@ -1587,10 +1591,12 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
claudeState.model(),
|
||||
claudeState.timeoutSeconds(),
|
||||
claudeKeyDescriptor,
|
||||
claudeState.apiKey().propertyValue(),
|
||||
openaiState.baseUrl(),
|
||||
openaiState.model(),
|
||||
openaiState.timeoutSeconds(),
|
||||
openaiKeyDescriptor);
|
||||
openaiKeyDescriptor,
|
||||
openaiState.apiKey().propertyValue());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1644,11 +1650,10 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
* 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.
|
||||
* In addition to updating the field-level findings shown by the automatic validation,
|
||||
* this action appends a dedicated INFO confirmation message plus one message per concrete
|
||||
* finding to the central message area. These entries accumulate across repeated clicks
|
||||
* so the user can compare successive runs.
|
||||
* <p>
|
||||
* Differences from the automatic background validation:
|
||||
* <ul>
|
||||
@@ -1663,24 +1668,35 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
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();
|
||||
EditorValidationInput input = buildValidationInput();
|
||||
EditorValidationReport report = editorValidator.validate(input);
|
||||
|
||||
// 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.";
|
||||
// Update field-level findings (drives red labels under problematic inputs).
|
||||
pendingFieldFindings.clear();
|
||||
for (EditorValidationFinding finding : report.findings()) {
|
||||
if (finding.hasFieldKey()) {
|
||||
pendingFieldFindings.add(new GuiFieldFinding(finding.fieldKey().orElseThrow(),
|
||||
toGuiSeverity(finding.severity()), finding.message()));
|
||||
}
|
||||
}
|
||||
|
||||
// Drop silent auto-validation entries so the central message area is not flooded
|
||||
// by keystroke-level background checks; explicit action entries always accumulate.
|
||||
pendingMessages.removeIf(m -> m.source().isPresent()
|
||||
&& "Validierung".equals(m.source().get()));
|
||||
|
||||
// Append a timestamped confirmation plus each concrete finding as its own entry.
|
||||
int findingCount = report.findings().size();
|
||||
String confirmationText = findingCount == 0
|
||||
? "Aktion Validieren wurde ausgeführt. Keine Befunde."
|
||||
: "Aktion Validieren wurde ausgeführt. "
|
||||
+ findingCount + " Befund" + (findingCount == 1 ? "" : "e") + " gefunden:";
|
||||
pendingMessages.add(GuiMessageEntry.of(
|
||||
GuiMessageSeverity.INFO, confirmationText, "Validierung-Aktion"));
|
||||
for (EditorValidationFinding finding : report.findings()) {
|
||||
pendingMessages.add(GuiMessageEntry.of(
|
||||
toGuiSeverity(finding.severity()), finding.message(), "Validierung-Aktion"));
|
||||
}
|
||||
|
||||
lastValidationResult = new GuiEditorValidationResult(
|
||||
List.copyOf(pendingMessages),
|
||||
@@ -1727,13 +1743,19 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
* list.
|
||||
* <p>
|
||||
* Each message is rendered as one {@link TextFlow} row. The severity prefix is coloured using
|
||||
* the CSS colour from {@link GuiMessageSeverity#getPrefixCssColour()}; the remainder of the
|
||||
* message text is always black. The placeholder text is shown when the list is empty.
|
||||
* the CSS colour from {@link GuiMessageSeverity#getPrefixCssColour()} and carries the message
|
||||
* timestamp in {@code [HH:mm:ss]} form; the remainder of the message text is always black.
|
||||
* The placeholder text is shown when the list is empty.
|
||||
* <p>
|
||||
* Each message row exposes a context menu with a "Kopieren" action that copies the full line
|
||||
* (timestamp, severity label, body text) to the system clipboard. The container also offers
|
||||
* an "Alles kopieren" menu entry that concatenates all current messages in display order.
|
||||
* <p>
|
||||
* Must be called on the JavaFX Application Thread.
|
||||
*/
|
||||
void refreshMessagesArea() {
|
||||
messagesAreaBox.getChildren().clear();
|
||||
attachAllCopyContextMenu();
|
||||
if (pendingMessages.isEmpty()) {
|
||||
Text placeholder = new Text("Keine Meldungen vorhanden.");
|
||||
placeholder.setStyle("-fx-fill: #888888;");
|
||||
@@ -1741,17 +1763,100 @@ public final class GuiConfigurationEditorWorkspace {
|
||||
return;
|
||||
}
|
||||
for (GuiMessageEntry entry : pendingMessages) {
|
||||
Text prefix = new Text(entry.severity().getPrefixLabel() + " ");
|
||||
String timestampLabel = formatTimestamp(entry.timestamp());
|
||||
String severityLabel = entry.severity().getPrefixLabel();
|
||||
Text prefix = new Text(timestampLabel + " " + severityLabel + " ");
|
||||
prefix.setStyle("-fx-fill: " + entry.severity().getPrefixCssColour() + ";"
|
||||
+ " -fx-font-weight: bold;");
|
||||
Text body = new Text(entry.text());
|
||||
body.setStyle("-fx-fill: black;");
|
||||
TextFlow row = new TextFlow(prefix, body);
|
||||
row.setStyle("-fx-padding: 1px 4px;");
|
||||
String fullLine = timestampLabel + " " + severityLabel + " " + entry.text();
|
||||
attachRowCopyContextMenu(row, fullLine);
|
||||
messagesAreaBox.getChildren().add(row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a context menu on the messages container that copies all rendered lines to the
|
||||
* system clipboard. Each rendered row receives its own context menu; this menu handles
|
||||
* clicks on empty areas of the container.
|
||||
*/
|
||||
private void attachAllCopyContextMenu() {
|
||||
ContextMenu menu = new ContextMenu();
|
||||
MenuItem copyAll = new MenuItem("Alles kopieren");
|
||||
copyAll.setOnAction(e -> copyAllMessagesToClipboard());
|
||||
menu.getItems().add(copyAll);
|
||||
messagesAreaBox.setOnContextMenuRequested(e -> {
|
||||
if (!pendingMessages.isEmpty()) {
|
||||
menu.show(messagesAreaBox, e.getScreenX(), e.getScreenY());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a per-row context menu that copies the row's full textual content and also
|
||||
* offers the container-level "Alles kopieren" action.
|
||||
*
|
||||
* @param row the message row node; must not be {@code null}
|
||||
* @param fullLine the full text of this row to copy when "Kopieren" is selected
|
||||
*/
|
||||
private void attachRowCopyContextMenu(TextFlow row, String fullLine) {
|
||||
ContextMenu menu = new ContextMenu();
|
||||
MenuItem copyRow = new MenuItem("Kopieren");
|
||||
copyRow.setOnAction(e -> writeToClipboard(fullLine));
|
||||
MenuItem copyAll = new MenuItem("Alles kopieren");
|
||||
copyAll.setOnAction(e -> copyAllMessagesToClipboard());
|
||||
menu.getItems().addAll(copyRow, copyAll);
|
||||
row.setOnContextMenuRequested(e ->
|
||||
menu.show(row, e.getScreenX(), e.getScreenY()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all current message lines to the system clipboard in display order.
|
||||
*/
|
||||
private void copyAllMessagesToClipboard() {
|
||||
if (pendingMessages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (GuiMessageEntry entry : pendingMessages) {
|
||||
sb.append(formatTimestamp(entry.timestamp()))
|
||||
.append(" ")
|
||||
.append(entry.severity().getPrefixLabel())
|
||||
.append(" ")
|
||||
.append(entry.text())
|
||||
.append(System.lineSeparator());
|
||||
}
|
||||
writeToClipboard(sb.toString().trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes plain text to the system clipboard.
|
||||
*
|
||||
* @param text the text to copy; {@code null} is treated as empty
|
||||
*/
|
||||
private void writeToClipboard(String text) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(text == null ? "" : text);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a message timestamp as {@code [HH:mm:ss]} in the system default zone.
|
||||
*
|
||||
* @param instant the source instant; must not be {@code null}
|
||||
* @return the formatted prefix; never {@code null}
|
||||
*/
|
||||
private static String formatTimestamp(java.time.Instant instant) {
|
||||
return "[" + TIMESTAMP_FORMATTER.format(instant) + "]";
|
||||
}
|
||||
|
||||
private static final java.time.format.DateTimeFormatter TIMESTAMP_FORMATTER =
|
||||
java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")
|
||||
.withZone(java.time.ZoneId.systemDefault());
|
||||
|
||||
// =========================================================================
|
||||
// Field-level error label rendering
|
||||
// =========================================================================
|
||||
|
||||
+4
-5
@@ -145,17 +145,16 @@ public final class GuiTechnicalTestCoordinator {
|
||||
/**
|
||||
* 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.
|
||||
* Fügt für jedes Checkpoint-Ergebnis einen neuen Eintrag zur geteilten Nachrichtenliste
|
||||
* hinzu; vorhandene Einträge bleiben erhalten, sodass die Meldungen über mehrere
|
||||
* Testläufe hinweg akkumulieren. 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("")));
|
||||
// Akkumulieren: Vorherige Einträge anderer Läufe bleiben erhalten.
|
||||
|
||||
long successCount = 0;
|
||||
long failureErrorCount = 0;
|
||||
|
||||
+18
-5
@@ -48,15 +48,28 @@ public final class GuiConfigurationTemplateFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the empty editor state used when the GUI starts without a loaded configuration.
|
||||
* Creates the editor state used when the GUI starts without a loaded configuration.
|
||||
* <p>
|
||||
* This start state intentionally does not show the standard template yet. The template
|
||||
* is reserved for the explicit {@code Neu} action so the GUI starts without an implicit
|
||||
* draft and only shows the welcome guidance until the user requests a new configuration.
|
||||
* The start state contains the standard configuration template so the GUI shows the
|
||||
* default values immediately, equivalent to the explicit {@code Neu} action having been
|
||||
* triggered. No file snapshot is associated with the state.
|
||||
*
|
||||
* @return a clean editor state without a loaded file snapshot and without template values
|
||||
* @return a clean editor state with the standard template values and no loaded file snapshot
|
||||
*/
|
||||
public static GuiConfigurationEditorState createBlankStartState() {
|
||||
return createStandardTemplate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a truly empty editor state without any template values.
|
||||
* <p>
|
||||
* This factory is reserved for tests that intentionally need an editor state with empty
|
||||
* field values and no provider configurations. Production startup uses
|
||||
* {@link #createBlankStartState()} which returns the standard template instead.
|
||||
*
|
||||
* @return a clean editor state without any template values
|
||||
*/
|
||||
public static GuiConfigurationEditorState createEmptyStartState() {
|
||||
GuiConfigurationValues blankValues = new GuiConfigurationValues(
|
||||
"",
|
||||
"",
|
||||
|
||||
+2
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.TextField;
|
||||
@@ -65,6 +66,7 @@ public final class GuiModelFieldContainer extends StackPane {
|
||||
|
||||
// Initial state: show text field (NOT_YET_LOADED → manual input)
|
||||
applyVisibility(false);
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
getChildren().addAll(comboBox, textField);
|
||||
}
|
||||
|
||||
|
||||
+5
-7
@@ -206,14 +206,14 @@ class GuiAdapterSmokeTest {
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Verifies that the editor workspace starts without a loaded configuration, shows the
|
||||
* welcome guidance, and exposes the fixed GUI structure of the current shell.
|
||||
* Verifies that the editor workspace starts without a loaded configuration, immediately
|
||||
* shows the standard template defaults, and exposes the fixed GUI structure of the current shell.
|
||||
*
|
||||
* @throws Exception if the FX thread task fails or times out
|
||||
*/
|
||||
@Test
|
||||
@Order(5)
|
||||
void editorWorkspace_startStateShowsEmptyHeaderWelcomeGuidanceAndOneTab() throws Exception {
|
||||
void editorWorkspace_startStateShowsEmptyHeaderDefaultsAndOneTab() throws Exception {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicReference<Throwable> fxError = new AtomicReference<>();
|
||||
AtomicReference<GuiConfigurationEditorWorkspace> workspaceReference = new AtomicReference<>();
|
||||
@@ -225,10 +225,8 @@ class GuiAdapterSmokeTest {
|
||||
|
||||
assertEquals("", workspace.configurationPathText(),
|
||||
"The header path must stay empty before any configuration is loaded");
|
||||
assertTrue(workspace.isWelcomeGuidanceVisible(),
|
||||
"The welcome guidance must be visible in the unloaded start state");
|
||||
assertTrue(workspace.welcomeText().contains("Willkommen"),
|
||||
"The welcome text must be shown in German");
|
||||
assertFalse(workspace.isWelcomeGuidanceVisible(),
|
||||
"The welcome guidance must stay hidden because the standard template is shown immediately");
|
||||
assertNotNull(workspace.root(),
|
||||
"The workspace root must be available");
|
||||
assertEquals("Neu", workspace.newButton().getText(),
|
||||
|
||||
+12
-12
@@ -196,14 +196,14 @@ class GuiEditorIntegrationTest {
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Verifies that starting the GUI without a {@code --config} argument produces the defined
|
||||
* blank welcome state: header path is empty, welcome guidance is visible, and the editor is
|
||||
* not in dirty state.
|
||||
* Verifies that starting the GUI without a {@code --config} argument shows the standard
|
||||
* template defaults immediately: header path is empty, welcome guidance is hidden, the
|
||||
* editor is not in dirty state, and the standard default values are populated.
|
||||
*
|
||||
* @throws Exception if the FX thread task fails or times out
|
||||
*/
|
||||
@Test
|
||||
void guiStartup_withoutConfigPath_showsBlankWelcomeState() throws Exception {
|
||||
void guiStartup_withoutConfigPath_showsStandardTemplateDefaults() throws Exception {
|
||||
GuiStartupContext blankContext = GuiStartupContext.blank(Optional.empty());
|
||||
|
||||
AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
@@ -215,14 +215,14 @@ class GuiEditorIntegrationTest {
|
||||
|
||||
assertEquals("", workspace.configurationPathText(),
|
||||
"Header path must be empty when no configuration is loaded");
|
||||
assertTrue(workspace.isWelcomeGuidanceVisible(),
|
||||
"Welcome guidance must be visible when no configuration is loaded");
|
||||
assertFalse(workspace.isWelcomeGuidanceVisible(),
|
||||
"Welcome guidance must stay hidden because the standard template is shown immediately");
|
||||
assertFalse(workspace.editorState().hasLoadedFileSnapshot(),
|
||||
"Editor state must have no file snapshot in blank start state");
|
||||
"Editor state must have no file snapshot in default start state");
|
||||
assertFalse(workspace.editorState().isDirty(),
|
||||
"Blank start state must not be dirty");
|
||||
assertTrue(workspace.welcomeText().contains("Willkommen"),
|
||||
"Welcome text must be shown in German");
|
||||
"Default start state must not be dirty");
|
||||
assertEquals("./work/local/source", workspace.editorState().values().sourceFolder(),
|
||||
"Default start state must populate the standard source folder");
|
||||
|
||||
} catch (Throwable t) {
|
||||
error.set(t);
|
||||
@@ -302,8 +302,8 @@ class GuiEditorIntegrationTest {
|
||||
try {
|
||||
GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(context);
|
||||
|
||||
assertTrue(workspace.isWelcomeGuidanceVisible(),
|
||||
"Welcome guidance must be visible when config path does not exist");
|
||||
assertFalse(workspace.isWelcomeGuidanceVisible(),
|
||||
"Welcome guidance must stay hidden because the standard template is shown immediately");
|
||||
assertEquals("", workspace.configurationPathText(),
|
||||
"Header path must be empty when config file was not found");
|
||||
assertFalse(workspace.editorState().hasLoadedFileSnapshot(),
|
||||
|
||||
+16
-14
@@ -97,28 +97,29 @@ class GuiEditorRegressionSmokeTest {
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Regression: starting without a configuration produces the blank welcome state.
|
||||
* Regression: starting without a configuration immediately shows the standard template defaults.
|
||||
* <p>
|
||||
* The workspace must display the welcome guidance, the header path must be empty, and
|
||||
* the editor state must not have a file snapshot. "Neu" and "Öffnen" must be present.
|
||||
* The workspace must keep the welcome guidance hidden because the standard template values
|
||||
* are populated right away. The header path stays empty and no file snapshot is associated
|
||||
* with the editor state. "Neu" and "Öffnen" must be present.
|
||||
*
|
||||
* @throws Exception if the FX thread task fails or times out
|
||||
*/
|
||||
@Test
|
||||
@Order(1)
|
||||
void guiStart_withoutConfig_showsBlankWelcomeStateAndExposesNeuAndOeffnenButtons()
|
||||
void guiStart_withoutConfig_showsStandardTemplateDefaultsAndExposesNeuAndOeffnenButtons()
|
||||
throws Exception {
|
||||
runOnFx(() -> {
|
||||
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(Optional.empty());
|
||||
|
||||
assertTrue(ws.isWelcomeGuidanceVisible(),
|
||||
"Welcome guidance must be visible on blank start");
|
||||
assertFalse(ws.isWelcomeGuidanceVisible(),
|
||||
"Welcome guidance must stay hidden because the standard template is shown immediately");
|
||||
assertEquals("", ws.configurationPathText(),
|
||||
"Header path must be empty on blank start");
|
||||
"Header path must be empty on default start");
|
||||
assertFalse(ws.editorState().hasLoadedFileSnapshot(),
|
||||
"No file snapshot must exist on blank start");
|
||||
"No file snapshot must exist on default start");
|
||||
assertFalse(ws.editorState().isDirty(),
|
||||
"Blank start state must not be dirty");
|
||||
"Default start state must not be dirty");
|
||||
assertEquals("Neu", ws.newButton().getText(),
|
||||
"'Neu' button must be present");
|
||||
assertEquals("Öffnen", ws.openButton().getText(),
|
||||
@@ -131,23 +132,24 @@ class GuiEditorRegressionSmokeTest {
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Regression: "Neu" switches the workspace to the standard template, hides the welcome
|
||||
* guidance, and leaves the state clean with all template fields populated.
|
||||
* Regression: "Neu" reloads the standard template values, keeps the welcome guidance
|
||||
* hidden, and leaves the state clean with all template fields populated.
|
||||
*
|
||||
* @throws Exception if the FX thread task fails or times out
|
||||
*/
|
||||
@Test
|
||||
@Order(2)
|
||||
void neu_withStandardTemplate_populatesFieldsAndHidesWelcomeGuidance() throws Exception {
|
||||
void neu_withStandardTemplate_populatesFieldsAndKeepsWelcomeHidden() throws Exception {
|
||||
runOnFx(() -> {
|
||||
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(Optional.empty());
|
||||
|
||||
assertTrue(ws.isWelcomeGuidanceVisible(), "Precondition: welcome must be visible");
|
||||
assertFalse(ws.isWelcomeGuidanceVisible(),
|
||||
"Precondition: welcome must already be hidden because the start state shows defaults");
|
||||
|
||||
ws.requestNewConfiguration();
|
||||
|
||||
assertFalse(ws.isWelcomeGuidanceVisible(),
|
||||
"Welcome guidance must be hidden after 'Neu'");
|
||||
"Welcome guidance must remain hidden after 'Neu'");
|
||||
assertEquals("", ws.editorState().configurationPathText(),
|
||||
"Path must remain empty after 'Neu' (no file saved yet)");
|
||||
assertFalse(ws.editorState().isDirty(),
|
||||
|
||||
+14
-13
@@ -39,7 +39,7 @@ import javafx.scene.control.Button;
|
||||
* {@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>A second trigger appends a fresh batch of test entries (accumulation semantics).</li>
|
||||
* <li>The post-result callback is invoked after the result is applied.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
@@ -162,17 +162,18 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Scenario: replace semantics – second trigger replaces previous entries
|
||||
// Scenario: accumulation semantics – second trigger appends fresh entries
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Smoke test: triggering the coordinator twice replaces the previous SOURCE_TAG
|
||||
* entries; the count remains the same as after a single trigger.
|
||||
* Smoke test: triggering the coordinator twice accumulates both runs; the
|
||||
* second trigger appends a fresh batch of SOURCE_TAG entries without
|
||||
* removing the first batch.
|
||||
*
|
||||
* @throws Exception if the FX thread task fails or times out
|
||||
*/
|
||||
@Test
|
||||
void trigger_twice_replacesPreviousTestEntries() throws Exception {
|
||||
void trigger_twice_accumulatesTestEntries() throws Exception {
|
||||
runOnFx(() -> {
|
||||
List<GuiMessageEntry> messages = new ArrayList<>();
|
||||
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });
|
||||
@@ -189,8 +190,8 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
||||
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
|
||||
.count();
|
||||
|
||||
assertEquals(countAfterFirst, countAfterSecond,
|
||||
"Second trigger must replace (not append) the previous test entries");
|
||||
assertEquals(countAfterFirst * 2, countAfterSecond,
|
||||
"Second trigger must append a fresh batch, doubling the SOURCE_TAG entries");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,9 +248,9 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||
"3", "10", "500",
|
||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||
EffectiveApiKeyDescriptor.absent(),
|
||||
EffectiveApiKeyDescriptor.absent(), "",
|
||||
"https://api.openai.com", "gpt-4", "30",
|
||||
EffectiveApiKeyDescriptor.absent()));
|
||||
EffectiveApiKeyDescriptor.absent(), ""));
|
||||
|
||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||
new EditorConfigurationValidator(),
|
||||
@@ -283,9 +284,9 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||
"3", "10", "500",
|
||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||
EffectiveApiKeyDescriptor.absent(),
|
||||
EffectiveApiKeyDescriptor.absent(), "",
|
||||
"https://api.openai.com", "gpt-4", "30",
|
||||
EffectiveApiKeyDescriptor.absent()));
|
||||
EffectiveApiKeyDescriptor.absent(), ""));
|
||||
|
||||
// Second trigger with the updated (unsaved) input.
|
||||
coordinator.triggerTechnicalTests();
|
||||
@@ -370,9 +371,9 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||
"3", "10", "2000",
|
||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||
EffectiveApiKeyDescriptor.absent(),
|
||||
EffectiveApiKeyDescriptor.absent(), "",
|
||||
"https://api.openai.com", "gpt-4", "30",
|
||||
EffectiveApiKeyDescriptor.absent());
|
||||
EffectiveApiKeyDescriptor.absent(), "");
|
||||
|
||||
GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator(
|
||||
orchestrator,
|
||||
|
||||
+50
-37
@@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
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.GuiConfigurationTemplateFactory;
|
||||
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;
|
||||
@@ -34,11 +35,11 @@ import javafx.scene.control.Button;
|
||||
* <h2>Covered scenarios</h2>
|
||||
* <ul>
|
||||
* <li>Clicking "Validieren" with an incomplete configuration produces ERROR findings and
|
||||
* an INFO message reporting the finding count.</li>
|
||||
* an INFO message reporting the finding count plus one entry per concrete finding.</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" twice appends a second action-confirmation INFO message
|
||||
* (accumulation semantics; each click adds a fresh snapshot).</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>
|
||||
@@ -112,8 +113,8 @@ class GuiValidateActionSmokeTest {
|
||||
/**
|
||||
* 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.
|
||||
* ERROR and the central message area contains one INFO confirmation with source
|
||||
* "Validierung-Aktion" plus one entry per concrete finding with the same source.
|
||||
*
|
||||
* @throws Exception if the FX thread task fails or times out
|
||||
*/
|
||||
@@ -122,8 +123,9 @@ class GuiValidateActionSmokeTest {
|
||||
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.
|
||||
// Force an incomplete state: replace the editor state with a truly empty one
|
||||
// (no active provider, no template values) so validation produces errors.
|
||||
ws.editorState = GuiConfigurationTemplateFactory.createEmptyStartState();
|
||||
|
||||
ws.validateButton.fire();
|
||||
|
||||
@@ -136,15 +138,17 @@ class GuiValidateActionSmokeTest {
|
||||
&& 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."),
|
||||
assertFalse(actionMessages.isEmpty(),
|
||||
"At least one action message must be present");
|
||||
GuiMessageEntry confirmation = actionMessages.get(0);
|
||||
assertEquals(GuiMessageSeverity.INFO, confirmation.severity(),
|
||||
"First action message must be the INFO confirmation");
|
||||
assertTrue(confirmation.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'");
|
||||
assertFalse(confirmation.text().contains("Keine Befunde"),
|
||||
"With errors present the confirmation must NOT say 'Keine Befunde'");
|
||||
assertTrue(actionMessages.size() > 1,
|
||||
"Each concrete finding must be listed as its own action message");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,13 +180,13 @@ class GuiValidateActionSmokeTest {
|
||||
&& 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");
|
||||
assertFalse(actionMessages.isEmpty(),
|
||||
"At least one action message must be present");
|
||||
GuiMessageEntry confirmation = actionMessages.get(0);
|
||||
assertEquals(GuiMessageSeverity.INFO, confirmation.severity(),
|
||||
"First action message must be the INFO confirmation");
|
||||
// Template may have WARNINGs but no ERRORs. The fieldFindings count may be 0.
|
||||
assertTrue(msg.text().startsWith("Aktion Validieren wurde ausgeführt."),
|
||||
assertTrue(confirmation.text().startsWith("Aktion Validieren wurde ausgeführt."),
|
||||
"Action-confirmation message text must start with expected prefix");
|
||||
});
|
||||
}
|
||||
@@ -244,25 +248,27 @@ class GuiValidateActionSmokeTest {
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Smoke test: clicking "Validieren" twice must leave exactly one action-confirmation
|
||||
* INFO message in the message list (the second click replaces the first).
|
||||
* Smoke test: clicking "Validieren" twice must leave two action-confirmation
|
||||
* INFO messages in the message list (accumulation semantics — each click appends
|
||||
* a fresh snapshot of findings).
|
||||
*
|
||||
* @throws Exception if the FX thread task fails or times out
|
||||
*/
|
||||
@Test
|
||||
void validateAction_clickedTwice_infoMessageAppearsExactlyOnce() throws Exception {
|
||||
void validateAction_clickedTwice_infoMessageAppearsTwice() throws Exception {
|
||||
runOnFx(() -> {
|
||||
GuiConfigurationEditorWorkspace ws = buildWorkspace();
|
||||
|
||||
ws.validateButton.fire();
|
||||
ws.validateButton.fire();
|
||||
|
||||
long count = ws.pendingMessages.stream()
|
||||
long confirmationCount = ws.pendingMessages.stream()
|
||||
.filter(m -> m.source().isPresent()
|
||||
&& ACTION_SOURCE.equals(m.source().get()))
|
||||
.filter(m -> m.text().startsWith("Aktion Validieren wurde ausgeführt."))
|
||||
.count();
|
||||
assertEquals(1, count,
|
||||
"After two clicks the action-confirmation INFO message must appear exactly once");
|
||||
assertEquals(2, confirmationCount,
|
||||
"After two clicks two action-confirmation INFO messages must be present");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -296,12 +302,10 @@ class GuiValidateActionSmokeTest {
|
||||
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(),
|
||||
noOpApiKeyResolutionPort(),
|
||||
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()),
|
||||
noOpApiKeyResolutionPort()),
|
||||
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; }
|
||||
@@ -318,7 +322,7 @@ class GuiValidateActionSmokeTest {
|
||||
},
|
||||
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())),
|
||||
noOpApiKeyResolutionPort())),
|
||||
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"); }
|
||||
@@ -338,6 +342,17 @@ class GuiValidateActionSmokeTest {
|
||||
// Helpers
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Returns a no-op {@link de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort}
|
||||
* that always reports {@code ABSENT}. Used for tests that do not exercise actual API-key
|
||||
* resolution.
|
||||
*/
|
||||
private static de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort
|
||||
noOpApiKeyResolutionPort() {
|
||||
return (family, propertyValue) -> de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
|
||||
.EffectiveApiKeyDescriptor.absent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a workspace with no-op loader/writer and absent API-key resolution,
|
||||
* suitable for in-memory validation tests.
|
||||
@@ -354,12 +369,10 @@ class GuiValidateActionSmokeTest {
|
||||
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(),
|
||||
noOpApiKeyResolutionPort(),
|
||||
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()),
|
||||
noOpApiKeyResolutionPort()),
|
||||
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; }
|
||||
@@ -376,7 +389,7 @@ class GuiValidateActionSmokeTest {
|
||||
},
|
||||
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())),
|
||||
noOpApiKeyResolutionPort())),
|
||||
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"); }
|
||||
|
||||
+17
-1
@@ -58,7 +58,7 @@ class GuiConfigurationTemplateFactoryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createBlankStartState_startsWithoutLoadedConfigurationAndWithoutTemplateValues() {
|
||||
void createBlankStartState_returnsStandardTemplateValuesWithoutLoadedFile() {
|
||||
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createBlankStartState();
|
||||
|
||||
assertFalse(state.isDirty());
|
||||
@@ -66,6 +66,22 @@ class GuiConfigurationTemplateFactoryTest {
|
||||
assertTrue(state.isNewConfiguration());
|
||||
assertEquals("", state.configurationPathText());
|
||||
|
||||
GuiConfigurationValues values = state.values();
|
||||
assertEquals("./work/local/source", values.sourceFolder());
|
||||
assertEquals("./work/local/target", values.targetFolder());
|
||||
assertEquals(AiProviderFamily.CLAUDE.getIdentifier(), values.activeProviderFamily());
|
||||
assertFalse(values.providerConfigurations().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createEmptyStartState_startsWithoutLoadedConfigurationAndWithoutTemplateValues() {
|
||||
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createEmptyStartState();
|
||||
|
||||
assertFalse(state.isDirty());
|
||||
assertFalse(state.hasLoadedFileSnapshot());
|
||||
assertTrue(state.isNewConfiguration());
|
||||
assertEquals("", state.configurationPathText());
|
||||
|
||||
GuiConfigurationValues values = state.values();
|
||||
assertEquals("", values.sourceFolder());
|
||||
assertEquals("", values.targetFolder());
|
||||
|
||||
Reference in New Issue
Block a user