Skip to content

refactor: make ThemeService's settings path redirectable for tests - #1928

Merged
laurentiu021 merged 1 commit into
mainfrom
refactor/themeservice-testable-path
Aug 18, 2026
Merged

refactor: make ThemeService's settings path redirectable for tests#1928
laurentiu021 merged 1 commit into
mainfrom
refactor/themeservice-testable-path

Conversation

@laurentiu021

Copy link
Copy Markdown
Owner

Why

ThemeService held its settings path in a static readonly SettingsPath built from
Environment.SpecialFolder.ApplicationData. That known-folder API ignores the APPDATA environment
variable
(verified in #1741), so no test could point the service at a temp directory — any test that
constructed it and saved would overwrite the developer's real theme.json. It is the same
untestable-static-path class that already caused data loss in SpeedTestHistoryService (#1734), and
the last instance-backed offender on the
ArchitectureTests.Services_DoNotHoldUserDataPathsInStaticFields ratchet.

What

  • The path moves to an instance field set from the shared string? configDir = null constructor seam
    the other persistence services already use. Production is unchanged: it reaches the service through
    the Instance singleton, which passes null → the real %AppData%\SysManager\theme.json (ROAMING, as
    before). The ctor is internal because the app has exactly one theme service.
  • ThemeService.SettingsPath comes off the ratchet's known-list, leaving only LogService — a
    static partial class by Serilog-sink design, which needs a genuine design decision (drop the
    static-sink model), not a mechanical edit, and which no test constructs (lowest risk).
  • One enabling change: Apply() now no-ops when Application.Current is null instead of throwing.
    It is the service's single WPF touch-point, reached from every public entry, so guarding it is what
    lets the persistence path run in a headless unit test. Harmless in production, where
    Application.Current is always set; a no-op is the correct behaviour when there is no resource
    dictionary to repaint.

Verification

New ThemeServiceTests, headless:

  • SetPreset writes to the configured directory and leaves the real profile byte-for-byte untouched
    (the actual data-loss guard);
  • a second instance pointed at the same directory reads the choice back;
  • an empty directory keeps the default and creates no file;
  • a shade value survives a reload.

Red proof: 3 mutations, both touched source files restored byte-for-byte.

Mutation Must go red
ignore configDir, hard-code the real %AppData% path (the shipped defect) SetPreset_WritesToTheConfiguredDirectory_NotTheRealProfile
re-introduce a static user-data path anywhere in Services the ratchet
remove the Apply null-guard (throws again headless) all the headless theme tests

The read-back and shade tests deliberately do not discriminate the path mutation — they round-trip
over whatever single path both instances share — which is why the "…NotTheRealProfile" test is the
load-bearing one for #1741.

Behaviour in the running app is identical, so this is refactor: — all four projects build 0/0,
dotnet format --verify-no-changes exit 0 on both, author headers present, leak scan over all 32 terms
gives 0 hits. No version bump, no release.

…1741)

ThemeService held its settings path in a `static readonly SettingsPath` built
from Environment.SpecialFolder.ApplicationData. That API resolves through the
Win32 known-folder function and ignores the APPDATA environment variable, so no
test could point it at a temp directory — any test that constructed the service
and saved would overwrite the developer's real theme.json. It is the same
untestable-static-path class that already caused data loss in
SpeedTestHistoryService (#1734) and the last instance-backed offender on the
ArchitectureTests.Services_DoNotHoldUserDataPathsInStaticFields ratchet.

The path moves to an instance field set from the shared `string? configDir = null`
constructor seam the other persistence services already use — production still
reaches it through the Instance singleton (null -> the real %AppData% path,
ROAMING, unchanged), the ctor is internal because the app has exactly one theme
service. ThemeService.SettingsPath comes off the ratchet's known-list, leaving
only LogService (a static partial class by Serilog-sink design, which needs a
real design decision, not a mechanical edit).

One enabling change: Apply() now no-ops when Application.Current is null instead of
NPEing. It is the service's single WPF touch-point, reached from every public
entry, so guarding it is what lets the persistence path run in a headless unit
test — and it is harmless in production, where Application.Current is always set.

New ThemeServiceTests exercise the seam headlessly: SetPreset writes to the
configured directory and leaves the real profile untouched; a second instance
reads the choice back; an empty directory keeps the default; a shade survives a
reload. Red proof, 3 mutations: hard-coding the real path reddens the "not the
real profile" test; re-adding a static user-data path anywhere in Services reddens
the ratchet; removing the Apply guard reddens all the headless tests. Behaviour in
the running app is unchanged — no version bump, no release.
@laurentiu021
laurentiu021 merged commit 3074bb1 into main Aug 18, 2026
5 checks passed
@laurentiu021
laurentiu021 deleted the refactor/themeservice-testable-path branch August 18, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant