ADR: Preferences Architecture and Testable Store Injection

Status

Proposed (Date: 2026-07-12)

Context

org.omegat.util.Preferences is a static facade over one mutable store, built lazily once per JVM behind a didInit guard. That is correct for a launch, but in tests it means the first test to touch preferences fixes the store for every test that follows, so mutated values leak and cause order-dependent failures. The concrete symptom was EditorControllerTest asserting offsets that depend on MARK_PARA_DELIMITATIONS, which the console tests leave as false (fragile assertions from #1326). There was no ADR or specification for the mechanism, and the first fix was an ad hoc resetPreferencesForTest() back door into private state.

Decision

Give Preferences one explicit installation point for its store and let tests inject an isolated store, mirroring org.omegat.core.data.CoreState (ADR 2025002) and RuntimePreferenceStore.

Specification: static accessors delegate to an installed IPreferences. PreferencesImpl holds values in memory over an IPrefsPersistence backing (PreferencesXML on disk in production). init() builds the production store against the config dir and is idempotent via didInit.

Implementation:

  • @VisibleForTesting static synchronized setPreferences(IPreferences) becomes the sole assignment point; init() routes through it. Installing sets didInit, so a later init() keeps the injected store.

  • TestPreferences is an empty, never-persisted in-memory store; TestPreferencesInitializer installs a fresh one per test.

  • The resetPreferencesForTest() back door is removed.

Production behavior is unchanged.

Consequences

Tests become isolated and order-independent, and the full suite passes. Preferences follow the same testable-singleton seam as CoreState. Tests that need on-disk persistence still construct PreferencesImpl/PreferencesXML directly. Preferences stays a static facade; full dependency injection is out of scope.

Alternatives Considered

  • Pin the one preference in the failing test — patches one case, leaves the leak.

  • resetPreferencesForTest() back door — fixes the leak but is a bespoke private-state mutator, off-pattern.

  • Full dependency injection — cleanest long-term, but a large cross-cutting change.

References