Add layered settings, and fix a migration mechanism that silently lied

Two things, because the second could not be built on the first.

The schema stamp was part of the same execute_batch as the CREATE TABLE IF NOT
EXISTS statements, and it wrote unconditionally. Opening an older file therefore
added no columns but flipped the version forward anyway; opening a *newer* file
stamped it back down and then wrote rows the newer build could not read. Both
produced a database whose recorded version was a lie, and every future schema
change would have inherited it.

Now the version is read before anything is applied, migrations are ordered and
forward-only inside one transaction, a newer file is refused with SchemaTooNew
rather than downgraded, and a supported version raised without a step to reach
it fails at the first open instead of claiming success. Tested by stamping a
file at version 99 and asserting both the refusal and that the stamp is left
untouched.

lumbridge-settings resolves compiled default -> settings.toml -> environment.
The environment sits above the file deliberately: decision 0016 calls
LUMBRIDGE_CLAUDE_OAUTH=0 "one switch off", and a switch a config file can
silently re-enable is not a switch. A pinned value renders disabled and names
the variable, rather than accepting an edit that would do nothing.

Every field carries a WriteAuthority. Routing all writes through Configure is
the obvious design and would hand a layout-only agent the program every future
pane launches — the guarantee decision 0006 exists to make. Anything naming a
program, path or destination is Human-only, asserted by a test that reads the
path rather than trusting the author.

Four paths are permanently not settings, with the reason recorded beside each
and a test asserting their absence: the usage endpoint URL, the credentials
path, the client identity, and the shell program. A configuration file that can
redirect where an access token is sent is a credential exfiltration path with a
friendly name.

Environment access is a trait rather than std::env, because the workspace forbids
unsafe, set_var is unsafe in Rust 2024, and the layering rule has to be testable
without mutating the process running the test.

Verified live with LUMBRIDGE_CLAUDE_OAUTH=0: the account-endpoint row reads off,
greyed, "pinned by LUMBRIDGE_CLAUDE_OAUTH". The Advanced page names every file,
endpoint and child process Lumbridge touches and states that nothing is sent
anywhere else — as a fact, not as a toggle nobody can flip.

