Titellänge nun parametrisierbar

This commit is contained in:
2026-04-22 09:53:03 +02:00
parent 088fd85572
commit 8286d0f0e5
74 changed files with 1450 additions and 236 deletions
@@ -1370,7 +1370,7 @@ public final class GuiConfigurationEditorWorkspace {
// =========================================================================
/**
* Builds the "Verarbeitungslimits" section with text fields for the three numeric limit
* Builds the "Verarbeitungslimits" section with text fields for the numeric limit
* parameters and a checkbox for the sensitive-logging flag.
*
* @return the card node for the "Verarbeitungslimits" section
@@ -1392,6 +1392,11 @@ public final class GuiConfigurationEditorWorkspace {
val -> updateValues(editorState.values().withMaxTextCharacters(val)));
addSimpleRow(grid, row++, "Maximale Zeichenzahl:", maxCharsField);
TextField maxTitleLengthField = boundTextField(
editorState.values().maxTitleLength(),
val -> updateValues(editorState.values().withMaxTitleLength(val)));
addSimpleRow(grid, row++, "Max. Titellänge (Zeichen):", maxTitleLengthField);
TextField maxRetriesField = boundTextField(
editorState.values().maxRetriesTransient(),
val -> updateValues(editorState.values().withMaxRetriesTransient(val)));
@@ -1587,6 +1592,7 @@ public final class GuiConfigurationEditorWorkspace {
values.maxRetriesTransient(),
values.maxPages(),
values.maxTextCharacters(),
values.maxTitleLength(),
claudeState.baseUrl(),
claudeState.model(),
claudeState.timeoutSeconds(),
@@ -92,6 +92,7 @@ public final class GuiApiKeyMerger {
current.maxRetriesTransient(),
current.maxPages(),
current.maxTextCharacters(),
current.maxTitleLength(),
current.logAiSensitive(),
current.activeProviderFamily(),
merged);
@@ -25,6 +25,7 @@ public final class GuiConfigurationEditorStateFactory {
private static final String PROP_MAX_RETRIES_TRANSIENT = "max.retries.transient";
private static final String PROP_MAX_PAGES = "max.pages";
private static final String PROP_MAX_TEXT_CHARACTERS = "max.text.characters";
private static final String PROP_MAX_TITLE_LENGTH = "max.title.length";
private static final String PROP_LOG_AI_SENSITIVE = "log.ai.sensitive";
private static final String PROP_ACTIVE_PROVIDER = "ai.provider.active";
private static final String PROP_CLAUDE_BASE_URL = "ai.provider.claude.baseUrl";
@@ -74,6 +75,7 @@ public final class GuiConfigurationEditorStateFactory {
propertyOrBlank(properties, PROP_MAX_RETRIES_TRANSIENT),
propertyOrBlank(properties, PROP_MAX_PAGES),
propertyOrBlank(properties, PROP_MAX_TEXT_CHARACTERS),
propertyOrBlank(properties, PROP_MAX_TITLE_LENGTH),
propertyOrBlank(properties, PROP_LOG_AI_SENSITIVE),
propertyOrBlank(properties, PROP_ACTIVE_PROVIDER),
providerConfigurations);
@@ -24,6 +24,7 @@ public final class GuiConfigurationTemplateFactory {
private static final String MAX_RETRIES_TRANSIENT = "3";
private static final String MAX_PAGES = "10";
private static final String MAX_TEXT_CHARACTERS = "5000";
private static final String DEFAULT_MAX_TITLE_LENGTH = "60";
private static final String OPENAI_BASE_URL = "https://api.openai.com/v1";
private static final String OPENAI_MODEL = "gpt-4o-mini";
@@ -83,6 +84,7 @@ public final class GuiConfigurationTemplateFactory {
"",
"",
"",
"",
Map.of());
return new GuiConfigurationEditorState(Optional.empty(), blankValues, blankValues, Optional.empty());
}
@@ -116,6 +118,7 @@ public final class GuiConfigurationTemplateFactory {
MAX_RETRIES_TRANSIENT,
MAX_PAGES,
MAX_TEXT_CHARACTERS,
DEFAULT_MAX_TITLE_LENGTH,
Boolean.toString(false),
AiProviderFamily.CLAUDE.getIdentifier(),
providerConfigurations);
@@ -23,6 +23,7 @@ import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
* @param maxRetriesTransient transient retry limit as editable text
* @param maxPages page limit as editable text
* @param maxTextCharacters text limit as editable text
* @param maxTitleLength maximum base-title length as editable text
* @param logAiSensitive raw value of {@code log.ai.sensitive} as editable text
* @param activeProviderFamily raw value of {@code ai.provider.active} as editable text
* @param providerConfigurations provider-specific editor state keyed by provider family
@@ -38,6 +39,7 @@ public record GuiConfigurationValues(
String maxRetriesTransient,
String maxPages,
String maxTextCharacters,
String maxTitleLength,
String logAiSensitive,
String activeProviderFamily,
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations) {
@@ -55,6 +57,7 @@ public record GuiConfigurationValues(
* @param maxRetriesTransient transient retry limit; {@code null} becomes an empty string
* @param maxPages page limit; {@code null} becomes an empty string
* @param maxTextCharacters text limit; {@code null} becomes an empty string
* @param maxTitleLength maximum base-title length; {@code null} becomes an empty string
* @param logAiSensitive raw {@code log.ai.sensitive} value; {@code null} becomes an empty string
* @param activeProviderFamily raw {@code ai.provider.active} value; {@code null} becomes an empty string
* @param providerConfigurations provider-specific state map; must not be {@code null}
@@ -70,6 +73,7 @@ public record GuiConfigurationValues(
maxRetriesTransient = normalizeText(maxRetriesTransient);
maxPages = normalizeText(maxPages);
maxTextCharacters = normalizeText(maxTextCharacters);
maxTitleLength = normalizeText(maxTitleLength);
logAiSensitive = normalizeText(logAiSensitive);
activeProviderFamily = normalizeText(activeProviderFamily);
@@ -98,7 +102,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withActiveProviderFamily(String providerFamily) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, providerFamily, providerConfigurations);
maxTitleLength, logAiSensitive, providerFamily, providerConfigurations);
}
/**
@@ -110,7 +114,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withSourceFolder(String value) {
return new GuiConfigurationValues(value, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -122,7 +126,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withTargetFolder(String value) {
return new GuiConfigurationValues(sourceFolder, value, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -134,7 +138,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withSqliteFile(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, value, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -146,7 +150,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withPromptTemplateFile(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, value,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -158,7 +162,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withRuntimeLockFile(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
value, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -170,7 +174,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withLogDirectory(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, value, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -182,7 +186,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withLogLevel(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, value, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -194,7 +198,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withMaxRetriesTransient(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, value, maxPages, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -206,7 +210,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withMaxPages(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, value, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -218,7 +222,19 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withMaxTextCharacters(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, value,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
* Returns a copy with a different maximum base-title length value.
*
* @param value new value; {@code null} becomes an empty string
* @return a new configuration values object with the requested title-length value
*/
public GuiConfigurationValues withMaxTitleLength(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
value, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -230,7 +246,7 @@ public record GuiConfigurationValues(
public GuiConfigurationValues withLogAiSensitive(String value) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
value, activeProviderFamily, providerConfigurations);
maxTitleLength, value, activeProviderFamily, providerConfigurations);
}
/**
@@ -243,7 +259,7 @@ public record GuiConfigurationValues(
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, activeProviderFamily, providerConfigurations);
maxTitleLength, logAiSensitive, activeProviderFamily, providerConfigurations);
}
/**
@@ -157,7 +157,7 @@ class GuiConfigurationEditorWorkspaceSaveTest {
return new GuiConfigurationValues(
"./source", "./target", "./db.sqlite", "./prompt.txt",
"./app.lock", "./logs", "INFO", "3", "10", "5000",
"false", "claude", providers);
"60", "false", "claude", providers);
}
private GuiConfigurationEditorState buildState(GuiConfigurationValues baseline,
@@ -174,6 +174,7 @@ class GuiDirtyStateTest {
v.maxRetriesTransient(),
v.maxPages(),
v.maxTextCharacters(),
v.maxTitleLength(),
v.logAiSensitive(),
v.activeProviderFamily(),
v.providerConfigurations());
@@ -93,6 +93,8 @@ class GuiEditorFieldBindingTest {
"Max pages must match the standard template default");
assertEquals("5000", v.maxTextCharacters(),
"Max text characters must match the standard template default");
assertEquals("60", v.maxTitleLength(),
"Max title length must match the standard template default");
assertEquals("false", v.logAiSensitive(),
"log.ai.sensitive must match the standard template default (false)");
});
@@ -422,6 +424,7 @@ class GuiEditorFieldBindingTest {
.withMaxRetriesTransient("5")
.withMaxPages("20")
.withMaxTextCharacters("1000")
.withMaxTitleLength("80")
.withLogAiSensitive("true")
.withActiveProviderFamily("openai-compatible");
@@ -435,6 +438,7 @@ class GuiEditorFieldBindingTest {
assertEquals("5", modified.maxRetriesTransient());
assertEquals("20", modified.maxPages());
assertEquals("1000", modified.maxTextCharacters());
assertEquals("80", modified.maxTitleLength());
assertEquals("true", modified.logAiSensitive());
assertEquals("openai-compatible", modified.activeProviderFamily());
@@ -431,6 +431,162 @@ class GuiEditorValidationSmokeTest {
});
}
// =========================================================================
// Scenario: max.title.length validation per value band
// =========================================================================
/**
* Smoke test: when the standard template is applied and the title-length field is cleared
* via the {@code withMaxTitleLength("")} copy, the local validation produces an ERROR
* finding for {@code max.title.length}.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void emptyMaxTitleLength_producesFieldFindingError() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws =
new GuiConfigurationEditorWorkspace(Optional.empty());
ws.requestNewConfiguration();
ws.editorState = ws.editorState().withValues(
ws.editorState().values().withMaxTitleLength(""));
ws.validateButton.fire();
assertNotNull(ws.lastValidationResult(),
"lastValidationResult must not be null after editing");
assertTrue(ws.lastValidationResult().hasFieldFindingFor("max.title.length"),
"Clearing max.title.length must produce a field finding");
boolean hasErrorForField = ws.lastValidationResult().fieldFindings().stream()
.anyMatch(f -> "max.title.length".equals(f.fieldKey())
&& f.severity() == GuiMessageSeverity.ERROR);
assertTrue(hasErrorForField,
"Empty max.title.length must be an ERROR for this field");
});
}
/**
* Smoke test: a too-small title-length value (below the minimum of 10) produces an ERROR
* finding for the field.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void tooSmallMaxTitleLength_producesFieldFindingError() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws =
new GuiConfigurationEditorWorkspace(Optional.empty());
ws.requestNewConfiguration();
ws.editorState = ws.editorState().withValues(
ws.editorState().values().withMaxTitleLength("5"));
ws.validateButton.fire();
assertTrue(ws.lastValidationResult().hasFieldFindingFor("max.title.length"),
"Value below minimum must produce a field finding");
boolean hasErrorForField = ws.lastValidationResult().fieldFindings().stream()
.anyMatch(f -> "max.title.length".equals(f.fieldKey())
&& f.severity() == GuiMessageSeverity.ERROR);
assertTrue(hasErrorForField,
"Value below minimum must be an ERROR for this field");
});
}
/**
* Smoke test: a too-large title-length value (above the upper limit of 120) produces an ERROR
* finding for the field.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void tooLargeMaxTitleLength_producesFieldFindingError() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws =
new GuiConfigurationEditorWorkspace(Optional.empty());
ws.requestNewConfiguration();
ws.editorState = ws.editorState().withValues(
ws.editorState().values().withMaxTitleLength("200"));
ws.validateButton.fire();
assertTrue(ws.lastValidationResult().hasFieldFindingFor("max.title.length"),
"Value above safe maximum must produce a field finding");
boolean hasErrorForField = ws.lastValidationResult().fieldFindings().stream()
.anyMatch(f -> "max.title.length".equals(f.fieldKey())
&& f.severity() == GuiMessageSeverity.ERROR);
assertTrue(hasErrorForField,
"Value above safe maximum must be an ERROR for this field");
});
}
/**
* Smoke test: a value in the lower warning band (10..19) produces a field finding that is
* not marked as ERROR.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void lowWarnMaxTitleLength_producesWarningOnly() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws =
new GuiConfigurationEditorWorkspace(Optional.empty());
ws.requestNewConfiguration();
ws.editorState = ws.editorState().withValues(
ws.editorState().values().withMaxTitleLength("15"));
ws.validateButton.fire();
assertTrue(ws.lastValidationResult().hasFieldFindingFor("max.title.length"),
"Value in low warn band must produce a field finding");
boolean hasErrorForField = ws.lastValidationResult().fieldFindings().stream()
.anyMatch(f -> "max.title.length".equals(f.fieldKey())
&& f.severity() == GuiMessageSeverity.ERROR);
assertFalse(hasErrorForField,
"Value in low warn band must not produce an ERROR for this field");
});
}
/**
* Smoke test: a value in the upper warning band (100..120) produces a field finding that is
* not marked as ERROR.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void highWarnMaxTitleLength_producesWarningOnly() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws =
new GuiConfigurationEditorWorkspace(Optional.empty());
ws.requestNewConfiguration();
ws.editorState = ws.editorState().withValues(
ws.editorState().values().withMaxTitleLength("110"));
ws.validateButton.fire();
assertTrue(ws.lastValidationResult().hasFieldFindingFor("max.title.length"),
"Value in high warn band must produce a field finding");
boolean hasErrorForField = ws.lastValidationResult().fieldFindings().stream()
.anyMatch(f -> "max.title.length".equals(f.fieldKey())
&& f.severity() == GuiMessageSeverity.ERROR);
assertFalse(hasErrorForField,
"Value in high warn band must not produce an ERROR for this field");
});
}
/**
* Smoke test: the default template value of 60 produces no finding for the title-length field.
*
* @throws Exception if the FX thread task fails or times out
*/
@Test
void defaultMaxTitleLength_producesNoFieldFinding() throws Exception {
runOnFx(() -> {
GuiConfigurationEditorWorkspace ws =
new GuiConfigurationEditorWorkspace(Optional.empty());
ws.requestNewConfiguration();
assertNotNull(ws.lastValidationResult(),
"lastValidationResult must not be null after 'Neu'");
assertFalse(ws.lastValidationResult().hasFieldFindingFor("max.title.length"),
"Default value 60 must not produce a field finding");
});
}
// =========================================================================
// Helpers
// =========================================================================
@@ -458,6 +614,7 @@ class GuiEditorValidationSmokeTest {
+ "max.retries.transient=3\n"
+ "max.pages=10\n"
+ "max.text.characters=500\n"
+ "max.title.length=60\n"
+ "prompt.template.file=./config/prompt.txt\n";
Files.writeString(path, content, StandardCharsets.UTF_8);
}
@@ -246,7 +246,7 @@ class GuiTechnicalTestCoordinatorSmokeTest {
new EditorValidationInput(
"claude",
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
"3", "10", "500",
"3", "10", "500", "60",
"https://api.anthropic.com", "claude-3-sonnet", "30",
EffectiveApiKeyDescriptor.absent(), "",
"https://api.openai.com", "gpt-4", "30",
@@ -282,7 +282,7 @@ class GuiTechnicalTestCoordinatorSmokeTest {
currentInput.set(new EditorValidationInput(
"", // empty active provider → validation error in block 1
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
"3", "10", "500",
"3", "10", "500", "60",
"https://api.anthropic.com", "claude-3-sonnet", "30",
EffectiveApiKeyDescriptor.absent(), "",
"https://api.openai.com", "gpt-4", "30",
@@ -369,7 +369,7 @@ class GuiTechnicalTestCoordinatorSmokeTest {
EditorValidationInput blankInput = new EditorValidationInput(
"claude",
"/src", "/tgt", "/db.sqlite", "/prompt.txt",
"3", "10", "2000",
"3", "10", "2000", "60",
"https://api.anthropic.com", "claude-3-sonnet", "30",
EffectiveApiKeyDescriptor.absent(), "",
"https://api.openai.com", "gpt-4", "30",
@@ -874,6 +874,7 @@ class GuiUnsavedChangesGuardSmokeTest {
v.maxRetriesTransient(),
v.maxPages(),
v.maxTextCharacters(),
v.maxTitleLength(),
v.logAiSensitive(),
v.activeProviderFamily(),
v.providerConfigurations());
@@ -166,6 +166,7 @@ class GuiWindowTitleFormatterTest {
v.maxRetriesTransient(),
v.maxPages(),
v.maxTextCharacters(),
v.maxTitleLength(),
v.logAiSensitive(),
v.activeProviderFamily(),
v.providerConfigurations());
@@ -19,7 +19,7 @@ class ConfirmationDialogContentTest {
@Test
void fromPlan_extractsDescriptionsInOrder() {
var s1 = new CorrectionSuggestion.CreateDirectory("/path/a", "Zielordner anlegen");
var s2 = new CorrectionSuggestion.CreatePromptFile("/path/prompt.txt", "Prompt-Datei erzeugen");
var s2 = new CorrectionSuggestion.CreatePromptFile("/path/prompt.txt", "Prompt-Datei erzeugen", 60);
var plan = new CorrectionPlan(List.of(s1, s2));
var content = ConfirmationDialogContent.fromPlan(plan);
@@ -0,0 +1,78 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Properties;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link GuiConfigurationEditorStateFactory}.
* <p>
* Verifies that loaded properties are correctly mapped into the editor state, with specific
* attention to the {@code max.title.length} property mapping.
*/
class GuiConfigurationEditorStateFactoryTest {
@Test
void fromPropertiesSnapshot_mapsMaxTitleLengthWhenPresent() {
Properties props = new Properties();
props.setProperty("source.folder", "./s");
props.setProperty("target.folder", "./t");
props.setProperty("sqlite.file", "./db");
props.setProperty("prompt.template.file", "./p.txt");
props.setProperty("ai.provider.active", "claude");
props.setProperty("max.retries.transient", "3");
props.setProperty("max.pages", "10");
props.setProperty("max.text.characters", "5000");
props.setProperty("max.title.length", "80");
GuiConfigurationFileSnapshot snapshot =
new GuiConfigurationFileSnapshot(Path.of("config/application.properties"), props);
GuiConfigurationEditorState state =
GuiConfigurationEditorStateFactory.fromPropertiesSnapshot(snapshot, Optional.empty());
assertEquals("80", state.values().maxTitleLength(),
"Loaded max.title.length value must be present in the editor state");
assertTrue(state.hasLoadedFileSnapshot());
assertFalse(state.isDirty());
}
@Test
void fromPropertiesSnapshot_missingMaxTitleLengthBecomesBlank() {
Properties props = new Properties();
props.setProperty("source.folder", "./s");
props.setProperty("target.folder", "./t");
props.setProperty("ai.provider.active", "claude");
// max.title.length intentionally omitted
GuiConfigurationFileSnapshot snapshot =
new GuiConfigurationFileSnapshot(Path.of("config/application.properties"), props);
GuiConfigurationEditorState state =
GuiConfigurationEditorStateFactory.fromPropertiesSnapshot(snapshot, Optional.empty());
assertEquals("", state.values().maxTitleLength(),
"A missing max.title.length property must be mapped to an empty string");
}
@Test
void fromPropertiesSnapshot_blankMaxTitleLengthBecomesBlank() {
Properties props = new Properties();
props.setProperty("source.folder", "./s");
props.setProperty("target.folder", "./t");
props.setProperty("ai.provider.active", "claude");
props.setProperty("max.title.length", " ");
GuiConfigurationFileSnapshot snapshot =
new GuiConfigurationFileSnapshot(Path.of("config/application.properties"), props);
GuiConfigurationEditorState state =
GuiConfigurationEditorStateFactory.fromPropertiesSnapshot(snapshot, Optional.empty());
assertEquals("", state.values().maxTitleLength(),
"A blank max.title.length property must be trimmed to an empty string");
}
}
@@ -24,6 +24,7 @@ class GuiConfigurationEditorStateTest {
state.values().maxRetriesTransient(),
state.values().maxPages(),
state.values().maxTextCharacters(),
state.values().maxTitleLength(),
"maybe",
"claude-42",
state.values().providerConfigurations());
@@ -90,6 +91,7 @@ class GuiConfigurationEditorStateTest {
state.values().maxRetriesTransient(),
state.values().maxPages(),
state.values().maxTextCharacters(),
state.values().maxTitleLength(),
"true",
"openai-compatible",
state.values().providerConfigurations());
@@ -34,6 +34,7 @@ class GuiConfigurationTemplateFactoryTest {
assertEquals("3", values.maxRetriesTransient());
assertEquals("10", values.maxPages());
assertEquals("5000", values.maxTextCharacters());
assertEquals("60", values.maxTitleLength());
assertEquals("false", values.logAiSensitive());
assertEquals(AiProviderFamily.CLAUDE.getIdentifier(), values.activeProviderFamily());
@@ -69,6 +70,8 @@ class GuiConfigurationTemplateFactoryTest {
GuiConfigurationValues values = state.values();
assertEquals("./work/local/source", values.sourceFolder());
assertEquals("./work/local/target", values.targetFolder());
assertEquals("60", values.maxTitleLength(),
"Standard template must supply the default title-length value");
assertEquals(AiProviderFamily.CLAUDE.getIdentifier(), values.activeProviderFamily());
assertFalse(values.providerConfigurations().isEmpty());
}
@@ -93,6 +96,7 @@ class GuiConfigurationTemplateFactoryTest {
assertEquals("", values.maxRetriesTransient());
assertEquals("", values.maxPages());
assertEquals("", values.maxTextCharacters());
assertEquals("", values.maxTitleLength());
assertEquals("", values.logAiSensitive());
assertEquals("", values.activeProviderFamily());
assertTrue(values.providerConfigurations().isEmpty());
@@ -28,10 +28,12 @@ class GuiConfigurationValuesTest {
"12",
"34",
"56",
"78",
"maybe",
"not-a-provider-family",
providerConfigurations);
assertEquals("78", values.maxTitleLength());
assertEquals("maybe", values.logAiSensitive());
assertEquals("not-a-provider-family", values.activeProviderFamily());
assertEquals(GuiProviderConfigurationState.blank(), values.providerConfiguration(AiProviderFamily.CLAUDE));
@@ -53,6 +55,7 @@ class GuiConfigurationValuesTest {
"12",
"34",
"56",
"60",
"true",
AiProviderFamily.CLAUDE.getIdentifier(),
providerConfigurations);
@@ -62,4 +65,58 @@ class GuiConfigurationValuesTest {
assertNotSame(providerConfigurations, values.providerConfigurations());
assertEquals(1, values.providerConfigurations().size());
}
@Test
void withMaxTitleLength_producesIndependentCopy() {
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations = new LinkedHashMap<>();
providerConfigurations.put(AiProviderFamily.CLAUDE, GuiProviderConfigurationState.blank());
GuiConfigurationValues original = new GuiConfigurationValues(
"./source",
"./target",
"./config/db.sqlite",
"./config/prompt.txt",
"./config/runtime.lock",
"./logs",
"INFO",
"3",
"10",
"5000",
"60",
"false",
AiProviderFamily.CLAUDE.getIdentifier(),
providerConfigurations);
GuiConfigurationValues updated = original.withMaxTitleLength("80");
assertEquals("80", updated.maxTitleLength());
assertEquals("60", original.maxTitleLength(),
"Original instance must remain unchanged");
assertEquals(original.sourceFolder(), updated.sourceFolder(),
"Unrelated fields must be preserved when copying");
}
@Test
void nullMaxTitleLengthBecomesEmptyString() {
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations = new LinkedHashMap<>();
providerConfigurations.put(AiProviderFamily.CLAUDE, GuiProviderConfigurationState.blank());
GuiConfigurationValues values = new GuiConfigurationValues(
"./source",
"./target",
"./config/db.sqlite",
"./config/prompt.txt",
"./config/runtime.lock",
"./logs",
"INFO",
"3",
"10",
"5000",
null,
"false",
AiProviderFamily.CLAUDE.getIdentifier(),
providerConfigurations);
assertEquals("", values.maxTitleLength());
}
}