M10, AP-001 freigegeben

This commit is contained in:
2026-04-20 12:29:14 +02:00
parent fd5b3c5809
commit 20b847d821
14 changed files with 825 additions and 0 deletions
+1
View File
@@ -74,3 +74,4 @@ replay_pid*
/review-input.zip
/run-milestone.ps1
/run-v11.ps1
.m2repo
@@ -0,0 +1,25 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
/**
* Derived change-state view for the editor content.
* <p>
* The value is computed from the comparison between the baseline values and the current
* editor values.
*/
public enum GuiChangeState {
/** The current editor state matches its baseline. */
CLEAN,
/** The current editor state has diverged from its baseline. */
DIRTY;
/**
* Returns whether this state represents unsaved changes.
*
* @return {@code true} when the editor is dirty, otherwise {@code false}
*/
public boolean isDirty() {
return this == DIRTY;
}
}
@@ -0,0 +1,121 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import java.util.Objects;
import java.util.Optional;
/**
* Root editor state for the GUI configuration editor.
* <p>
* The state keeps the loaded file snapshot, the baseline values and the current editable values
* separate so later GUI layers can compare the current editor content against the original
* representation without relying on a manually maintained dirty flag.
*
* @param loadedFileSnapshot loaded file representation, or empty when the editor was created
* from the standard template
* @param baselineValues values representing the baseline the editor compares against
* @param values current editable configuration values
*/
public record GuiConfigurationEditorState(
Optional<GuiConfigurationFileSnapshot> loadedFileSnapshot,
GuiConfigurationValues baselineValues,
GuiConfigurationValues values) {
/**
* Creates a new editor state.
*
* @param loadedFileSnapshot loaded file representation; {@code null} becomes empty
* @param baselineValues baseline values; must not be {@code null}
* @param values current editable configuration values; must not be {@code null}
*/
public GuiConfigurationEditorState {
loadedFileSnapshot = loadedFileSnapshot == null ? Optional.empty() : loadedFileSnapshot;
baselineValues = Objects.requireNonNull(baselineValues, "baselineValues must not be null");
values = Objects.requireNonNull(values, "values must not be null");
}
/**
* Returns whether the editor currently contains unsaved changes.
*
* @return {@code true} when the current values differ from the baseline, otherwise {@code false}
*/
public boolean isDirty() {
return !baselineValues.equals(values);
}
/**
* Returns the derived change state for the current editor content.
*
* @return {@link GuiChangeState#DIRTY} when the editor is dirty, otherwise {@link GuiChangeState#CLEAN}
*/
public GuiChangeState changeState() {
return isDirty() ? GuiChangeState.DIRTY : GuiChangeState.CLEAN;
}
/**
* Returns whether the editor currently has a loaded file snapshot.
*
* @return {@code true} when the editor was loaded from disk, otherwise {@code false}
*/
public boolean hasLoadedFileSnapshot() {
return loadedFileSnapshot.isPresent();
}
/**
* Returns whether the editor represents a newly created configuration.
*
* @return {@code true} when no file snapshot is loaded, otherwise {@code false}
*/
public boolean isNewConfiguration() {
return loadedFileSnapshot.isEmpty();
}
/**
* Returns a copy with different current editable values.
*
* @param values the new editable values; must not be {@code null}
* @return a new editor state containing the supplied values
*/
public GuiConfigurationEditorState withValues(GuiConfigurationValues values) {
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values);
}
/**
* Returns a copy with a different baseline.
*
* @param baselineValues the new baseline values; must not be {@code null}
* @return a new editor state using the supplied baseline
*/
public GuiConfigurationEditorState withBaselineValues(GuiConfigurationValues baselineValues) {
return new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, values);
}
/**
* Returns a copy with a loaded file snapshot.
*
* @param snapshot the loaded file snapshot; must not be {@code null}
* @return a new editor state linked to the supplied snapshot
*/
public GuiConfigurationEditorState withLoadedFileSnapshot(GuiConfigurationFileSnapshot snapshot) {
return new GuiConfigurationEditorState(Optional.of(snapshot), baselineValues, values);
}
/**
* Returns a copy without any loaded file snapshot.
*
* @return a new editor state without a loaded file snapshot
*/
public GuiConfigurationEditorState withoutLoadedFileSnapshot() {
return new GuiConfigurationEditorState(Optional.empty(), baselineValues, values);
}
/**
* Returns a clean copy of this editor state by resetting the current values to the baseline.
*
* @return a clean editor state
*/
public GuiConfigurationEditorState markClean() {
return baselineValues.equals(values)
? this
: new GuiConfigurationEditorState(loadedFileSnapshot, baselineValues, baselineValues);
}
}
@@ -0,0 +1,86 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Properties;
/**
* Snapshot of a configuration file loaded from disk.
* <p>
* This type is intentionally separate from the editable editor state. It represents the file
* that was loaded, not the mutable values currently being edited.
*/
public final class GuiConfigurationFileSnapshot {
private final Path filePath;
private final Properties properties;
/**
* Creates a snapshot for the given file and properties.
*
* @param filePath path of the loaded file; must not be {@code null}
* @param properties loaded properties; must not be {@code null}
*/
public GuiConfigurationFileSnapshot(Path filePath, Properties properties) {
this.filePath = Objects.requireNonNull(filePath, "filePath must not be null");
this.properties = copyOf(properties);
}
/**
* Returns the file path of the loaded configuration.
*
* @return the snapshot file path; never {@code null}
*/
public Path filePath() {
return filePath;
}
/**
* Returns a defensive copy of the loaded properties.
*
* @return a copy of the loaded properties; never {@code null}
*/
public Properties properties() {
return copyOf(properties);
}
/**
* Returns whether this snapshot was loaded from the given path.
*
* @param candidatePath the path to compare; may be {@code null}
* @return {@code true} when the paths match, otherwise {@code false}
*/
public boolean hasFilePath(Path candidatePath) {
return filePath.equals(candidatePath);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof GuiConfigurationFileSnapshot that)) {
return false;
}
return filePath.equals(that.filePath) && properties.equals(that.properties);
}
@Override
public int hashCode() {
return Objects.hash(filePath, properties);
}
@Override
public String toString() {
return "GuiConfigurationFileSnapshot{"
+ "filePath=" + filePath
+ ", properties=" + properties
+ '}';
}
private static Properties copyOf(Properties source) {
Properties copy = new Properties();
copy.putAll(Objects.requireNonNull(source, "properties must not be null"));
return copy;
}
}
@@ -0,0 +1,83 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
/**
* Creates the standard GUI configuration template.
* <p>
* The template contains both known provider blocks, a complete set of general configuration
* values and a clean editor state that can be used for a brand-new configuration.
*/
public final class GuiConfigurationTemplateFactory {
private static final String SOURCE_FOLDER = "./work/local/source";
private static final String TARGET_FOLDER = "./work/local/target";
private static final String SQLITE_FILE = "./work/local/pdf-umbenenner.db";
private static final String PROMPT_TEMPLATE_FILE = "./config/prompts/template.txt";
private static final String RUNTIME_LOCK_FILE = "./work/local/pdf-umbenenner.lock";
private static final String LOG_DIRECTORY = "./work/local/logs";
private static final String LOG_LEVEL = "INFO";
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 OPENAI_BASE_URL = "https://api.openai.com/v1";
private static final String OPENAI_MODEL = "gpt-4o-mini";
private static final String OPENAI_TIMEOUT_SECONDS = "30";
private static final String CLAUDE_BASE_URL = "https://api.anthropic.com";
private static final String CLAUDE_MODEL = "claude-3-5-sonnet-20241022";
private static final String CLAUDE_TIMEOUT_SECONDS = "60";
private GuiConfigurationTemplateFactory() {
// Utility class.
}
/**
* Creates a brand-new clean editor state containing the standard configuration template.
*
* @return a clean editor state with no loaded file snapshot
*/
public static GuiConfigurationEditorState createStandardTemplate() {
GuiConfigurationValues standardValues = createStandardValues();
return new GuiConfigurationEditorState(Optional.empty(), standardValues, standardValues);
}
/**
* Creates the editable values for the standard configuration template.
*
* @return the standard editable configuration values
*/
public static GuiConfigurationValues createStandardValues() {
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations = new LinkedHashMap<>();
providerConfigurations.put(AiProviderFamily.CLAUDE, new GuiProviderConfigurationState(
CLAUDE_BASE_URL,
CLAUDE_MODEL,
CLAUDE_TIMEOUT_SECONDS,
GuiProviderApiKeyState.unresolved()));
providerConfigurations.put(AiProviderFamily.OPENAI_COMPATIBLE, new GuiProviderConfigurationState(
OPENAI_BASE_URL,
OPENAI_MODEL,
OPENAI_TIMEOUT_SECONDS,
GuiProviderApiKeyState.unresolved()));
return new GuiConfigurationValues(
SOURCE_FOLDER,
TARGET_FOLDER,
SQLITE_FILE,
PROMPT_TEMPLATE_FILE,
RUNTIME_LOCK_FILE,
LOG_DIRECTORY,
LOG_LEVEL,
MAX_RETRIES_TRANSIENT,
MAX_PAGES,
MAX_TEXT_CHARACTERS,
Boolean.toString(false),
AiProviderFamily.CLAUDE.getIdentifier(),
providerConfigurations);
}
}
@@ -0,0 +1,107 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
/**
* Editable GUI values for the complete configuration document.
* <p>
* This object combines all known general configuration values and the provider-specific blocks
* in a single model that can be rendered and edited by the GUI.
*
* @param sourceFolder source-folder path as editable text
* @param targetFolder target-folder path as editable text
* @param sqliteFile SQLite file path as editable text
* @param promptTemplateFile prompt-template path as editable text
* @param runtimeLockFile optional runtime lock file path as editable text
* @param logDirectory optional log directory as editable text
* @param logLevel log level as editable text
* @param maxRetriesTransient transient retry limit as editable text
* @param maxPages page limit as editable text
* @param maxTextCharacters text limit 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
*/
public record GuiConfigurationValues(
String sourceFolder,
String targetFolder,
String sqliteFile,
String promptTemplateFile,
String runtimeLockFile,
String logDirectory,
String logLevel,
String maxRetriesTransient,
String maxPages,
String maxTextCharacters,
String logAiSensitive,
String activeProviderFamily,
Map<AiProviderFamily, GuiProviderConfigurationState> providerConfigurations) {
/**
* Creates a new editable configuration state.
*
* @param sourceFolder source-folder path; {@code null} becomes an empty string
* @param targetFolder target-folder path; {@code null} becomes an empty string
* @param sqliteFile SQLite file path; {@code null} becomes an empty string
* @param promptTemplateFile prompt-template path; {@code null} becomes an empty string
* @param runtimeLockFile runtime lock file path; {@code null} becomes an empty string
* @param logDirectory log directory; {@code null} becomes an empty string
* @param logLevel log level; {@code null} becomes an empty string
* @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 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}
*/
public GuiConfigurationValues {
sourceFolder = normalizeText(sourceFolder);
targetFolder = normalizeText(targetFolder);
sqliteFile = normalizeText(sqliteFile);
promptTemplateFile = normalizeText(promptTemplateFile);
runtimeLockFile = normalizeText(runtimeLockFile);
logDirectory = normalizeText(logDirectory);
logLevel = normalizeText(logLevel);
maxRetriesTransient = normalizeText(maxRetriesTransient);
maxPages = normalizeText(maxPages);
maxTextCharacters = normalizeText(maxTextCharacters);
logAiSensitive = normalizeText(logAiSensitive);
activeProviderFamily = normalizeText(activeProviderFamily);
LinkedHashMap<AiProviderFamily, GuiProviderConfigurationState> copy = new LinkedHashMap<>();
Objects.requireNonNull(providerConfigurations, "providerConfigurations must not be null")
.forEach(copy::put);
providerConfigurations = Collections.unmodifiableMap(copy);
}
/**
* Returns the editable state for the requested provider family.
*
* @param providerFamily the provider family to look up; must not be {@code null}
* @return the matching provider state, or {@code null} when not present
*/
public GuiProviderConfigurationState providerConfiguration(AiProviderFamily providerFamily) {
return providerConfigurations.get(providerFamily);
}
/**
* Returns a copy with a different active provider-family identifier.
*
* @param providerFamily raw provider-family identifier; {@code null} becomes an empty string
* @return a new configuration values object with the requested active provider value
*/
public GuiConfigurationValues withActiveProviderFamily(String providerFamily) {
return new GuiConfigurationValues(sourceFolder, targetFolder, sqliteFile, promptTemplateFile,
runtimeLockFile, logDirectory, logLevel, maxRetriesTransient, maxPages, maxTextCharacters,
logAiSensitive, providerFamily, providerConfigurations);
}
private static String normalizeText(String value) {
return value == null ? "" : value;
}
}
@@ -0,0 +1,67 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import java.util.Objects;
/**
* Editable state for a provider-specific API key.
* <p>
* This value object keeps the editable property value separate from the later displayable
* origin of the effective key value. The editor can therefore preserve the property text
* while later layers determine whether the effective value comes from the file, an
* environment variable, a compatibility fallback, or a technical default.
*
* @param propertyValue editable API-key property value as entered or loaded from the file
* @param effectiveValueOrigin provenance of the effective value shown or inferred later
* @param effectiveValueDescription human-readable provenance description for later display
*/
public record GuiProviderApiKeyState(
String propertyValue,
GuiValueOrigin effectiveValueOrigin,
String effectiveValueDescription) {
/**
* Creates a key state with a blank property value and unresolved provenance.
*
* @return a new unresolved API-key state
*/
public static GuiProviderApiKeyState unresolved() {
return new GuiProviderApiKeyState("", GuiValueOrigin.UNKNOWN, "nicht aufgeloest");
}
/**
* Creates a key state with the supplied property value and unresolved provenance.
*
* @param propertyValue editable property value; may be {@code null}
* @return a new unresolved API-key state
*/
public static GuiProviderApiKeyState unresolved(String propertyValue) {
return new GuiProviderApiKeyState(propertyValue, GuiValueOrigin.UNKNOWN, "nicht aufgeloest");
}
/**
* Creates a key state with an explicit effective-value provenance.
*
* @param propertyValue editable property value; may be {@code null}
* @param effectiveValueOrigin origin of the effective value; must not be {@code null}
* @param effectiveValueDescription human-readable provenance description; may be {@code null}
* @return a new API-key state with the requested provenance
*/
public static GuiProviderApiKeyState resolved(String propertyValue,
GuiValueOrigin effectiveValueOrigin,
String effectiveValueDescription) {
return new GuiProviderApiKeyState(propertyValue, effectiveValueOrigin, effectiveValueDescription);
}
/**
* Creates a new key state.
*
* @param propertyValue editable property value; {@code null} is converted to an empty string
* @param effectiveValueOrigin origin of the effective value; {@code null} becomes {@link GuiValueOrigin#UNKNOWN}
* @param effectiveValueDescription human-readable provenance description; {@code null} becomes an empty string
*/
public GuiProviderApiKeyState {
propertyValue = propertyValue == null ? "" : propertyValue;
effectiveValueOrigin = Objects.requireNonNullElse(effectiveValueOrigin, GuiValueOrigin.UNKNOWN);
effectiveValueDescription = effectiveValueDescription == null ? "" : effectiveValueDescription;
}
}
@@ -0,0 +1,45 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import java.util.Objects;
/**
* Editable GUI state for one provider-family configuration block.
* <p>
* The fields are stored as editor-friendly strings so later GUI layers can bind them directly
* to text-based controls without prematurely parsing or validating them.
*
* @param baseUrl provider endpoint base URL as editable text
* @param model model identifier as editable text
* @param timeoutSeconds timeout value as editable text
* @param apiKey API-key state for the provider
*/
public record GuiProviderConfigurationState(
String baseUrl,
String model,
String timeoutSeconds,
GuiProviderApiKeyState apiKey) {
/**
* Creates a provider configuration state with all text fields blank and the API key unresolved.
*
* @return a blank provider configuration state
*/
public static GuiProviderConfigurationState blank() {
return new GuiProviderConfigurationState("", "", "", GuiProviderApiKeyState.unresolved());
}
/**
* Creates a new provider configuration state.
*
* @param baseUrl provider endpoint base URL; {@code null} becomes an empty string
* @param model model identifier; {@code null} becomes an empty string
* @param timeoutSeconds timeout value; {@code null} becomes an empty string
* @param apiKey API-key state; when {@code null}, an unresolved blank key state is used
*/
public GuiProviderConfigurationState {
baseUrl = baseUrl == null ? "" : baseUrl;
model = model == null ? "" : model;
timeoutSeconds = timeoutSeconds == null ? "" : timeoutSeconds;
apiKey = Objects.requireNonNullElse(apiKey, GuiProviderApiKeyState.unresolved());
}
}
@@ -0,0 +1,25 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
/**
* Describes the effective origin of a configuration value shown by the editor.
* <p>
* The enum is intentionally broad enough to accommodate later provenance display for
* provider-specific values such as API keys.
*/
public enum GuiValueOrigin {
/** The value has not been resolved to a concrete origin yet. */
UNKNOWN,
/** The value comes from the editable property value stored in the configuration file. */
PROPERTY_VALUE,
/** The value comes from a provider-specific environment variable. */
ENVIRONMENT_VARIABLE,
/** The value comes from the legacy environment variable accepted for compatibility. */
LEGACY_ENVIRONMENT_VARIABLE,
/** The value is a technical default supplied by the application. */
DEFAULT_VALUE
}
@@ -0,0 +1,12 @@
/**
* Editor state and template model for the JavaFX configuration editor.
* <p>
* This package contains the GUI-side representation of configuration data that can be edited
* independently from file I/O and validation. It separates loaded file snapshots, baseline
* editor values, current editor values, provider-specific API key state, and the derived
* dirty-state view used by the GUI.
* <p>
* The classes in this package are intentionally free of JavaFX controls so they can be reused
* by later GUI layers without coupling the model to a particular layout implementation.
*/
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
@@ -0,0 +1,81 @@
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 org.junit.jupiter.api.Test;
class GuiConfigurationEditorStateTest {
@Test
void dirtyStateReflectsComparisonAgainstBaseline() {
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
GuiConfigurationValues editedValues = new GuiConfigurationValues(
state.values().sourceFolder(),
state.values().targetFolder(),
state.values().sqliteFile(),
state.values().promptTemplateFile(),
state.values().runtimeLockFile(),
state.values().logDirectory(),
state.values().logLevel(),
state.values().maxRetriesTransient(),
state.values().maxPages(),
state.values().maxTextCharacters(),
"maybe",
"claude-42",
state.values().providerConfigurations());
GuiConfigurationEditorState dirty = state.withValues(editedValues);
assertTrue(dirty.isDirty());
assertEquals(state.baselineValues(), dirty.baselineValues());
assertEquals(editedValues, dirty.values());
assertEquals(GuiChangeState.DIRTY, dirty.changeState());
GuiConfigurationEditorState cleanAgain = dirty.markClean();
assertFalse(cleanAgain.isDirty());
assertEquals(state.baselineValues(), cleanAgain.baselineValues());
assertEquals(state.baselineValues(), cleanAgain.values());
assertEquals(GuiChangeState.CLEAN, cleanAgain.changeState());
}
@Test
void loadedFileSnapshotCanBeAttachedWithoutChangingDirtyState() {
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
GuiConfigurationFileSnapshot snapshot = new GuiConfigurationFileSnapshot(
java.nio.file.Path.of("config/application.properties"),
new java.util.Properties());
GuiConfigurationEditorState withSnapshot = state.withLoadedFileSnapshot(snapshot);
assertTrue(withSnapshot.hasLoadedFileSnapshot());
assertFalse(withSnapshot.isNewConfiguration());
assertEquals(snapshot, withSnapshot.loadedFileSnapshot().orElseThrow());
assertFalse(withSnapshot.isDirty());
}
@Test
void resettingTheCurrentValuesToTheBaselineMakesTheEditorCleanAgain() {
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
GuiConfigurationValues editedValues = new GuiConfigurationValues(
state.values().sourceFolder(),
state.values().targetFolder(),
state.values().sqliteFile(),
state.values().promptTemplateFile(),
state.values().runtimeLockFile(),
state.values().logDirectory(),
state.values().logLevel(),
state.values().maxRetriesTransient(),
state.values().maxPages(),
state.values().maxTextCharacters(),
"true",
"openai-compatible",
state.values().providerConfigurations());
GuiConfigurationEditorState reverted = state.withValues(editedValues).withValues(state.baselineValues());
assertFalse(reverted.isDirty());
assertEquals(state.baselineValues(), reverted.values());
}
}
@@ -0,0 +1,29 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Path;
import java.util.Properties;
import org.junit.jupiter.api.Test;
class GuiConfigurationFileSnapshotTest {
@Test
void propertiesAreDefensivelyCopiedOnRead() {
Properties properties = new Properties();
properties.setProperty("source.folder", "./source");
GuiConfigurationFileSnapshot snapshot = new GuiConfigurationFileSnapshot(Path.of("config/application.properties"), properties);
Properties firstRead = snapshot.properties();
firstRead.setProperty("source.folder", "./changed");
Properties secondRead = snapshot.properties();
assertNotSame(firstRead, secondRead);
assertEquals("./source", secondRead.getProperty("source.folder"));
assertTrue(snapshot.hasFilePath(Path.of("config/application.properties")));
}
}
@@ -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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
class GuiConfigurationTemplateFactoryTest {
@Test
void createStandardTemplate_providesCompleteCleanEditorState() {
GuiConfigurationEditorState state = GuiConfigurationTemplateFactory.createStandardTemplate();
assertFalse(state.isDirty());
assertFalse(state.hasLoadedFileSnapshot());
assertTrue(state.isNewConfiguration());
GuiConfigurationValues values = state.values();
assertEquals("./work/local/source", values.sourceFolder());
assertEquals("./work/local/target", values.targetFolder());
assertEquals("./work/local/pdf-umbenenner.db", values.sqliteFile());
assertEquals("./config/prompts/template.txt", values.promptTemplateFile());
assertEquals("./work/local/pdf-umbenenner.lock", values.runtimeLockFile());
assertEquals("./work/local/logs", values.logDirectory());
assertEquals("INFO", values.logLevel());
assertEquals("3", values.maxRetriesTransient());
assertEquals("10", values.maxPages());
assertEquals("5000", values.maxTextCharacters());
assertEquals("false", values.logAiSensitive());
assertEquals(AiProviderFamily.CLAUDE.getIdentifier(), values.activeProviderFamily());
List<AiProviderFamily> providerOrder = values.providerConfigurations().keySet().stream().toList();
assertEquals(List.of(AiProviderFamily.CLAUDE, AiProviderFamily.OPENAI_COMPATIBLE), providerOrder);
GuiProviderConfigurationState claude = values.providerConfiguration(AiProviderFamily.CLAUDE);
assertNotNull(claude);
assertEquals("https://api.anthropic.com", claude.baseUrl());
assertEquals("claude-3-5-sonnet-20241022", claude.model());
assertEquals("60", claude.timeoutSeconds());
assertEquals("", claude.apiKey().propertyValue());
assertEquals(GuiValueOrigin.UNKNOWN, claude.apiKey().effectiveValueOrigin());
GuiProviderConfigurationState openAi = values.providerConfiguration(AiProviderFamily.OPENAI_COMPATIBLE);
assertNotNull(openAi);
assertEquals("https://api.openai.com/v1", openAi.baseUrl());
assertEquals("gpt-4o-mini", openAi.model());
assertEquals("30", openAi.timeoutSeconds());
assertEquals("", openAi.apiKey().propertyValue());
assertEquals(GuiValueOrigin.UNKNOWN, openAi.apiKey().effectiveValueOrigin());
}
@Test
void providerConfigurationMap_isImmutableFromOutside() {
GuiConfigurationValues values = GuiConfigurationTemplateFactory.createStandardValues();
assertThrows(UnsupportedOperationException.class, () ->
values.providerConfigurations().put(AiProviderFamily.CLAUDE, GuiProviderConfigurationState.blank()));
}
@Test
void withActiveProviderFamily_reusesProviderBlocksAndChangesSelection() {
GuiConfigurationValues values = GuiConfigurationTemplateFactory.createStandardValues();
GuiConfigurationValues switched = values.withActiveProviderFamily(AiProviderFamily.OPENAI_COMPATIBLE.getIdentifier());
assertEquals(AiProviderFamily.OPENAI_COMPATIBLE.getIdentifier(), switched.activeProviderFamily());
assertEquals(values.providerConfigurations(), switched.providerConfigurations());
assertEquals(values.sourceFolder(), switched.sourceFolder());
}
}
@@ -0,0 +1,65 @@
package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
class GuiConfigurationValuesTest {
@Test
void rawGeneralValuesRemainUntouched() {
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",
"DEBUG",
"12",
"34",
"56",
"maybe",
"not-a-provider-family",
providerConfigurations);
assertEquals("maybe", values.logAiSensitive());
assertEquals("not-a-provider-family", values.activeProviderFamily());
assertEquals(GuiProviderConfigurationState.blank(), values.providerConfiguration(AiProviderFamily.CLAUDE));
}
@Test
void providerConfigurationMapIsDefensivelyCopied() {
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",
"DEBUG",
"12",
"34",
"56",
"true",
AiProviderFamily.CLAUDE.getIdentifier(),
providerConfigurations);
providerConfigurations.put(AiProviderFamily.OPENAI_COMPATIBLE, GuiProviderConfigurationState.blank());
assertNotSame(providerConfigurations, values.providerConfigurations());
assertEquals(1, values.providerConfigurations().size());
}
}