Fix doppelte API-Key-Meldung: originLabel zeigt nur noch ENV-Hinweis

Der API-Key-Herkunfts-Label (apiKeyOriginLabel) zeigte bisher sowohl
INFO-Befunde (Schlüssel kommt aus Umgebungsvariable) als auch
WARNING/ERROR-Befunde (Schlüssel fehlt) an. Da das fieldErrorLabel
direkt darunter dieselben WARNING/ERROR-Befunde bereits anzeigt,
erschien die „Kein API-Key"-Meldung zweimal im selben Bereich.

Lösung: refreshApiKeyOriginLabels() wertet nur noch INFO-Befunde aus.
WARNING/ERROR-Befunde für fehlende API-Keys werden ausschließlich vom
fieldErrorLabel angezeigt. STYLE_ORIGIN_MISSING entfernt.

Drei neue Smoke-Tests sichern das Verhalten ab:
- apiKeyAbsent_originLabelHidden
- apiKeyAbsent_fieldErrorLabelVisible
- apiKeyAbsent_noDuplicateMessageInPendingMessages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 13:42:18 +02:00
parent 0f07947879
commit 4875a1ed42
2 changed files with 85 additions and 22 deletions
@@ -1967,14 +1967,14 @@ public final class GuiConfigurationEditorWorkspace {
/** /**
* Updates the API-key origin labels below each provider's API-key field. * Updates the API-key origin labels below each provider's API-key field.
* <p> * <p>
* The display logic follows the effective-key precedence order: * The label is shown exclusively when the effective key comes from an environment variable
* <ol> * (INFO finding). In all other cases the label is hidden:
* <li>If the effective key comes from an environment variable, the label shows the * <ul>
* variable name in an informational style.</li> * <li>Key from properties file: label hidden (no action required).</li>
* <li>If the property value is filled and no environment variable overrides it, the label * <li>Key absent (WARNING finding): label hidden — the field-error label registered in
* is hidden.</li> * {@link #fieldErrorLabels} already displays this finding directly below the field;
* <li>If no key source is effective, the label shows a warning in the field-error style.</li> * showing it a second time in the origin label would produce a visible duplicate.</li>
* </ol> * </ul>
* <p> * <p>
* The origin information is derived from the pending field findings produced by the validator, * The origin information is derived from the pending field findings produced by the validator,
* which already resolved the API-key precedence for both providers. * which already resolved the API-key precedence for both providers.
@@ -1988,28 +1988,20 @@ public final class GuiConfigurationEditorWorkspace {
String apiKeyField = "ai.provider." + family.getIdentifier() + ".apiKey"; String apiKeyField = "ai.provider." + family.getIdentifier() + ".apiKey";
// Look for an INFO finding about ENV-variable origin for this provider. // Look for an INFO finding about ENV-variable origin for this provider.
// WARNING/ERROR findings (missing key) are intentionally excluded here:
// those are already shown by the field-error label registered in fieldErrorLabels,
// and repeating them in the origin label would cause the same text to appear twice
// in close proximity below the API-key field.
Optional<GuiFieldFinding> envFinding = pendingFieldFindings.stream() Optional<GuiFieldFinding> envFinding = pendingFieldFindings.stream()
.filter(f -> apiKeyField.equals(f.fieldKey())) .filter(f -> apiKeyField.equals(f.fieldKey()))
.filter(f -> f.severity() == GuiMessageSeverity.INFO) .filter(f -> f.severity() == GuiMessageSeverity.INFO)
.findFirst(); .findFirst();
// Look for an ERROR/WARNING finding about missing key for this provider.
Optional<GuiFieldFinding> missingFinding = pendingFieldFindings.stream()
.filter(f -> apiKeyField.equals(f.fieldKey()))
.filter(f -> f.severity() == GuiMessageSeverity.ERROR
|| f.severity() == GuiMessageSeverity.WARNING)
.findFirst();
if (envFinding.isPresent()) { if (envFinding.isPresent()) {
label.setText(envFinding.get().text()); label.setText(envFinding.get().text());
label.setStyle(STYLE_ORIGIN_INFO); label.setStyle(STYLE_ORIGIN_INFO);
label.setVisible(true); label.setVisible(true);
label.setManaged(true); label.setManaged(true);
} else if (missingFinding.isPresent()) {
label.setText(missingFinding.get().text());
label.setStyle(STYLE_ORIGIN_MISSING);
label.setVisible(true);
label.setManaged(true);
} else { } else {
label.setText(""); label.setText("");
label.setVisible(false); label.setVisible(false);
@@ -2020,8 +2012,6 @@ public final class GuiConfigurationEditorWorkspace {
private static final String STYLE_ORIGIN_INFO = private static final String STYLE_ORIGIN_INFO =
"-fx-font-size: 11px; -fx-text-fill: #1565c0; -fx-padding: 0 0 4px 0;"; "-fx-font-size: 11px; -fx-text-fill: #1565c0; -fx-padding: 0 0 4px 0;";
private static final String STYLE_ORIGIN_MISSING =
"-fx-font-size: 11px; -fx-text-fill: #b71c1c; -fx-padding: 0 0 4px 0;";
// ========================================================================= // =========================================================================
// Path picker helpers // Path picker helpers
@@ -361,6 +361,79 @@ class GuiMessageAreaSmokeTest {
}); });
} }
// =========================================================================
// Scenario: absent API key — origin label hidden, no duplicate in message area
// =========================================================================
/**
* Smoke test: when no API key is configured and no environment variable provides one, the
* api-key origin label below the Claude API-key field must be hidden (not visible).
* <p>
* The field-error label (registered in {@code fieldErrorLabels}) already shows the warning
* below the input field; duplicating the same text in the origin label would cause the same
* message to appear twice in close visual proximity. The origin label is therefore hidden
* for the absent-key case.
*/
@Test
void apiKeyAbsent_originLabelHidden() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(Optional.empty());
ws.requestNewConfiguration();
// Standard template has active provider = Claude but no API key value.
Label originLabel = ws.apiKeyOriginLabels.get(AiProviderFamily.CLAUDE);
assertNotNull(originLabel,
"An api-key origin label must be registered for AiProviderFamily.CLAUDE");
assertFalse(originLabel.isVisible(),
"Claude api-key origin label must be HIDDEN when no API key is configured "
+ "— the field-error label already shows the warning, so the origin label "
+ "must not repeat it");
});
}
/**
* Smoke test: when no API key is configured, the field-error label registered for
* {@code ai.provider.claude.apiKey} must be visible and carry a non-blank warning text.
* <p>
* This is the single intended location for the missing-key warning below the input field.
*/
@Test
void apiKeyAbsent_fieldErrorLabelVisible() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(Optional.empty());
ws.requestNewConfiguration();
Label errorLabel = ws.fieldErrorLabels.get("ai.provider.claude.apiKey");
assertNotNull(errorLabel,
"A field-error label must be registered for 'ai.provider.claude.apiKey'");
assertTrue(errorLabel.isVisible(),
"api-key field-error label must be visible when no API key is configured");
assertFalse(errorLabel.getText().isBlank(),
"api-key field-error label must carry a non-blank warning text");
});
}
/**
* Smoke test: when no API key is configured, the text of the missing-key warning must appear
* exactly once in {@code pendingMessages}. Having the same text twice would mean two
* validation sources independently write the same finding to the central message area.
*/
@Test
void apiKeyAbsent_noDuplicateMessageInPendingMessages() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws = new GuiConfigurationEditorWorkspace(Optional.empty());
ws.requestNewConfiguration();
// Collect all message texts that contain the canonical "Kein API-Schlüssel" fragment.
long count = ws.pendingMessages.stream()
.filter(m -> m.text().contains("API-Schlüssel") || m.text().contains("API-Key"))
.count();
assertTrue(count <= 1,
"The missing-API-key finding must appear at most once in pendingMessages, "
+ "but found " + count + " occurrences");
});
}
// ========================================================================= // =========================================================================
// Scenario: INFO-coloured prefix in the message area (model-catalogue success) // Scenario: INFO-coloured prefix in the message area (model-catalogue success)
// ========================================================================= // =========================================================================