diff --git a/.gitignore b/.gitignore index 6d092af..04eab71 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,4 @@ replay_pid* /review-input.zip /run-milestone.ps1 /run-v11.ps1 +.m2repo diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiChangeState.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiChangeState.java new file mode 100644 index 0000000..d1e2529 --- /dev/null +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiChangeState.java @@ -0,0 +1,25 @@ +package de.gecheckt.pdf.umbenenner.adapter.in.gui.editor; + +/** + * Derived change-state view for the editor content. + *
+ * 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; + } +} diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationEditorState.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationEditorState.java new file mode 100644 index 0000000..72d0657 --- /dev/null +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationEditorState.java @@ -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. + *
+ * 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
+ * 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;
+ }
+}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationTemplateFactory.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationTemplateFactory.java
new file mode 100644
index 0000000..8a711d4
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationTemplateFactory.java
@@ -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.
+ *
+ * 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
+ * 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
+ * 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;
+ }
+}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiProviderConfigurationState.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiProviderConfigurationState.java
new file mode 100644
index 0000000..39ef352
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiProviderConfigurationState.java
@@ -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.
+ *
+ * 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());
+ }
+}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiValueOrigin.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiValueOrigin.java
new file mode 100644
index 0000000..ee170d6
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiValueOrigin.java
@@ -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.
+ *
+ * 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
+}
diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/package-info.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/package-info.java
new file mode 100644
index 0000000..00337bf
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/package-info.java
@@ -0,0 +1,12 @@
+/**
+ * Editor state and template model for the JavaFX configuration editor.
+ *
+ * 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.
+ *
+ * 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;
diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationEditorStateTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationEditorStateTest.java
new file mode 100644
index 0000000..a673a60
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationEditorStateTest.java
@@ -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());
+ }
+}
diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationFileSnapshotTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationFileSnapshotTest.java
new file mode 100644
index 0000000..8b7c256
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationFileSnapshotTest.java
@@ -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")));
+ }
+}
diff --git a/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationTemplateFactoryTest.java b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationTemplateFactoryTest.java
new file mode 100644
index 0000000..071225c
--- /dev/null
+++ b/pdf-umbenenner-adapter-in-gui/src/test/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/editor/GuiConfigurationTemplateFactoryTest.java
@@ -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