use super::*;
#[test]
fn settings_deserialize_ignores_schema_metadata() {
let settings: Settings = serde_json::from_str(
r#"{"$schema":"./state/settings.schema.json","agent":{"model":{"provider":"openai-codex"}}}"#,
)
.unwrap();
assert_eq!(
settings.selected_model.provider.as_deref(),
Some("openai-codex")
);
}
#[test]
fn settings_default_serialization_omits_empty_groups() {
let value = serde_json::to_value(Settings::default()).unwrap();
assert_eq!(value, json!({"schema_version": 2}));
}
#[test]
fn hook_settings_preserve_unknown_fields_on_settings_update() {
let temp = tempfile::TempDir::new().unwrap();
let paths = McPaths::from_root(temp.path().join("mc"));
fs::create_dir_all(&paths.root).unwrap();
fs::write(
&paths.settings_file,
r#"{"automation":{"hooks":{"future":{"keep":true}}}}"#,
)
.unwrap();
set_selected_model(&paths, "provider", "model").unwrap();
let value = read_settings_value(&paths);
assert_eq!(value["automation"]["hooks"]["future"]["keep"], true);
assert_eq!(value["agent"]["model"]["provider"], "provider");
assert_eq!(value["agent"]["model"]["model"], "model");
}
#[test]
fn hook_settings_default_without_unknown_fields_is_omitted_on_settings_update() {
let temp = tempfile::TempDir::new().unwrap();
let paths = McPaths::from_root(temp.path().join("mc"));
fs::create_dir_all(&paths.root).unwrap();
fs::write(&paths.settings_file, r#"{}"#).unwrap();
set_selected_model(&paths, "provider", "model").unwrap();
let value = read_settings_value(&paths);
assert!(value.get("hooks").is_none());
}
#[test]
fn hook_settings_known_fields_override_old_raw_values_on_settings_update() {
let temp = tempfile::TempDir::new().unwrap();
let paths = McPaths::from_root(temp.path().join("mc"));
fs::create_dir_all(&paths.root).unwrap();
fs::write(
&paths.settings_file,
r#"{"automation":{"hooks":{"enabled":false,"future":{"keep":true}}}}"#,
)
.unwrap();
update_settings_preserving_unknown_top_level_fields(&paths, |settings| {
settings.hooks.enabled = true;
})
.unwrap();
let value = read_settings_value(&paths);
assert_eq!(value["automation"]["hooks"]["enabled"], true);
assert_eq!(value["automation"]["hooks"]["future"]["keep"], true);
}
#[test]
fn settings_update_preserves_explicit_default_groups_and_null_values() {
let mut raw = json!({
"schema_version": 2,
"agent": {
"model": {"provider": "provider"},
"fast": {"enabled": false},
"context": null,
"subagents": {}
},
"providers": {"custom": {}},
"capabilities": {"tools": {"read": {}}, "mcp_approvals": {}},
"interface": {
"appearance": {"theme": "matrix-green", "future": null},
"no_color": null
},
"future": null
});
let mut settings: Settings = serde_json::from_value(raw.clone()).unwrap();
let before = settings.clone();
settings.selected_model.model = Some("model".to_string());
update_raw_from_settings(&mut raw, &before, &settings, SettingsScope::Global).unwrap();
assert_eq!(raw["agent"]["model"]["model"], "model");
assert_eq!(raw["agent"]["fast"], json!({"enabled": false}));
assert_eq!(raw["agent"]["context"], serde_json::Value::Null);
assert_eq!(raw["agent"]["subagents"], json!({}));
assert_eq!(raw["providers"]["custom"], json!({}));
assert_eq!(raw["capabilities"]["tools"]["read"], json!({}));
assert_eq!(raw["capabilities"]["mcp_approvals"], json!({}));
assert_eq!(
raw["interface"]["appearance"]["future"],
serde_json::Value::Null
);
assert_eq!(raw["interface"]["no_color"], serde_json::Value::Null);
assert_eq!(raw["future"], serde_json::Value::Null);
}
#[test]
fn settings_update_removes_last_entries_from_dynamic_maps() {
let mut raw = json!({
"schema_version": 2,
"agent": {"context": {
"enabled": true,
"max_tokens": 128000,
"reserve_tokens": 16384,
"keep_recent_tokens": 20000,
"model_overrides": {
"provider/model": {"max_tokens": 1000, "future": null}
}
}},
"providers": {"custom": {
"old": {
"label": "Old",
"base_url": "http://127.0.0.1:8080/v1",
"future": {"keep": true}
}
}},
"capabilities": {
"mcp_approvals": {
"/config/.mcp.json": {"old": true}
},
"lsp": {
"enabled": true,
"servers": {"rust": {"command": "rust-analyzer", "future": {"keep": true}}}
}
},
"future_root": {"keep": true}
});
let mut settings: Settings = serde_json::from_value(raw.clone()).unwrap();
let before = settings.clone();
settings.custom_providers.clear();
settings.mcp_approvals.clear();
settings.lsp.servers.clear();
settings.context.as_mut().unwrap().model_overrides.clear();
update_raw_from_settings(&mut raw, &before, &settings, SettingsScope::Global).unwrap();
assert!(raw.pointer("/providers/custom").is_none());
assert!(raw.pointer("/capabilities/mcp_approvals").is_none());
assert!(raw.pointer("/capabilities/lsp/servers").is_none());
assert!(raw.pointer("/agent/context/model_overrides").is_none());
assert_eq!(raw["future_root"]["keep"], true);
}
#[test]
fn shared_wire_preserves_null_empty_and_explicit_false_projections() {
let cases = [
(json!({}), json!({"schema_version": 2})),
(
json!({
"agent": {"primary_agent": null, "context": null, "subagents": {"disabled": []}},
"providers": {"anthropic": {"cache_ttl": null}, "custom": {}, "catalog": {"disabled": []}},
"capabilities": {"tools": {"disabled": []}, "mcp": {}},
"knowledge": {"skills": {"disabled": []}},
"automation": {}, "sessions": {"retention_days": 7},
"interface": {"no_color": null}
}),
json!({"schema_version": 2}),
),
(
json!({"interface": {"no_color": false, "tui": {"autocomplete": {"respects_gitignore": false}}}}),
json!({"schema_version": 2, "interface": {"no_color": false, "tui": {"autocomplete": {"respects_gitignore": false}}}}),
),
(
json!({"interface": {"tui": {"autocomplete": {"respects_gitignore": true}}}}),
json!({"schema_version": 2, "interface": {"tui": {}}}),
),
(
json!({"agent": {"subagents": {"execution": {"max_depth": 3}, "future": 42}},
"interface": {"tui": {"future": true, "autocomplete": {"respects_gitignore": true, "future": 42}}}}),
json!({"schema_version": 2, "agent": {"subagents": {"execution": {"absolute_paths": true, "max_depth": 3}, "future": 42}},
"interface": {"tui": {"future": true, "autocomplete": {"future": 42}}}}),
),
];
for (input, expected) in cases {
let settings: Settings = serde_json::from_value(input.clone()).unwrap();
assert_eq!(
serde_json::to_value(settings).unwrap(),
expected,
"input: {input}"
);
}
}
#[test]
fn shared_wire_rejects_non_object_groups_and_non_optional_nulls() {
for path in [
vec!["agent"],
vec!["providers"],
vec!["capabilities"],
vec!["knowledge"],
vec!["automation"],
vec!["sessions"],
vec!["interface"],
vec!["providers", "anthropic"],
vec!["interface", "tui"],
vec!["interface", "tui", "autocomplete"],
] {
for value in [json!([]), json!(null), json!(false)] {
let mut raw = json!({"schema_version": 2});
super::super::json::set_path(&mut raw, &path, value).unwrap();
assert!(
serde_json::from_value::<Settings>(raw.clone()).is_err(),
"accepted {raw}"
);
}
}
for raw in [
json!({"agent": {"fast": null}}),
json!({"agent": {"subagents": {"execution": null}}}),
json!({"knowledge": {"skills": null}}),
json!({"interface": {"tui": {"autocomplete": {"respects_gitignore": null}}}}),
] {
assert!(
serde_json::from_value::<Settings>(raw.clone()).is_err(),
"accepted {raw}"
);
}
}
#[test]
fn settings_reject_unsupported_or_malformed_schema_versions() {
for version in [
json!(1),
json!(0),
json!(3),
json!("2"),
json!(null),
json!(2.5),
] {
let error =
serde_json::from_value::<Settings>(json!({"schema_version": version})).unwrap_err();
assert!(error.to_string().contains("schema_version"), "{error}");
}
}
#[test]
fn settings_ignore_old_paths_instead_of_normalizing_them() {
let settings: Settings = serde_json::from_value(json!({
"schema_version": 2,
"agent": {"model": {"provider": "openai-codex"}},
"selected_model": {"model": "old-model"},
"disabled_skills": ["old-skill"],
"tools": {"ffgrep": {"absolute_paths": false}},
"capabilities": {"tools": {
"ffgrep": {"absolute_paths": false},
"fffind": {"absolute_paths": false},
"parallel_subagents": {"max_depth": 3}
}}
}))
.unwrap();
assert_eq!(
settings.selected_model.provider.as_deref(),
Some("openai-codex")
);
assert_eq!(settings.selected_model.model, None);
assert!(settings.skills.disabled.is_empty());
assert_eq!(settings.tools, ToolSettings::default());
}
#[test]
fn experimental_codex_reasoning_updates_require_explicit_opt_in() {
assert!(
!Settings::default()
.openai_codex
.experimental_reasoning_updates
);
let settings: Settings = serde_json::from_value(json!({
"schema_version": 2,
"providers": {"openai_codex": {"experimental_reasoning_updates": true}}
}))
.unwrap();
assert!(settings.openai_codex.experimental_reasoning_updates);
let roundtrip: Settings =
serde_json::from_value(serde_json::to_value(settings).unwrap()).unwrap();
assert!(roundtrip.openai_codex.experimental_reasoning_updates);
}