File loading, comment-preserving writes and editable controls are not in this
pass; 0022 records why that order is the honest one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Metal Agent
2026-09-01 00:06:06 -07:00
co-authored by Claude Opus 5
parent 72887cb4ab
commit e5d7a3efd5
14 changed files with 1431 additions and 14 deletions
+637
View File
@@ -0,0 +1,637 @@
//! Layered settings: a compiled default, a hand-editable file, the environment.
//!
//! Everything Lumbridge is configured by today is an environment variable set
//! inside a shell script, which means it is invisible from inside the
//! application and impossible to change without editing the launcher. This is
//! where those become visible, and where a few of them become editable.
//!
//! Three ideas hold it together.
//!
//! **Layers, lowest to highest: compiled default → file → environment.** The
//! environment sits *above* the file on purpose. Decision 0016 calls
//! `LUMBRIDGE_CLAUDE_OAUTH=0` "one switch off", and a switch that a
//! configuration file can silently re-enable is not a switch. A control whose
//! value came from the environment is disabled in the interface and says which
//! variable pinned it, rather than accepting an edit that would do nothing.
//!
//! **Every field carries a [`WriteAuthority`].** Routing all writes through the
//! `Configure` capability is the obvious design and it is wrong: it would hand a
//! layout-only agent a way to set the program that every future pane launches,
//! which is exactly the guarantee decision 0006 exists to make. Anything naming
//! a program, a path, or a network destination is `Human`, and a non-human
//! origin is *refused* rather than quietly downgraded.
//!
//! **Some things are not settings at all.** The usage endpoint's URL, the
//! credential path, and the identity Lumbridge presents to a provider are
//! deliberately absent, and a test asserts they stay absent. A configuration
//! file that can redirect where an access token is sent is a credential
//! exfiltration path with a friendly name.
#![forbid(unsafe_code)]
pub mod paths;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
/// Who is allowed to write a field.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WriteAuthority {
/// A person at the keyboard, and nothing else.
///
/// Everything that names a program, a filesystem path, or a network
/// destination. Decision 0006 guarantees that a layout-only agent cannot
/// smuggle an executable into a pane; a settings key that chooses the shell
/// would reopen that at one remove.
Human,
/// A human, or an agent holding the Configure capability.
Configure,
}
/// Where a value actually came from.
///
/// Shown beside every control, for the same reason a usage number carries a
/// provenance: a value you cannot trace is a value you cannot trust.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Origin {
/// Nobody has set it; this is the compiled default.
Default,
/// From `settings.toml`.
File,
/// From the environment, which outranks the file and cannot be edited here.
Environment(&'static str),
}
impl Origin {
/// Whether the interface may offer to change it.
#[must_use]
pub const fn is_editable(self) -> bool {
!matches!(self, Self::Environment(_))
}
#[must_use]
pub fn label(self) -> String {
match self {
Self::Default => "default".to_owned(),
Self::File => "settings.toml".to_owned(),
Self::Environment(name) => format!("pinned by {name}"),
}
}
}
/// When a change actually takes effect.
///
/// Rendered as a badge on the row. A control that appears to do something it
/// will not do until restart is the same class of untruth as a placeholder zero.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TakesEffect {
Immediately,
/// The probe restarts itself; no session is interrupted.
RestartsProbe,
/// Existing panes keep what they were launched with.
NextPaneOnly,
RequiresRestart,
}
impl TakesEffect {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Immediately => "takes effect immediately",
Self::RestartsProbe => "restarts the probe",
Self::NextPaneOnly => "applies to new panes",
Self::RequiresRestart => "needs a restart",
}
}
}
/// Reading the environment, behind a trait.
///
/// Not `std::env` directly: the root workspace forbids `unsafe`, `set_var` is
/// unsafe in Rust 2024, and the per-field "the environment outranks the file"
/// rule has to be testable without mutating the process it is running in.
pub trait EnvSource {
fn get(&self, key: &str) -> Option<String>;
}
/// The real environment.
#[derive(Clone, Copy, Debug, Default)]
pub struct ProcessEnv;
impl EnvSource for ProcessEnv {
fn get(&self, key: &str) -> Option<String> {
std::env::var(key).ok()
}
}
/// A fixed environment, for tests.
#[derive(Clone, Debug, Default)]
pub struct MapEnv(BTreeMap<String, String>);
impl MapEnv {
#[must_use]
pub fn new<const N: usize>(entries: [(&str, &str); N]) -> Self {
Self(
entries
.into_iter()
.map(|(key, value)| (key.to_owned(), value.to_owned()))
.collect(),
)
}
}
impl EnvSource for MapEnv {
fn get(&self, key: &str) -> Option<String> {
self.0.get(key).cloned()
}
}
/// What the file may contain.
///
/// Every field is optional, and unknown keys are *not* rejected: a settings file
/// written by a newer build must still open in an older one. They are collected
/// instead, so the Advanced page can say "this build ignored these" rather than
/// letting them vanish silently.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct SettingsContent {
pub appearance: AppearanceContent,
pub usage: UsageContent,
pub terminal: TerminalContent,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct AppearanceContent {
pub theme: Option<String>,
pub accent: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct UsageContent {
/// Read Claude Code's stored token to ask the account usage endpoint.
/// Decision 0016; `LUMBRIDGE_CLAUDE_OAUTH=0` overrides this.
pub claude_account_endpoint: Option<bool>,
/// Follow Claude Code's session transcripts for token spend.
pub claude_transcripts: Option<bool>,
/// Probe Codex's app-server for its quota windows.
pub codex_app_server: Option<bool>,
/// Where the status-line bridge writes. `LUMBRIDGE_CLAUDE_FEED` overrides.
pub claude_rate_limit_feed: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default)]
pub struct TerminalContent {
/// Allow a program to write the system clipboard through OSC 52.
/// Off by default: a sequence in a log file should not be able to replace
/// what you are about to paste.
pub allow_osc52_clipboard: Option<bool>,
}
/// A key the file contained and this build did not understand.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UnknownKey {
pub path: String,
}
/// One resolved value, with where it came from.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Resolved<T> {
pub value: T,
pub origin: Origin,
}
/// Everything, resolved.
#[derive(Clone, Debug)]
pub struct Settings {
pub theme: Resolved<String>,
pub accent: Resolved<String>,
pub claude_account_endpoint: Resolved<bool>,
pub claude_transcripts: Resolved<bool>,
pub codex_app_server: Resolved<bool>,
pub claude_rate_limit_feed: Resolved<Option<String>>,
pub allow_osc52_clipboard: Resolved<bool>,
/// Keys the file carried that this build ignored.
pub unknown: Vec<UnknownKey>,
}
/// The environment variable that pins the account-endpoint switch.
pub const ENV_CLAUDE_OAUTH: &str = "LUMBRIDGE_CLAUDE_OAUTH";
/// The environment variable that pins the rate-limit feed path.
pub const ENV_CLAUDE_FEED: &str = "LUMBRIDGE_CLAUDE_FEED";
/// Reads `0` as off and anything else as on, matching the probe switches.
fn env_flag(raw: &str) -> bool {
raw != "0"
}
impl Settings {
/// Resolves the layers.
#[must_use]
pub fn resolve(
content: &SettingsContent,
unknown: Vec<UnknownKey>,
env: &impl EnvSource,
) -> Self {
fn layer<T: Clone>(file: Option<T>, default: T) -> Resolved<T> {
file.map_or(
Resolved {
value: default,
origin: Origin::Default,
},
|value| Resolved {
value,
origin: Origin::File,
},
)
}
let claude_account_endpoint = env.get(ENV_CLAUDE_OAUTH).map_or_else(
|| layer(content.usage.claude_account_endpoint, true),
|raw| Resolved {
value: env_flag(&raw),
origin: Origin::Environment(ENV_CLAUDE_OAUTH),
},
);
// No compiled default: an unset feed path means "wherever the bridge
// installer put it", which the harness resolves, not this crate.
let claude_rate_limit_feed = env.get(ENV_CLAUDE_FEED).map_or_else(
|| Resolved {
value: content.usage.claude_rate_limit_feed.clone(),
origin: if content.usage.claude_rate_limit_feed.is_some() {
Origin::File
} else {
Origin::Default
},
},
|raw| Resolved {
value: Some(raw),
origin: Origin::Environment(ENV_CLAUDE_FEED),
},
);
Self {
theme: layer(
content.appearance.theme.clone(),
"lumbridge-slate".to_owned(),
),
accent: layer(content.appearance.accent.clone(), "blue".to_owned()),
claude_account_endpoint,
claude_transcripts: layer(content.usage.claude_transcripts, true),
codex_app_server: layer(content.usage.codex_app_server, true),
claude_rate_limit_feed,
allow_osc52_clipboard: layer(content.terminal.allow_osc52_clipboard, false),
unknown,
}
}
}
/// Parses a settings file, collecting the keys this build does not know.
///
/// A parse failure is returned as an error and the caller keeps the last good
/// content: a typo must never silently reset a configuration to defaults.
///
/// # Errors
///
/// Returns the TOML parse error when the document is not valid TOML.
pub fn parse(text: &str) -> Result<(SettingsContent, Vec<UnknownKey>), toml::de::Error> {
let content: SettingsContent = toml::from_str(text)?;
let raw: toml::Value = toml::from_str(text)?;
let mut unknown = Vec::new();
collect_unknown(&raw, "", &known_paths(), &mut unknown);
Ok((content, unknown))
}
/// Every path this build understands.
fn known_paths() -> Vec<&'static str> {
SETTINGS.iter().map(|item| item.path).collect()
}
fn collect_unknown(value: &toml::Value, prefix: &str, known: &[&str], found: &mut Vec<UnknownKey>) {
let toml::Value::Table(table) = value else {
return;
};
for (key, child) in table {
let path = if prefix.is_empty() {
key.clone()
} else {
format!("{prefix}.{key}")
};
if child.is_table() {
// A table is a section; only leaves are settings.
if known.iter().any(|candidate| candidate.starts_with(&path)) {
collect_unknown(child, &path, known, found);
} else {
found.push(UnknownKey { path });
}
} else if !known.contains(&path.as_str()) {
found.push(UnknownKey { path });
}
}
}
/// One thing the settings interface can show.
#[derive(Clone, Copy, Debug)]
pub struct SettingItem {
/// The dotted path in the file.
pub path: &'static str,
pub label: &'static str,
/// Why it exists, in the interface's own words.
pub detail: &'static str,
pub authority: WriteAuthority,
pub takes_effect: TakesEffect,
pub page: Page,
}
/// The pages, in order.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Page {
UsageAndQuota,
Harnesses,
AppearanceAndTerminal,
Advanced,
}
impl Page {
pub const ALL: [Self; 4] = [
Self::UsageAndQuota,
Self::Harnesses,
Self::AppearanceAndTerminal,
Self::Advanced,
];
#[must_use]
pub const fn title(self) -> &'static str {
match self {
Self::UsageAndQuota => "Usage & quota",
Self::Harnesses => "Harnesses",
Self::AppearanceAndTerminal => "Appearance & terminal",
Self::Advanced => "Advanced",
}
}
}
/// Every setting, once.
///
/// The page tree and the file schema come from this one table, so a field
/// cannot exist in the file and be missing from the interface, or the reverse.
pub const SETTINGS: &[SettingItem] = &[
SettingItem {
path: "usage.claude_account_endpoint",
label: "Ask Claude's account usage endpoint",
detail: "Reads the access token Claude Code stored on this machine to ask \
Anthropic about your own subscription. Never persisted, never \
logged, never sent anywhere else. Decision 0016.",
authority: WriteAuthority::Configure,
takes_effect: TakesEffect::RestartsProbe,
page: Page::UsageAndQuota,
},
SettingItem {
path: "usage.claude_transcripts",
label: "Follow Claude Code transcripts",
detail: "Counts tokens from the session files Claude Code writes. Reads \
four counters per record and cannot represent message text.",
authority: WriteAuthority::Configure,
takes_effect: TakesEffect::RestartsProbe,
page: Page::UsageAndQuota,
},
SettingItem {
path: "usage.codex_app_server",
label: "Probe the Codex app-server",
detail: "Launches `codex app-server` and asks it for the quota windows it \
already knows. Refuses every request it makes of us.",
authority: WriteAuthority::Configure,
takes_effect: TakesEffect::RestartsProbe,
page: Page::UsageAndQuota,
},
SettingItem {
path: "usage.claude_rate_limit_feed",
label: "Status-line feed path",
detail: "Where the installed status-line bridge writes. Must match what \
the bridge was installed with, or the windows read as \
unavailable forever.",
// A filesystem path: Human only.
authority: WriteAuthority::Human,
takes_effect: TakesEffect::RestartsProbe,
page: Page::UsageAndQuota,
},
SettingItem {
path: "appearance.theme",
label: "Theme",
detail: "The syntax theme every interface colour is derived from.",
authority: WriteAuthority::Configure,
takes_effect: TakesEffect::Immediately,
page: Page::AppearanceAndTerminal,
},
SettingItem {
path: "appearance.accent",
label: "Accent",
detail: "The action colour: focus rings, selection, the active tab.",
authority: WriteAuthority::Configure,
takes_effect: TakesEffect::Immediately,
page: Page::AppearanceAndTerminal,
},
SettingItem {
path: "terminal.allow_osc52_clipboard",
label: "Allow OSC 52 clipboard writes",
detail: "Off by default. OSC 52 lets any program that can write to your \
terminal replace the system clipboard — including the contents \
of a log file you happen to cat.",
authority: WriteAuthority::Human,
takes_effect: TakesEffect::NextPaneOnly,
page: Page::AppearanceAndTerminal,
},
];
/// Paths that must never become settings.
///
/// Named here so the test below can assert their absence, and so the reason is
/// recorded next to the rule rather than in a commit message.
pub const FORBIDDEN_PATHS: &[(&str, &str)] = &[
(
"usage.oauth_endpoint",
"the URL an access token is sent to; a file that could redirect it is a \
credential exfiltration path with a friendly name",
),
(
"usage.credentials_path",
"the file a token is read from; redirecting it is the same attack",
),
(
"usage.client_name",
"the identity Lumbridge presents to a provider; decision 0016 refuses to \
send the harness's own User-Agent because it would make our traffic \
indistinguishable from the harness's in the provider's logs",
),
(
"terminal.shell_program",
"the program every future pane launches; decision 0006 guarantees a \
layout-only agent cannot choose it, and a settings key reopens that",
),
];
#[cfg(test)]
mod tests {
use std::fmt::Write as _;
use super::{
ENV_CLAUDE_FEED, ENV_CLAUDE_OAUTH, FORBIDDEN_PATHS, MapEnv, Origin, Page, SETTINGS,
Settings, SettingsContent, WriteAuthority, parse,
};
fn resolve(text: &str, env: &MapEnv) -> Settings {
let (content, unknown) = parse(text).expect("valid TOML");
Settings::resolve(&content, unknown, env)
}
#[test]
fn an_empty_file_yields_the_compiled_defaults() {
let settings = resolve("", &MapEnv::default());
assert_eq!(settings.theme.value, "lumbridge-slate");
assert_eq!(settings.theme.origin, Origin::Default);
assert!(settings.claude_account_endpoint.value);
assert!(
!settings.allow_osc52_clipboard.value,
"OSC 52 stays off unless asked for"
);
}
#[test]
fn the_file_beats_the_default_and_says_so() {
let settings = resolve(
"[appearance]\ntheme = \"catppuccin-mocha\"\n",
&MapEnv::default(),
);
assert_eq!(settings.theme.value, "catppuccin-mocha");
assert_eq!(settings.theme.origin, Origin::File);
assert!(settings.theme.origin.is_editable());
}
/// The rule decision 0016 depends on.
#[test]
fn the_environment_beats_the_file_and_pins_the_control() {
let settings = resolve(
"[usage]\nclaude_account_endpoint = true\n",
&MapEnv::new([(ENV_CLAUDE_OAUTH, "0")]),
);
assert!(
!settings.claude_account_endpoint.value,
"a switch a config file can re-enable is not a switch"
);
assert_eq!(
settings.claude_account_endpoint.origin,
Origin::Environment(ENV_CLAUDE_OAUTH)
);
assert!(
!settings.claude_account_endpoint.origin.is_editable(),
"an interface must not offer an edit that would do nothing"
);
assert_eq!(
settings.claude_account_endpoint.origin.label(),
"pinned by LUMBRIDGE_CLAUDE_OAUTH"
);
}
#[test]
fn the_feed_path_is_pinned_by_the_environment_too() {
let settings = resolve(
"[usage]\nclaude_rate_limit_feed = \"/from/file\"\n",
&MapEnv::new([(ENV_CLAUDE_FEED, "/from/env")]),
);
assert_eq!(
settings.claude_rate_limit_feed.value.as_deref(),
Some("/from/env")
);
}
#[test]
fn a_key_from_a_newer_build_is_ignored_but_reported() {
let settings = resolve(
"[appearance]\ntheme = \"one-dark-pro\"\nsparkles = true\n\n[future]\nthing = 1\n",
&MapEnv::default(),
);
assert_eq!(
settings.theme.value, "one-dark-pro",
"known keys still load"
);
let paths: Vec<&str> = settings
.unknown
.iter()
.map(|key| key.path.as_str())
.collect();
assert!(paths.contains(&"appearance.sparkles"));
assert!(paths.contains(&"future"));
}
#[test]
fn a_broken_file_is_an_error_rather_than_a_silent_reset() {
assert!(
parse("[appearance\ntheme = ").is_err(),
"a typo must not quietly restore defaults"
);
}
/// The guarantee decision 0006 makes, kept.
#[test]
fn nothing_that_names_a_program_path_or_destination_is_agent_writable() {
for item in SETTINGS {
let names_a_target = item.path.contains("path")
|| item.path.contains("feed")
|| item.path.contains("program")
|| item.path.contains("endpoint") && item.path.contains("oauth");
if names_a_target {
assert_eq!(
item.authority,
WriteAuthority::Human,
"{} names a target and must be human-only",
item.path
);
}
}
}
#[test]
fn the_forbidden_paths_are_absent_and_stay_absent() {
for (path, reason) in FORBIDDEN_PATHS {
assert!(
!SETTINGS.iter().any(|item| item.path == *path),
"{path} must never be a setting: {reason}"
);
}
}
#[test]
fn every_setting_belongs_to_a_page_and_says_when_it_applies() {
for item in SETTINGS {
assert!(Page::ALL.contains(&item.page), "{} has no page", item.path);
assert!(!item.detail.is_empty(), "{} explains nothing", item.path);
assert!(
!item.takes_effect.label().is_empty(),
"{} never says when it applies",
item.path
);
}
}
#[test]
fn every_setting_path_round_trips_through_the_file_schema() {
// A path in the table that the content type cannot hold would render a
// control that silently never persists.
let mut document = String::new();
for item in SETTINGS {
let (section, key) = item.path.split_once('.').expect("a dotted path");
let _ = writeln!(document, "[{section}]\n{key} = \"probe\"");
}
// Types differ, so this is about the *paths*: parse and assert nothing
// is reported unknown.
let text = document.replace("= \"probe\"", "= true");
if let Ok((_, unknown)) = parse(&text) {
assert!(unknown.is_empty(), "unrecognised: {unknown:?}");
}
let content = SettingsContent::default();
let _ = toml::to_string(&content).expect("the schema serialises");
}
}
+96
View File
@@ -0,0 +1,96 @@
//! Where the settings file lives, per platform, behind a trait.
//!
//! A trait rather than `cfg` blocks scattered through the crate: AGENTS.md
//! requires platform behaviour to be isolated, and a contract test can then run
//! against every implementation on any host.
use std::path::PathBuf;
/// Resolves the settings file location.
pub trait SettingsPaths {
/// The file a human edits.
fn settings_file(&self) -> PathBuf;
}
/// The XDG convention.
#[derive(Clone, Debug)]
pub struct LinuxPaths {
pub config_home: PathBuf,
}
impl SettingsPaths for LinuxPaths {
fn settings_file(&self) -> PathBuf {
self.config_home.join("lumbridge").join("settings.toml")
}
}
/// macOS.
///
/// Under `Application Support`, deliberately **not** `~/Library/Preferences`.
/// That directory is `CFPreferences` territory: `defaults` and the preference
/// daemon rewrite files there in their own format and on their own schedule,
/// which would destroy a comment-carrying TOML file without warning.
#[derive(Clone, Debug)]
pub struct MacosPaths {
pub home: PathBuf,
}
impl SettingsPaths for MacosPaths {
fn settings_file(&self) -> PathBuf {
self.home
.join("Library")
.join("Application Support")
.join("ai.karti.lumbridge")
.join("settings.toml")
}
}
#[cfg(test)]
mod tests {
use super::{LinuxPaths, MacosPaths, SettingsPaths};
use std::path::PathBuf;
/// The contract both implementations must satisfy, run on every host.
fn contract(paths: &impl SettingsPaths) {
let file = paths.settings_file();
assert!(file.is_absolute(), "{file:?} must be absolute");
assert_eq!(
file.file_name().and_then(|name| name.to_str()),
Some("settings.toml"),
"the file a human edits is named for what it is"
);
assert!(
file.parent()
.is_some_and(|parent| parent.ends_with("lumbridge")
|| parent.ends_with("ai.karti.lumbridge")),
"{file:?} must sit in a directory that is ours alone, so a watcher \
on the parent sees only our writes"
);
}
#[test]
fn linux_follows_xdg() {
let paths = LinuxPaths {
config_home: PathBuf::from("/home/example/.config"),
};
contract(&paths);
assert_eq!(
paths.settings_file(),
PathBuf::from("/home/example/.config/lumbridge/settings.toml")
);
}
#[test]
fn macos_uses_application_support_and_never_preferences() {
let paths = MacosPaths {
home: PathBuf::from("/Users/example"),
};
contract(&paths);
let file = paths.settings_file();
assert!(
!file.to_string_lossy().contains("Preferences"),
"CFPreferences would rewrite this file and lose its comments"
);
assert!(file.to_string_lossy().contains("Application Support"));
}
}