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.ButtonType;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.ContextMenu;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.Separator;
|
import javafx.scene.control.Separator;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.control.TabPane;
|
import javafx.scene.control.TabPane;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
@@ -328,7 +332,7 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
this.configurationFileLoader = effectiveContext.configurationFileLoader();
|
this.configurationFileLoader = effectiveContext.configurationFileLoader();
|
||||||
this.configurationFileWriter = effectiveContext.configurationFileWriter();
|
this.configurationFileWriter = effectiveContext.configurationFileWriter();
|
||||||
this.editorState = effectiveContext.initialState();
|
this.editorState = effectiveContext.initialState();
|
||||||
this.welcomeGuidanceVisible = editorState.isNewConfiguration();
|
this.welcomeGuidanceVisible = false;
|
||||||
this.apiKeyResolutionPort = effectiveContext.apiKeyResolutionPort();
|
this.apiKeyResolutionPort = effectiveContext.apiKeyResolutionPort();
|
||||||
|
|
||||||
this.modelCatalogCoordinator = new GuiModelCatalogCoordinator(
|
this.modelCatalogCoordinator = new GuiModelCatalogCoordinator(
|
||||||
@@ -1587,10 +1591,12 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
claudeState.model(),
|
claudeState.model(),
|
||||||
claudeState.timeoutSeconds(),
|
claudeState.timeoutSeconds(),
|
||||||
claudeKeyDescriptor,
|
claudeKeyDescriptor,
|
||||||
|
claudeState.apiKey().propertyValue(),
|
||||||
openaiState.baseUrl(),
|
openaiState.baseUrl(),
|
||||||
openaiState.model(),
|
openaiState.model(),
|
||||||
openaiState.timeoutSeconds(),
|
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
|
* 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.
|
* safe to call on the FX Application Thread at any time.
|
||||||
* <p>
|
* <p>
|
||||||
* In addition to updating the findings shown by the automatic validation, this action
|
* In addition to updating the field-level findings shown by the automatic validation,
|
||||||
* appends a dedicated INFO message to the central message area to confirm to the user
|
* this action appends a dedicated INFO confirmation message plus one message per concrete
|
||||||
* that the action was explicitly executed and to report the number of findings found.
|
* finding to the central message area. These entries accumulate across repeated clicks
|
||||||
* The message uses a distinct source tag so that it can be replaced on subsequent
|
* so the user can compare successive runs.
|
||||||
* executions without removing messages from other sources.
|
|
||||||
* <p>
|
* <p>
|
||||||
* Differences from the automatic background validation:
|
* Differences from the automatic background validation:
|
||||||
* <ul>
|
* <ul>
|
||||||
@@ -1663,24 +1668,35 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
private void runValidationAction() {
|
private void runValidationAction() {
|
||||||
LOG.info("Aktion Validieren ausgeführt.");
|
LOG.info("Aktion Validieren ausgeführt.");
|
||||||
|
|
||||||
// Re-run in-memory validation; this updates pendingMessages, pendingFieldFindings
|
EditorValidationInput input = buildValidationInput();
|
||||||
// and lastValidationResult identically to the automatic background check.
|
EditorValidationReport report = editorValidator.validate(input);
|
||||||
runEditorValidation();
|
|
||||||
|
|
||||||
// Replace any previous action-confirmation message; preserve all other messages.
|
// Update field-level findings (drives red labels under problematic inputs).
|
||||||
pendingMessages.removeIf(m -> m.source().isPresent()
|
pendingFieldFindings.clear();
|
||||||
&& "Validierung-Aktion".equals(m.source().get()));
|
for (EditorValidationFinding finding : report.findings()) {
|
||||||
|
if (finding.hasFieldKey()) {
|
||||||
int findingCount = lastValidationResult.fieldFindings().size();
|
pendingFieldFindings.add(new GuiFieldFinding(finding.fieldKey().orElseThrow(),
|
||||||
String confirmationText;
|
toGuiSeverity(finding.severity()), finding.message()));
|
||||||
if (findingCount == 0) {
|
|
||||||
confirmationText = "Aktion Validieren wurde ausgeführt. Keine Befunde.";
|
|
||||||
} else {
|
|
||||||
confirmationText = "Aktion Validieren wurde ausgeführt. "
|
|
||||||
+ findingCount + " Befund" + (findingCount == 1 ? "" : "e") + " gefunden.";
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
pendingMessages.add(GuiMessageEntry.of(
|
||||||
GuiMessageSeverity.INFO, confirmationText, "Validierung-Aktion"));
|
GuiMessageSeverity.INFO, confirmationText, "Validierung-Aktion"));
|
||||||
|
for (EditorValidationFinding finding : report.findings()) {
|
||||||
|
pendingMessages.add(GuiMessageEntry.of(
|
||||||
|
toGuiSeverity(finding.severity()), finding.message(), "Validierung-Aktion"));
|
||||||
|
}
|
||||||
|
|
||||||
lastValidationResult = new GuiEditorValidationResult(
|
lastValidationResult = new GuiEditorValidationResult(
|
||||||
List.copyOf(pendingMessages),
|
List.copyOf(pendingMessages),
|
||||||
@@ -1727,13 +1743,19 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
* list.
|
* list.
|
||||||
* <p>
|
* <p>
|
||||||
* Each message is rendered as one {@link TextFlow} row. The severity prefix is coloured using
|
* 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
|
* the CSS colour from {@link GuiMessageSeverity#getPrefixCssColour()} and carries the message
|
||||||
* message text is always black. The placeholder text is shown when the list is empty.
|
* 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>
|
* <p>
|
||||||
* Must be called on the JavaFX Application Thread.
|
* Must be called on the JavaFX Application Thread.
|
||||||
*/
|
*/
|
||||||
void refreshMessagesArea() {
|
void refreshMessagesArea() {
|
||||||
messagesAreaBox.getChildren().clear();
|
messagesAreaBox.getChildren().clear();
|
||||||
|
attachAllCopyContextMenu();
|
||||||
if (pendingMessages.isEmpty()) {
|
if (pendingMessages.isEmpty()) {
|
||||||
Text placeholder = new Text("Keine Meldungen vorhanden.");
|
Text placeholder = new Text("Keine Meldungen vorhanden.");
|
||||||
placeholder.setStyle("-fx-fill: #888888;");
|
placeholder.setStyle("-fx-fill: #888888;");
|
||||||
@@ -1741,17 +1763,100 @@ public final class GuiConfigurationEditorWorkspace {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (GuiMessageEntry entry : pendingMessages) {
|
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() + ";"
|
prefix.setStyle("-fx-fill: " + entry.severity().getPrefixCssColour() + ";"
|
||||||
+ " -fx-font-weight: bold;");
|
+ " -fx-font-weight: bold;");
|
||||||
Text body = new Text(entry.text());
|
Text body = new Text(entry.text());
|
||||||
body.setStyle("-fx-fill: black;");
|
body.setStyle("-fx-fill: black;");
|
||||||
TextFlow row = new TextFlow(prefix, body);
|
TextFlow row = new TextFlow(prefix, body);
|
||||||
row.setStyle("-fx-padding: 1px 4px;");
|
row.setStyle("-fx-padding: 1px 4px;");
|
||||||
|
String fullLine = timestampLabel + " " + severityLabel + " " + entry.text();
|
||||||
|
attachRowCopyContextMenu(row, fullLine);
|
||||||
messagesAreaBox.getChildren().add(row);
|
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
|
// 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.
|
* Wendet das Ergebnis des vollständigen Gesamttests auf die geteilte Nachrichtenliste an.
|
||||||
* <p>
|
* <p>
|
||||||
* Entfernt alle vorherigen Einträge mit Quelle {@link #SOURCE_TAG} und fügt für jeden
|
* Fügt für jedes Checkpoint-Ergebnis einen neuen Eintrag zur geteilten Nachrichtenliste
|
||||||
* Checkpoint-Ergebnis einen neuen Eintrag hinzu. Zusätzlich wird eine Zusammenfassung
|
* hinzu; vorhandene Einträge bleiben erhalten, sodass die Meldungen über mehrere
|
||||||
* angehängt.
|
* Testläufe hinweg akkumulieren. Zusätzlich wird eine Zusammenfassung angehängt.
|
||||||
* <p>
|
* <p>
|
||||||
* Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
|
* Muss nur auf dem JavaFX Application Thread aufgerufen werden (via {@code resultDelivery}).
|
||||||
*
|
*
|
||||||
* @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
|
* @param report der vollständige Gesamttestbericht; darf nicht {@code null} sein
|
||||||
*/
|
*/
|
||||||
private void applyResult(TechnicalTestReport report) {
|
private void applyResult(TechnicalTestReport report) {
|
||||||
// Alte Einträge mit Source-Tag entfernen (Replace-Semantik)
|
// Akkumulieren: Vorherige Einträge anderer Läufe bleiben erhalten.
|
||||||
pendingMessages.removeIf(msg -> SOURCE_TAG.equals(msg.source().orElse("")));
|
|
||||||
|
|
||||||
long successCount = 0;
|
long successCount = 0;
|
||||||
long failureErrorCount = 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>
|
* <p>
|
||||||
* This start state intentionally does not show the standard template yet. The template
|
* The start state contains the standard configuration template so the GUI shows the
|
||||||
* is reserved for the explicit {@code Neu} action so the GUI starts without an implicit
|
* default values immediately, equivalent to the explicit {@code Neu} action having been
|
||||||
* draft and only shows the welcome guidance until the user requests a new configuration.
|
* 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() {
|
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(
|
GuiConfigurationValues blankValues = new GuiConfigurationValues(
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
|||||||
+2
@@ -4,6 +4,7 @@ import java.util.List;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.TextField;
|
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)
|
// Initial state: show text field (NOT_YET_LOADED → manual input)
|
||||||
applyVisibility(false);
|
applyVisibility(false);
|
||||||
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
getChildren().addAll(comboBox, textField);
|
getChildren().addAll(comboBox, textField);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-7
@@ -206,14 +206,14 @@ class GuiAdapterSmokeTest {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies that the editor workspace starts without a loaded configuration, shows the
|
* Verifies that the editor workspace starts without a loaded configuration, immediately
|
||||||
* welcome guidance, and exposes the fixed GUI structure of the current shell.
|
* 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
|
* @throws Exception if the FX thread task fails or times out
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Order(5)
|
@Order(5)
|
||||||
void editorWorkspace_startStateShowsEmptyHeaderWelcomeGuidanceAndOneTab() throws Exception {
|
void editorWorkspace_startStateShowsEmptyHeaderDefaultsAndOneTab() throws Exception {
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
AtomicReference<Throwable> fxError = new AtomicReference<>();
|
AtomicReference<Throwable> fxError = new AtomicReference<>();
|
||||||
AtomicReference<GuiConfigurationEditorWorkspace> workspaceReference = new AtomicReference<>();
|
AtomicReference<GuiConfigurationEditorWorkspace> workspaceReference = new AtomicReference<>();
|
||||||
@@ -225,10 +225,8 @@ class GuiAdapterSmokeTest {
|
|||||||
|
|
||||||
assertEquals("", workspace.configurationPathText(),
|
assertEquals("", workspace.configurationPathText(),
|
||||||
"The header path must stay empty before any configuration is loaded");
|
"The header path must stay empty before any configuration is loaded");
|
||||||
assertTrue(workspace.isWelcomeGuidanceVisible(),
|
assertFalse(workspace.isWelcomeGuidanceVisible(),
|
||||||
"The welcome guidance must be visible in the unloaded start state");
|
"The welcome guidance must stay hidden because the standard template is shown immediately");
|
||||||
assertTrue(workspace.welcomeText().contains("Willkommen"),
|
|
||||||
"The welcome text must be shown in German");
|
|
||||||
assertNotNull(workspace.root(),
|
assertNotNull(workspace.root(),
|
||||||
"The workspace root must be available");
|
"The workspace root must be available");
|
||||||
assertEquals("Neu", workspace.newButton().getText(),
|
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
|
* Verifies that starting the GUI without a {@code --config} argument shows the standard
|
||||||
* blank welcome state: header path is empty, welcome guidance is visible, and the editor is
|
* template defaults immediately: header path is empty, welcome guidance is hidden, the
|
||||||
* not in dirty state.
|
* editor is not in dirty state, and the standard default values are populated.
|
||||||
*
|
*
|
||||||
* @throws Exception if the FX thread task fails or times out
|
* @throws Exception if the FX thread task fails or times out
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void guiStartup_withoutConfigPath_showsBlankWelcomeState() throws Exception {
|
void guiStartup_withoutConfigPath_showsStandardTemplateDefaults() throws Exception {
|
||||||
GuiStartupContext blankContext = GuiStartupContext.blank(Optional.empty());
|
GuiStartupContext blankContext = GuiStartupContext.blank(Optional.empty());
|
||||||
|
|
||||||
AtomicReference<Throwable> error = new AtomicReference<>();
|
AtomicReference<Throwable> error = new AtomicReference<>();
|
||||||
@@ -215,14 +215,14 @@ class GuiEditorIntegrationTest {
|
|||||||
|
|
||||||
assertEquals("", workspace.configurationPathText(),
|
assertEquals("", workspace.configurationPathText(),
|
||||||
"Header path must be empty when no configuration is loaded");
|
"Header path must be empty when no configuration is loaded");
|
||||||
assertTrue(workspace.isWelcomeGuidanceVisible(),
|
assertFalse(workspace.isWelcomeGuidanceVisible(),
|
||||||
"Welcome guidance must be visible when no configuration is loaded");
|
"Welcome guidance must stay hidden because the standard template is shown immediately");
|
||||||
assertFalse(workspace.editorState().hasLoadedFileSnapshot(),
|
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(),
|
assertFalse(workspace.editorState().isDirty(),
|
||||||
"Blank start state must not be dirty");
|
"Default start state must not be dirty");
|
||||||
assertTrue(workspace.welcomeText().contains("Willkommen"),
|
assertEquals("./work/local/source", workspace.editorState().values().sourceFolder(),
|
||||||
"Welcome text must be shown in German");
|
"Default start state must populate the standard source folder");
|
||||||
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
error.set(t);
|
error.set(t);
|
||||||
@@ -302,8 +302,8 @@ class GuiEditorIntegrationTest {
|
|||||||
try {
|
try {
|
||||||
GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(context);
|
GuiConfigurationEditorWorkspace workspace = new GuiConfigurationEditorWorkspace(context);
|
||||||
|
|
||||||
assertTrue(workspace.isWelcomeGuidanceVisible(),
|
assertFalse(workspace.isWelcomeGuidanceVisible(),
|
||||||
"Welcome guidance must be visible when config path does not exist");
|
"Welcome guidance must stay hidden because the standard template is shown immediately");
|
||||||
assertEquals("", workspace.configurationPathText(),
|
assertEquals("", workspace.configurationPathText(),
|
||||||
"Header path must be empty when config file was not found");
|
"Header path must be empty when config file was not found");
|
||||||
assertFalse(workspace.editorState().hasLoadedFileSnapshot(),
|
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>
|
* <p>
|
||||||
* The workspace must display the welcome guidance, the header path must be empty, and
|
* The workspace must keep the welcome guidance hidden because the standard template values
|
||||||
* the editor state must not have a file snapshot. "Neu" and "Öffnen" must be present.
|
* 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
|
* @throws Exception if the FX thread task fails or times out
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Order(1)
|
@Order(1)
|
||||||
void guiStart_withoutConfig_showsBlankWelcomeStateAndExposesNeuAndOeffnenButtons()
|
void guiStart_withoutConfig_showsStandardTemplateDefaultsAndExposesNeuAndOeffnenButtons()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
runOnFx(() -> {
|
runOnFx(() -> {
|
||||||
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(Optional.empty());
|
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(Optional.empty());
|
||||||
|
|
||||||
assertTrue(ws.isWelcomeGuidanceVisible(),
|
assertFalse(ws.isWelcomeGuidanceVisible(),
|
||||||
"Welcome guidance must be visible on blank start");
|
"Welcome guidance must stay hidden because the standard template is shown immediately");
|
||||||
assertEquals("", ws.configurationPathText(),
|
assertEquals("", ws.configurationPathText(),
|
||||||
"Header path must be empty on blank start");
|
"Header path must be empty on default start");
|
||||||
assertFalse(ws.editorState().hasLoadedFileSnapshot(),
|
assertFalse(ws.editorState().hasLoadedFileSnapshot(),
|
||||||
"No file snapshot must exist on blank start");
|
"No file snapshot must exist on default start");
|
||||||
assertFalse(ws.editorState().isDirty(),
|
assertFalse(ws.editorState().isDirty(),
|
||||||
"Blank start state must not be dirty");
|
"Default start state must not be dirty");
|
||||||
assertEquals("Neu", ws.newButton().getText(),
|
assertEquals("Neu", ws.newButton().getText(),
|
||||||
"'Neu' button must be present");
|
"'Neu' button must be present");
|
||||||
assertEquals("Öffnen", ws.openButton().getText(),
|
assertEquals("Öffnen", ws.openButton().getText(),
|
||||||
@@ -131,23 +132,24 @@ class GuiEditorRegressionSmokeTest {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regression: "Neu" switches the workspace to the standard template, hides the welcome
|
* Regression: "Neu" reloads the standard template values, keeps the welcome guidance
|
||||||
* guidance, and leaves the state clean with all template fields populated.
|
* hidden, and leaves the state clean with all template fields populated.
|
||||||
*
|
*
|
||||||
* @throws Exception if the FX thread task fails or times out
|
* @throws Exception if the FX thread task fails or times out
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
@Order(2)
|
@Order(2)
|
||||||
void neu_withStandardTemplate_populatesFieldsAndHidesWelcomeGuidance() throws Exception {
|
void neu_withStandardTemplate_populatesFieldsAndKeepsWelcomeHidden() throws Exception {
|
||||||
runOnFx(() -> {
|
runOnFx(() -> {
|
||||||
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(Optional.empty());
|
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();
|
ws.requestNewConfiguration();
|
||||||
|
|
||||||
assertFalse(ws.isWelcomeGuidanceVisible(),
|
assertFalse(ws.isWelcomeGuidanceVisible(),
|
||||||
"Welcome guidance must be hidden after 'Neu'");
|
"Welcome guidance must remain hidden after 'Neu'");
|
||||||
assertEquals("", ws.editorState().configurationPathText(),
|
assertEquals("", ws.editorState().configurationPathText(),
|
||||||
"Path must remain empty after 'Neu' (no file saved yet)");
|
"Path must remain empty after 'Neu' (no file saved yet)");
|
||||||
assertFalse(ws.editorState().isDirty(),
|
assertFalse(ws.editorState().isDirty(),
|
||||||
|
|||||||
+14
-13
@@ -39,7 +39,7 @@ import javafx.scene.control.Button;
|
|||||||
* {@code technical-tests-button}.</li>
|
* {@code technical-tests-button}.</li>
|
||||||
* <li>Triggering the coordinator synchronously populates {@code pendingMessages}
|
* <li>Triggering the coordinator synchronously populates {@code pendingMessages}
|
||||||
* with entries tagged {@link GuiTechnicalTestCoordinator#SOURCE_TAG}.</li>
|
* 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>
|
* <li>The post-result callback is invoked after the result is applied.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* <p>
|
* <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
|
* Smoke test: triggering the coordinator twice accumulates both runs; the
|
||||||
* entries; the count remains the same as after a single trigger.
|
* 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
|
* @throws Exception if the FX thread task fails or times out
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void trigger_twice_replacesPreviousTestEntries() throws Exception {
|
void trigger_twice_accumulatesTestEntries() throws Exception {
|
||||||
runOnFx(() -> {
|
runOnFx(() -> {
|
||||||
List<GuiMessageEntry> messages = new ArrayList<>();
|
List<GuiMessageEntry> messages = new ArrayList<>();
|
||||||
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });
|
GuiTechnicalTestCoordinator coordinator = buildSyncCoordinator(messages, report -> { });
|
||||||
@@ -189,8 +190,8 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
|||||||
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
|
&& GuiTechnicalTestCoordinator.SOURCE_TAG.equals(m.source().get()))
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
assertEquals(countAfterFirst, countAfterSecond,
|
assertEquals(countAfterFirst * 2, countAfterSecond,
|
||||||
"Second trigger must replace (not append) the previous test entries");
|
"Second trigger must append a fresh batch, doubling the SOURCE_TAG entries");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,9 +248,9 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
|||||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.absent(),
|
EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"https://api.openai.com", "gpt-4", "30",
|
"https://api.openai.com", "gpt-4", "30",
|
||||||
EffectiveApiKeyDescriptor.absent()));
|
EffectiveApiKeyDescriptor.absent(), ""));
|
||||||
|
|
||||||
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
TechnicalTestOrchestrator orchestrator = new TechnicalTestOrchestrator(
|
||||||
new EditorConfigurationValidator(),
|
new EditorConfigurationValidator(),
|
||||||
@@ -283,9 +284,9 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
|||||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.absent(),
|
EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"https://api.openai.com", "gpt-4", "30",
|
"https://api.openai.com", "gpt-4", "30",
|
||||||
EffectiveApiKeyDescriptor.absent()));
|
EffectiveApiKeyDescriptor.absent(), ""));
|
||||||
|
|
||||||
// Second trigger with the updated (unsaved) input.
|
// Second trigger with the updated (unsaved) input.
|
||||||
coordinator.triggerTechnicalTests();
|
coordinator.triggerTechnicalTests();
|
||||||
@@ -370,9 +371,9 @@ class GuiTechnicalTestCoordinatorSmokeTest {
|
|||||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||||
"3", "10", "2000",
|
"3", "10", "2000",
|
||||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.absent(),
|
EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"https://api.openai.com", "gpt-4", "30",
|
"https://api.openai.com", "gpt-4", "30",
|
||||||
EffectiveApiKeyDescriptor.absent());
|
EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator(
|
GuiTechnicalTestCoordinator coordinator = new GuiTechnicalTestCoordinator(
|
||||||
orchestrator,
|
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.GuiConfigurationEditorState;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
|
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.GuiEditorValidationResult;
|
||||||
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
|
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.adapter.in.gui.editor.GuiMessageSeverity;
|
||||||
@@ -34,11 +35,11 @@ import javafx.scene.control.Button;
|
|||||||
* <h2>Covered scenarios</h2>
|
* <h2>Covered scenarios</h2>
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Clicking "Validieren" with an incomplete configuration produces ERROR findings and
|
* <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
|
* <li>Clicking "Validieren" with a valid template configuration produces no ERRORs and
|
||||||
* an INFO message reporting "Keine Befunde." or a zero count.</li>
|
* an INFO message reporting "Keine Befunde." or a zero count.</li>
|
||||||
* <li>Clicking "Validieren" twice replaces the previous action-confirmation INFO message
|
* <li>Clicking "Validieren" twice appends a second action-confirmation INFO message
|
||||||
* (replace semantics; the message appears exactly once).</li>
|
* (accumulation semantics; each click adds a fresh snapshot).</li>
|
||||||
* <li>Clicking "Validieren" does not trigger any file write (the writer stub records no
|
* <li>Clicking "Validieren" does not trigger any file write (the writer stub records no
|
||||||
* calls).</li>
|
* calls).</li>
|
||||||
* <li>The button is findable by its CSS ID {@code validate-button}.</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
|
* 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
|
* 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
|
* ERROR and the central message area contains one INFO confirmation with source
|
||||||
* "Validierung-Aktion" that reports the number of findings.
|
* "Validierung-Aktion" plus one entry per concrete finding with the same source.
|
||||||
*
|
*
|
||||||
* @throws Exception if the FX thread task fails or times out
|
* @throws Exception if the FX thread task fails or times out
|
||||||
*/
|
*/
|
||||||
@@ -122,8 +123,9 @@ class GuiValidateActionSmokeTest {
|
|||||||
runOnFx(() -> {
|
runOnFx(() -> {
|
||||||
GuiConfigurationEditorWorkspace ws = buildWorkspace();
|
GuiConfigurationEditorWorkspace ws = buildWorkspace();
|
||||||
|
|
||||||
// Force an incomplete state: start with blank (no active provider).
|
// Force an incomplete state: replace the editor state with a truly empty one
|
||||||
// The blank start state already has an empty active provider → errors expected.
|
// (no active provider, no template values) so validation produces errors.
|
||||||
|
ws.editorState = GuiConfigurationTemplateFactory.createEmptyStartState();
|
||||||
|
|
||||||
ws.validateButton.fire();
|
ws.validateButton.fire();
|
||||||
|
|
||||||
@@ -136,15 +138,17 @@ class GuiValidateActionSmokeTest {
|
|||||||
&& ACTION_SOURCE.equals(m.source().get()))
|
&& ACTION_SOURCE.equals(m.source().get()))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
assertEquals(1, actionMessages.size(),
|
assertFalse(actionMessages.isEmpty(),
|
||||||
"Exactly one action-confirmation INFO message must be present");
|
"At least one action message must be present");
|
||||||
GuiMessageEntry msg = actionMessages.get(0);
|
GuiMessageEntry confirmation = actionMessages.get(0);
|
||||||
assertEquals(GuiMessageSeverity.INFO, msg.severity(),
|
assertEquals(GuiMessageSeverity.INFO, confirmation.severity(),
|
||||||
"Action-confirmation message must have INFO severity");
|
"First action message must be the INFO confirmation");
|
||||||
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");
|
"Action-confirmation message text must start with expected prefix");
|
||||||
assertFalse(msg.text().contains("Keine Befunde"),
|
assertFalse(confirmation.text().contains("Keine Befunde"),
|
||||||
"With errors present the message must NOT say '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()))
|
&& ACTION_SOURCE.equals(m.source().get()))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
assertEquals(1, actionMessages.size(),
|
assertFalse(actionMessages.isEmpty(),
|
||||||
"Exactly one action-confirmation INFO message must be present");
|
"At least one action message must be present");
|
||||||
GuiMessageEntry msg = actionMessages.get(0);
|
GuiMessageEntry confirmation = actionMessages.get(0);
|
||||||
assertEquals(GuiMessageSeverity.INFO, msg.severity(),
|
assertEquals(GuiMessageSeverity.INFO, confirmation.severity(),
|
||||||
"Action-confirmation message must have INFO severity");
|
"First action message must be the INFO confirmation");
|
||||||
// Template may have WARNINGs but no ERRORs. The fieldFindings count may be 0.
|
// 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");
|
"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
|
* Smoke test: clicking "Validieren" twice must leave two action-confirmation
|
||||||
* INFO message in the message list (the second click replaces the first).
|
* 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
|
* @throws Exception if the FX thread task fails or times out
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void validateAction_clickedTwice_infoMessageAppearsExactlyOnce() throws Exception {
|
void validateAction_clickedTwice_infoMessageAppearsTwice() throws Exception {
|
||||||
runOnFx(() -> {
|
runOnFx(() -> {
|
||||||
GuiConfigurationEditorWorkspace ws = buildWorkspace();
|
GuiConfigurationEditorWorkspace ws = buildWorkspace();
|
||||||
|
|
||||||
ws.validateButton.fire();
|
ws.validateButton.fire();
|
||||||
ws.validateButton.fire();
|
ws.validateButton.fire();
|
||||||
|
|
||||||
long count = ws.pendingMessages.stream()
|
long confirmationCount = ws.pendingMessages.stream()
|
||||||
.filter(m -> m.source().isPresent()
|
.filter(m -> m.source().isPresent()
|
||||||
&& ACTION_SOURCE.equals(m.source().get()))
|
&& ACTION_SOURCE.equals(m.source().get()))
|
||||||
|
.filter(m -> m.text().startsWith("Aktion Validieren wurde ausgeführt."))
|
||||||
.count();
|
.count();
|
||||||
assertEquals(1, count,
|
assertEquals(2, confirmationCount,
|
||||||
"After two clicks the action-confirmation INFO message must appear exactly once");
|
"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
|
req -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
|
||||||
.ModelCatalogResult.IncompleteConfiguration(
|
.ModelCatalogResult.IncompleteConfiguration(
|
||||||
req.providerIdentifier(), "kein Port im Test"),
|
req.providerIdentifier(), "kein Port im Test"),
|
||||||
(family, propertyValue) ->
|
noOpApiKeyResolutionPort(),
|
||||||
de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
|
|
||||||
.EffectiveApiKeyDescriptor.absent(),
|
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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() {
|
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort() {
|
||||||
@Override public boolean isDirectoryReadable(String p) { return false; }
|
@Override public boolean isDirectoryReadable(String p) { return false; }
|
||||||
@Override public boolean isDirectoryWritableOrCreatable(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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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 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
|
// 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,
|
* Builds a workspace with no-op loader/writer and absent API-key resolution,
|
||||||
* suitable for in-memory validation tests.
|
* suitable for in-memory validation tests.
|
||||||
@@ -354,12 +369,10 @@ class GuiValidateActionSmokeTest {
|
|||||||
req -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
|
req -> new de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
|
||||||
.ModelCatalogResult.IncompleteConfiguration(
|
.ModelCatalogResult.IncompleteConfiguration(
|
||||||
req.providerIdentifier(), "kein Port im Test"),
|
req.providerIdentifier(), "kein Port im Test"),
|
||||||
(family, propertyValue) ->
|
noOpApiKeyResolutionPort(),
|
||||||
de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog
|
|
||||||
.EffectiveApiKeyDescriptor.absent(),
|
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ProviderTechnicalTestService(
|
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"),
|
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() {
|
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort() {
|
||||||
@Override public boolean isDirectoryReadable(String p) { return false; }
|
@Override public boolean isDirectoryReadable(String p) { return false; }
|
||||||
@Override public boolean isDirectoryWritableOrCreatable(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(
|
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"),
|
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.CorrectionExecutionService(
|
||||||
new de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort() {
|
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 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
|
@Test
|
||||||
void createBlankStartState_startsWithoutLoadedConfigurationAndWithoutTemplateValues() {
|
void createBlankStartState_returnsStandardTemplateValuesWithoutLoadedFile() {
|
||||||
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createBlankStartState();
|
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createBlankStartState();
|
||||||
|
|
||||||
assertFalse(state.isDirty());
|
assertFalse(state.isDirty());
|
||||||
@@ -66,6 +66,22 @@ class GuiConfigurationTemplateFactoryTest {
|
|||||||
assertTrue(state.isNewConfiguration());
|
assertTrue(state.isNewConfiguration());
|
||||||
assertEquals("", state.configurationPathText());
|
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();
|
GuiConfigurationValues values = state.values();
|
||||||
assertEquals("", values.sourceFolder());
|
assertEquals("", values.sourceFolder());
|
||||||
assertEquals("", values.targetFolder());
|
assertEquals("", values.targetFolder());
|
||||||
|
|||||||
+45
@@ -1,6 +1,7 @@
|
|||||||
package de.gecheckt.pdf.umbenenner.adapter.out.validation;
|
package de.gecheckt.pdf.umbenenner.adapter.out.validation;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
||||||
@@ -103,6 +104,50 @@ public class EnvironmentApiKeyResolutionAdapter implements ApiKeyResolutionPort
|
|||||||
return EffectiveApiKeyDescriptor.absent();
|
return EffectiveApiKeyDescriptor.absent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den effektiven API-Schlüssel-Rohwert anhand derselben Vorrangregel wie {@link #resolve}.
|
||||||
|
*
|
||||||
|
* @param family die Provider-Familie; darf nicht {@code null} sein
|
||||||
|
* @param propertyValue aktueller Property-Wert aus dem Editor; darf nicht {@code null} sein
|
||||||
|
* @return der effektive Rohwert, falls verfügbar; sonst leer
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Optional<String> resolveEffectiveApiKeyValue(AiProviderFamily family, String propertyValue) {
|
||||||
|
Objects.requireNonNull(family, "family must not be null");
|
||||||
|
Objects.requireNonNull(propertyValue, "propertyValue must not be null");
|
||||||
|
|
||||||
|
return switch (family) {
|
||||||
|
case CLAUDE -> resolveClaudeValue(propertyValue);
|
||||||
|
case OPENAI_COMPATIBLE -> resolveOpenAiCompatibleValue(propertyValue);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> resolveClaudeValue(String propertyValue) {
|
||||||
|
String envValue = environmentLookup.apply(ENV_CLAUDE_API_KEY);
|
||||||
|
if (isPresent(envValue)) {
|
||||||
|
return Optional.of(envValue);
|
||||||
|
}
|
||||||
|
if (isPresent(propertyValue)) {
|
||||||
|
return Optional.of(propertyValue);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> resolveOpenAiCompatibleValue(String propertyValue) {
|
||||||
|
String primaryEnv = environmentLookup.apply(ENV_OPENAI_API_KEY);
|
||||||
|
if (isPresent(primaryEnv)) {
|
||||||
|
return Optional.of(primaryEnv);
|
||||||
|
}
|
||||||
|
String legacyEnv = environmentLookup.apply(ENV_LEGACY_OPENAI_API_KEY);
|
||||||
|
if (isPresent(legacyEnv)) {
|
||||||
|
return Optional.of(legacyEnv);
|
||||||
|
}
|
||||||
|
if (isPresent(propertyValue)) {
|
||||||
|
return Optional.of(propertyValue);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isPresent(String value) {
|
private static boolean isPresent(String value) {
|
||||||
return value != null && !value.isBlank();
|
return value != null && !value.isBlank();
|
||||||
}
|
}
|
||||||
|
|||||||
+31
@@ -1,5 +1,7 @@
|
|||||||
package de.gecheckt.pdf.umbenenner.application.validation.editor;
|
package de.gecheckt.pdf.umbenenner.application.validation.editor;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
|
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.port.out.modelcatalog.EffectiveApiKeyDescriptor;
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApi
|
|||||||
* <p>
|
* <p>
|
||||||
* Implementierungen dieses Ports liegen im Adapter-Out-Modul.
|
* Implementierungen dieses Ports liegen im Adapter-Out-Modul.
|
||||||
*/
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
public interface ApiKeyResolutionPort {
|
public interface ApiKeyResolutionPort {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,4 +40,32 @@ public interface ApiKeyResolutionPort {
|
|||||||
* @return der Descriptor für die effektive Schlüsselherkunft; nie {@code null}
|
* @return der Descriptor für die effektive Schlüsselherkunft; nie {@code null}
|
||||||
*/
|
*/
|
||||||
EffectiveApiKeyDescriptor resolve(AiProviderFamily family, String propertyValue);
|
EffectiveApiKeyDescriptor resolve(AiProviderFamily family, String propertyValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den effektiven API-Schlüssel-Rohwert anhand derselben Vorrangregel wie {@link #resolve}.
|
||||||
|
* <p>
|
||||||
|
* Dient technischen Tests wie dem Modellkatalogabruf, die den tatsächlichen Schlüssel im
|
||||||
|
* HTTP-Header benötigen. Die Herkunft selbst wird nicht mit zurückgegeben – dafür ist
|
||||||
|
* {@link #resolve} zuständig.
|
||||||
|
* <p>
|
||||||
|
* Diese Default-Implementierung deckt den Fall ab, in dem ein Adapter ausschließlich die
|
||||||
|
* Property-Datei kennt: liefert {@link #resolve} {@code ABSENT}, wird ein leerer Optional
|
||||||
|
* zurückgegeben; andernfalls wird der nicht-leere Property-Wert geliefert. Adapter, die
|
||||||
|
* Umgebungsvariablen lesen, müssen diese Methode überschreiben, damit der ENV-Wert auch
|
||||||
|
* tatsächlich an HTTP-Aufrufer durchgereicht wird.
|
||||||
|
*
|
||||||
|
* @param family die Provider-Familie; darf nicht {@code null} sein
|
||||||
|
* @param propertyValue aktueller Property-Wert aus dem Editor (kann leer sein); darf nicht {@code null} sein
|
||||||
|
* @return der effektive Schlüssel-Rohwert, falls eine der Quellen einen Wert liefert; sonst leer
|
||||||
|
*/
|
||||||
|
default Optional<String> resolveEffectiveApiKeyValue(AiProviderFamily family, String propertyValue) {
|
||||||
|
EffectiveApiKeyDescriptor descriptor = resolve(family, propertyValue);
|
||||||
|
if (descriptor.isAbsent()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (propertyValue != null && !propertyValue.isBlank()) {
|
||||||
|
return Optional.of(propertyValue);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -28,10 +28,12 @@ import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApi
|
|||||||
* @param claudeModel Rohtextwert des Claude-Modellnamens
|
* @param claudeModel Rohtextwert des Claude-Modellnamens
|
||||||
* @param claudeTimeoutSeconds Rohtextwert des Claude-Timeouts
|
* @param claudeTimeoutSeconds Rohtextwert des Claude-Timeouts
|
||||||
* @param claudeApiKeyDescriptor API-Key-Herkunft für den Claude-Provider; nie {@code null}
|
* @param claudeApiKeyDescriptor API-Key-Herkunft für den Claude-Provider; nie {@code null}
|
||||||
|
* @param claudeApiKeyPropertyValue Rohwert des Claude-API-Schlüssels aus der Properties-Datei
|
||||||
* @param openaiBaseUrl Rohtextwert der OpenAI-kompatiblen Basis-URL
|
* @param openaiBaseUrl Rohtextwert der OpenAI-kompatiblen Basis-URL
|
||||||
* @param openaiModel Rohtextwert des OpenAI-kompatiblen Modellnamens
|
* @param openaiModel Rohtextwert des OpenAI-kompatiblen Modellnamens
|
||||||
* @param openaiTimeoutSeconds Rohtextwert des OpenAI-kompatiblen Timeouts
|
* @param openaiTimeoutSeconds Rohtextwert des OpenAI-kompatiblen Timeouts
|
||||||
* @param openaiApiKeyDescriptor API-Key-Herkunft für den OpenAI-kompatiblen Provider; nie {@code null}
|
* @param openaiApiKeyDescriptor API-Key-Herkunft für den OpenAI-kompatiblen Provider; nie {@code null}
|
||||||
|
* @param openaiApiKeyPropertyValue Rohwert des OpenAI-API-Schlüssels aus der Properties-Datei
|
||||||
*/
|
*/
|
||||||
public record EditorValidationInput(
|
public record EditorValidationInput(
|
||||||
String activeProviderIdentifier,
|
String activeProviderIdentifier,
|
||||||
@@ -46,10 +48,12 @@ public record EditorValidationInput(
|
|||||||
String claudeModel,
|
String claudeModel,
|
||||||
String claudeTimeoutSeconds,
|
String claudeTimeoutSeconds,
|
||||||
EffectiveApiKeyDescriptor claudeApiKeyDescriptor,
|
EffectiveApiKeyDescriptor claudeApiKeyDescriptor,
|
||||||
|
String claudeApiKeyPropertyValue,
|
||||||
String openaiBaseUrl,
|
String openaiBaseUrl,
|
||||||
String openaiModel,
|
String openaiModel,
|
||||||
String openaiTimeoutSeconds,
|
String openaiTimeoutSeconds,
|
||||||
EffectiveApiKeyDescriptor openaiApiKeyDescriptor) {
|
EffectiveApiKeyDescriptor openaiApiKeyDescriptor,
|
||||||
|
String openaiApiKeyPropertyValue) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt eine neue Eingabe für den Validator.
|
* Erstellt eine neue Eingabe für den Validator.
|
||||||
@@ -66,10 +70,12 @@ public record EditorValidationInput(
|
|||||||
* @param claudeModel Claude-Modellname; {@code null} wird zu leerem String
|
* @param claudeModel Claude-Modellname; {@code null} wird zu leerem String
|
||||||
* @param claudeTimeoutSeconds Claude-Timeout; {@code null} wird zu leerem String
|
* @param claudeTimeoutSeconds Claude-Timeout; {@code null} wird zu leerem String
|
||||||
* @param claudeApiKeyDescriptor Claude-API-Key-Herkunft; darf nicht {@code null} sein
|
* @param claudeApiKeyDescriptor Claude-API-Key-Herkunft; darf nicht {@code null} sein
|
||||||
|
* @param claudeApiKeyPropertyValue Claude-API-Key-Rohwert aus der Properties-Datei; {@code null} wird zu leerem String
|
||||||
* @param openaiBaseUrl OpenAI-Basis-URL; {@code null} wird zu leerem String
|
* @param openaiBaseUrl OpenAI-Basis-URL; {@code null} wird zu leerem String
|
||||||
* @param openaiModel OpenAI-Modellname; {@code null} wird zu leerem String
|
* @param openaiModel OpenAI-Modellname; {@code null} wird zu leerem String
|
||||||
* @param openaiTimeoutSeconds OpenAI-Timeout; {@code null} wird zu leerem String
|
* @param openaiTimeoutSeconds OpenAI-Timeout; {@code null} wird zu leerem String
|
||||||
* @param openaiApiKeyDescriptor OpenAI-API-Key-Herkunft; darf nicht {@code null} sein
|
* @param openaiApiKeyDescriptor OpenAI-API-Key-Herkunft; darf nicht {@code null} sein
|
||||||
|
* @param openaiApiKeyPropertyValue OpenAI-API-Key-Rohwert aus der Properties-Datei; {@code null} wird zu leerem String
|
||||||
* @throws NullPointerException wenn {@code claudeApiKeyDescriptor} oder {@code openaiApiKeyDescriptor} {@code null} sind
|
* @throws NullPointerException wenn {@code claudeApiKeyDescriptor} oder {@code openaiApiKeyDescriptor} {@code null} sind
|
||||||
*/
|
*/
|
||||||
public EditorValidationInput {
|
public EditorValidationInput {
|
||||||
@@ -86,11 +92,13 @@ public record EditorValidationInput(
|
|||||||
claudeTimeoutSeconds = normalizeText(claudeTimeoutSeconds);
|
claudeTimeoutSeconds = normalizeText(claudeTimeoutSeconds);
|
||||||
claudeApiKeyDescriptor = Objects.requireNonNull(claudeApiKeyDescriptor,
|
claudeApiKeyDescriptor = Objects.requireNonNull(claudeApiKeyDescriptor,
|
||||||
"claudeApiKeyDescriptor must not be null");
|
"claudeApiKeyDescriptor must not be null");
|
||||||
|
claudeApiKeyPropertyValue = normalizeText(claudeApiKeyPropertyValue);
|
||||||
openaiBaseUrl = normalizeText(openaiBaseUrl);
|
openaiBaseUrl = normalizeText(openaiBaseUrl);
|
||||||
openaiModel = normalizeText(openaiModel);
|
openaiModel = normalizeText(openaiModel);
|
||||||
openaiTimeoutSeconds = normalizeText(openaiTimeoutSeconds);
|
openaiTimeoutSeconds = normalizeText(openaiTimeoutSeconds);
|
||||||
openaiApiKeyDescriptor = Objects.requireNonNull(openaiApiKeyDescriptor,
|
openaiApiKeyDescriptor = Objects.requireNonNull(openaiApiKeyDescriptor,
|
||||||
"openaiApiKeyDescriptor must not be null");
|
"openaiApiKeyDescriptor must not be null");
|
||||||
|
openaiApiKeyPropertyValue = normalizeText(openaiApiKeyPropertyValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String normalizeText(String value) {
|
private static String normalizeText(String value) {
|
||||||
|
|||||||
+25
-14
@@ -79,6 +79,8 @@ public class ProviderTechnicalTestService {
|
|||||||
private static final int DEFAULT_TIMEOUT_SECONDS = 30;
|
private static final int DEFAULT_TIMEOUT_SECONDS = 30;
|
||||||
|
|
||||||
private final AiModelCatalogPort modelCatalogPort;
|
private final AiModelCatalogPort modelCatalogPort;
|
||||||
|
private final ApiKeyResolutionPort apiKeyResolutionPort;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Erstellt einen neuen Service mit den erforderlichen Ports.
|
* Erstellt einen neuen Service mit den erforderlichen Ports.
|
||||||
*
|
*
|
||||||
@@ -89,7 +91,7 @@ public class ProviderTechnicalTestService {
|
|||||||
public ProviderTechnicalTestService(AiModelCatalogPort modelCatalogPort,
|
public ProviderTechnicalTestService(AiModelCatalogPort modelCatalogPort,
|
||||||
ApiKeyResolutionPort apiKeyResolutionPort) {
|
ApiKeyResolutionPort apiKeyResolutionPort) {
|
||||||
this.modelCatalogPort = Objects.requireNonNull(modelCatalogPort, "modelCatalogPort must not be null");
|
this.modelCatalogPort = Objects.requireNonNull(modelCatalogPort, "modelCatalogPort must not be null");
|
||||||
Objects.requireNonNull(apiKeyResolutionPort,
|
this.apiKeyResolutionPort = Objects.requireNonNull(apiKeyResolutionPort,
|
||||||
"apiKeyResolutionPort must not be null");
|
"apiKeyResolutionPort must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,13 +379,10 @@ public class ProviderTechnicalTestService {
|
|||||||
/**
|
/**
|
||||||
* Baut den {@link ModelCatalogRequest} aus dem aktuellen Editorzustand auf.
|
* Baut den {@link ModelCatalogRequest} aus dem aktuellen Editorzustand auf.
|
||||||
* <p>
|
* <p>
|
||||||
* Da {@link EditorValidationInput} keinen direkten API-Key-String enthält, sondern
|
* Der effektive API-Key-Rohwert wird über den {@link ApiKeyResolutionPort} ermittelt
|
||||||
* nur einen bereits aufgelösten {@link EffectiveApiKeyDescriptor}, wird der Descriptor
|
* (Vorrangregel: providerspezifische ENV → Legacy-ENV → Property-Wert) und in den
|
||||||
* aus dem Editorzustand direkt verwendet. Der Adapter-Out-Seitige Dispatcher erwartet
|
* Request übernommen. Dadurch ist der Schlüssel bereits beim Adapter verfügbar und
|
||||||
* den Key entweder als ENV-Variable (die er selbst liest) oder als optionalen Wert
|
* spiegelt exakt die Quelle wider, die zuvor im Deskriptor ausgewiesen wurde.
|
||||||
* im Request. Da die Auflösung beim Service bereits über {@link ApiKeyResolutionPort}
|
|
||||||
* erfolgt ist, wird für den Catalog-Request ein leerer Optional-Wert geliefert –
|
|
||||||
* der Adapter verwendet dann intern seine eigene ENV-Variable-Auflösung.
|
|
||||||
*
|
*
|
||||||
* @param input aktueller Editorzustand
|
* @param input aktueller Editorzustand
|
||||||
* @param family aktive Provider-Familie
|
* @param family aktive Provider-Familie
|
||||||
@@ -393,12 +392,9 @@ public class ProviderTechnicalTestService {
|
|||||||
private ModelCatalogRequest buildCatalogRequest(EditorValidationInput input,
|
private ModelCatalogRequest buildCatalogRequest(EditorValidationInput input,
|
||||||
AiProviderFamily family,
|
AiProviderFamily family,
|
||||||
EffectiveApiKeyDescriptor apiKeyDesc) {
|
EffectiveApiKeyDescriptor apiKeyDesc) {
|
||||||
// EditorValidationInput enthält keinen direkten API-Key-String-Wert, nur den Descriptor.
|
String propertyValue = resolveApiKeyPropertyValue(input, family);
|
||||||
// Für den ModelCatalogRequest übergeben wir einen leeren Optional für den apiKey,
|
Optional<String> apiKeyForRequest = apiKeyResolutionPort
|
||||||
// sodass der Adapter seine eigene ENV-Variable-Auflösung durchführt.
|
.resolveEffectiveApiKeyValue(family, propertyValue);
|
||||||
// Der Adapter liefert dann IncompleteConfiguration, wenn auch er keinen Key findet –
|
|
||||||
// was aber nicht passiert, da wir oben bereits geprüft haben, dass apiKeyDesc nicht ABSENT ist.
|
|
||||||
Optional<String> apiKeyForRequest = Optional.empty();
|
|
||||||
|
|
||||||
String rawBaseUrl = resolveBaseUrlValue(input, family);
|
String rawBaseUrl = resolveBaseUrlValue(input, family);
|
||||||
Optional<String> baseUrl = rawBaseUrl.isBlank() ? Optional.empty() : Optional.of(rawBaseUrl);
|
Optional<String> baseUrl = rawBaseUrl.isBlank() ? Optional.empty() : Optional.of(rawBaseUrl);
|
||||||
@@ -412,6 +408,21 @@ public class ProviderTechnicalTestService {
|
|||||||
timeout);
|
timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest den Roh-Property-Wert des API-Schlüssels für die angegebene Provider-Familie
|
||||||
|
* aus dem Editorzustand.
|
||||||
|
*
|
||||||
|
* @param input aktueller Editorzustand
|
||||||
|
* @param family aktive Provider-Familie
|
||||||
|
* @return Property-Wert; nie {@code null}, leer wenn nicht gesetzt
|
||||||
|
*/
|
||||||
|
private String resolveApiKeyPropertyValue(EditorValidationInput input, AiProviderFamily family) {
|
||||||
|
return switch (family) {
|
||||||
|
case CLAUDE -> input.claudeApiKeyPropertyValue();
|
||||||
|
case OPENAI_COMPATIBLE -> input.openaiApiKeyPropertyValue();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liest den bereits aufgelösten {@link EffectiveApiKeyDescriptor} für die aktive Provider-Familie
|
* Liest den bereits aufgelösten {@link EffectiveApiKeyDescriptor} für die aktive Provider-Familie
|
||||||
* direkt aus dem {@link EditorValidationInput}.
|
* direkt aus dem {@link EditorValidationInput}.
|
||||||
|
|||||||
+55
-53
@@ -41,10 +41,12 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude-3-5-sonnet", // claudeModel
|
"claude-3-5-sonnet", // claudeModel
|
||||||
"30", // claudeTimeoutSeconds
|
"30", // claudeTimeoutSeconds
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(), // claudeApiKeyDescriptor
|
EffectiveApiKeyDescriptor.fromPropertyFile(), // claudeApiKeyDescriptor
|
||||||
|
"sk-claude", // claudeApiKeyPropertyValue
|
||||||
"https://api.openai.com", // openaiBaseUrl
|
"https://api.openai.com", // openaiBaseUrl
|
||||||
"gpt-4", // openaiModel
|
"gpt-4", // openaiModel
|
||||||
"30", // openaiTimeoutSeconds
|
"30", // openaiTimeoutSeconds
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile() // openaiApiKeyDescriptor
|
EffectiveApiKeyDescriptor.fromPropertyFile(), // openaiApiKeyDescriptor
|
||||||
|
"sk-openai" // openaiApiKeyPropertyValue
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +59,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
EditorValidationInput input = new EditorValidationInput(
|
EditorValidationInput input = new EditorValidationInput(
|
||||||
"", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent(),
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -74,8 +76,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
EditorValidationInput input = new EditorValidationInput(
|
EditorValidationInput input = new EditorValidationInput(
|
||||||
"unknown-provider", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"unknown-provider", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent(),
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -94,8 +96,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -109,8 +111,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -124,8 +126,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -139,8 +141,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -158,8 +160,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"0", "10", "500",
|
"0", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -173,8 +175,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"-1", "10", "500",
|
"-1", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -188,8 +190,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"1", "10", "500",
|
"1", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -204,8 +206,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"abc", "10", "500",
|
"abc", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -223,8 +225,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "0", "500",
|
"3", "0", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -238,8 +240,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "101", "500",
|
"3", "101", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -253,8 +255,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "100", "500",
|
"3", "100", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -275,8 +277,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "1000",
|
"3", "10", "1000",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -290,8 +292,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "1001",
|
"3", "10", "1001",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -305,8 +307,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "3000",
|
"3", "10", "3000",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -320,8 +322,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "3001",
|
"3", "10", "3001",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -340,8 +342,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "0",
|
"3", "10", "0",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -359,8 +361,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "", "30",
|
"https://api.anthropic.com", "", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -374,8 +376,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"", "claude-3-5-sonnet", "30",
|
"", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -389,8 +391,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "-5",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "-5",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -408,8 +410,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.absent(),
|
EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -434,8 +436,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromProviderEnvVar("ANTHROPIC_API_KEY"),
|
EffectiveApiKeyDescriptor.fromProviderEnvVar("ANTHROPIC_API_KEY"), "",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent());
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -449,9 +451,9 @@ class EditorConfigurationValidatorTest {
|
|||||||
EditorValidationInput input = new EditorValidationInput(
|
EditorValidationInput input = new EditorValidationInput(
|
||||||
"openai-compatible", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"openai-compatible", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent(),
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"https://api.openai.com", "gpt-4", "30",
|
"https://api.openai.com", "gpt-4", "30",
|
||||||
EffectiveApiKeyDescriptor.fromLegacyEnvVar("PDF_UMBENENNER_API_KEY"));
|
EffectiveApiKeyDescriptor.fromLegacyEnvVar("PDF_UMBENENNER_API_KEY"), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -478,9 +480,9 @@ class EditorConfigurationValidatorTest {
|
|||||||
EditorValidationInput input = new EditorValidationInput(
|
EditorValidationInput input = new EditorValidationInput(
|
||||||
"openai-compatible", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"openai-compatible", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"", "", "30", EffectiveApiKeyDescriptor.absent(),
|
"", "", "30", EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"https://api.openai.com", "gpt-4", "30",
|
"https://api.openai.com", "gpt-4", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile());
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-openai");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
@@ -498,8 +500,8 @@ class EditorConfigurationValidatorTest {
|
|||||||
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
"claude", "C:/source", "C:/target", "C:/db.sqlite", "C:/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-5-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-claude",
|
||||||
"", "", "", EffectiveApiKeyDescriptor.absent());
|
"", "", "", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
EditorValidationReport report = validator.validate(input);
|
EditorValidationReport report = validator.validate(input);
|
||||||
|
|
||||||
|
|||||||
+29
-8
@@ -29,9 +29,9 @@ class ProviderTechnicalTestServiceTest {
|
|||||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||||
"3", "10", "2000",
|
"3", "10", "2000",
|
||||||
"https://api.anthropic.com", model, "30",
|
"https://api.anthropic.com", model, "30",
|
||||||
apiKeyDescriptor,
|
apiKeyDescriptor, "sk-test",
|
||||||
"https://api.openai.com", "gpt-4", "30",
|
"https://api.openai.com", "gpt-4", "30",
|
||||||
EffectiveApiKeyDescriptor.absent());
|
EffectiveApiKeyDescriptor.absent(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static EditorValidationInput openaiInput(EffectiveApiKeyDescriptor apiKeyDescriptor,
|
private static EditorValidationInput openaiInput(EffectiveApiKeyDescriptor apiKeyDescriptor,
|
||||||
@@ -41,9 +41,9 @@ class ProviderTechnicalTestServiceTest {
|
|||||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||||
"3", "10", "2000",
|
"3", "10", "2000",
|
||||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.absent(),
|
EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"https://api.openai.com", model, "30",
|
"https://api.openai.com", model, "30",
|
||||||
apiKeyDescriptor);
|
apiKeyDescriptor, "sk-test");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static EffectiveApiKeyDescriptor keyFromEnv() {
|
private static EffectiveApiKeyDescriptor keyFromEnv() {
|
||||||
@@ -60,11 +60,32 @@ class ProviderTechnicalTestServiceTest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stub-Port der immer den über den Konstruktor übergebenen Descriptor zurückgibt,
|
* Stub-Port der immer den über den Konstruktor übergebenen Descriptor zurückgibt,
|
||||||
* unabhängig von family und propertyValue.
|
* unabhängig von family und propertyValue. Liefert den Roh-API-Schlüssel aus
|
||||||
|
* {@code propertyValue}, falls gesetzt; sonst einen festen Stub-Wert, sobald der
|
||||||
|
* Descriptor eine Quelle meldet.
|
||||||
*/
|
*/
|
||||||
private static de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort
|
private static de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort
|
||||||
apiKeyPort(EffectiveApiKeyDescriptor descriptor) {
|
apiKeyPort(EffectiveApiKeyDescriptor descriptor) {
|
||||||
return (family, propertyValue) -> descriptor;
|
return new de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort() {
|
||||||
|
@Override
|
||||||
|
public EffectiveApiKeyDescriptor resolve(
|
||||||
|
de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily family,
|
||||||
|
String propertyValue) {
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.util.Optional<String> resolveEffectiveApiKeyValue(
|
||||||
|
de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily family,
|
||||||
|
String propertyValue) {
|
||||||
|
if (propertyValue != null && !propertyValue.isBlank()) {
|
||||||
|
return java.util.Optional.of(propertyValue);
|
||||||
|
}
|
||||||
|
return descriptor.isAbsent()
|
||||||
|
? java.util.Optional.empty()
|
||||||
|
: java.util.Optional.of("stub-key");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -388,9 +409,9 @@ class ProviderTechnicalTestServiceTest {
|
|||||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||||
"3", "10", "2000",
|
"3", "10", "2000",
|
||||||
"", "model", "30",
|
"", "model", "30",
|
||||||
EffectiveApiKeyDescriptor.absent(),
|
EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"", "model", "30",
|
"", "model", "30",
|
||||||
EffectiveApiKeyDescriptor.absent());
|
EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
List<CheckpointResult> results = service.runProviderChecks(input);
|
List<CheckpointResult> results = service.runProviderChecks(input);
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -31,9 +31,9 @@ class TechnicalTestOrchestratorTest {
|
|||||||
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-test",
|
||||||
"https://api.openai.com", "gpt-4", "30",
|
"https://api.openai.com", "gpt-4", "30",
|
||||||
EffectiveApiKeyDescriptor.absent());
|
EffectiveApiKeyDescriptor.absent(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,9 +45,9 @@ class TechnicalTestOrchestratorTest {
|
|||||||
"", "", "", "",
|
"", "", "", "",
|
||||||
"", "", "",
|
"", "", "",
|
||||||
"", "", "",
|
"", "", "",
|
||||||
EffectiveApiKeyDescriptor.absent(),
|
EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"", "", "",
|
"", "", "",
|
||||||
EffectiveApiKeyDescriptor.absent());
|
EffectiveApiKeyDescriptor.absent(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** No-op {@link PathCheckPort}: alle Prüfungen liefern {@code false}. */
|
/** No-op {@link PathCheckPort}: alle Prüfungen liefern {@code false}. */
|
||||||
@@ -372,9 +372,9 @@ class TechnicalTestOrchestratorTest {
|
|||||||
"", // kein Prompt-Pfad
|
"", // kein Prompt-Pfad
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-test",
|
||||||
"https://api.openai.com", "gpt-4", "30",
|
"https://api.openai.com", "gpt-4", "30",
|
||||||
EffectiveApiKeyDescriptor.absent());
|
EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
// PathCheckPort: Dateien fehlen, aber Elternordner sind schreibbar
|
// PathCheckPort: Dateien fehlen, aber Elternordner sind schreibbar
|
||||||
PathCheckPort pathPort = new PathCheckPort() {
|
PathCheckPort pathPort = new PathCheckPort() {
|
||||||
@@ -417,9 +417,9 @@ class TechnicalTestOrchestratorTest {
|
|||||||
"", // kein Prompt-Pfad
|
"", // kein Prompt-Pfad
|
||||||
"3", "10", "500",
|
"3", "10", "500",
|
||||||
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
"https://api.anthropic.com", "claude-3-sonnet", "30",
|
||||||
EffectiveApiKeyDescriptor.fromPropertyFile(),
|
EffectiveApiKeyDescriptor.fromPropertyFile(), "sk-test",
|
||||||
"https://api.openai.com", "gpt-4", "30",
|
"https://api.openai.com", "gpt-4", "30",
|
||||||
EffectiveApiKeyDescriptor.absent());
|
EffectiveApiKeyDescriptor.absent(), "");
|
||||||
|
|
||||||
PathCheckPort pathPort = new PathCheckPort() {
|
PathCheckPort pathPort = new PathCheckPort() {
|
||||||
@Override public boolean isDirectoryReadable(String p) { return true; }
|
@Override public boolean isDirectoryReadable(String p) { return true; }
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@ class TechnicalTestRequestTest {
|
|||||||
private static EditorValidationInput minimalInput() {
|
private static EditorValidationInput minimalInput() {
|
||||||
return new EditorValidationInput(
|
return new EditorValidationInput(
|
||||||
"claude", "", "", "", "", "3", "10", "2000",
|
"claude", "", "", "", "", "3", "10", "2000",
|
||||||
"", "model-x", "60", EffectiveApiKeyDescriptor.absent(),
|
"", "model-x", "60", EffectiveApiKeyDescriptor.absent(), "",
|
||||||
"", "", "60", EffectiveApiKeyDescriptor.absent());
|
"", "", "60", EffectiveApiKeyDescriptor.absent(), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user