use super::*;
#[test]
fn settings_editor_saves_requested_groups_without_replacing_other_fields() {
let temp = tempfile::tempdir().unwrap();
let (paths, _) = paths_with_local_settings(&temp);
fs::write(
&paths.settings_file,
serde_json::to_vec(&json!({
"schema_version": 2,
"future": {"keep": 1},
"agent": {"summarizer": {"prompt": "keep prompt", "future": 2}},
"sessions": {"retention_days": 17},
"capabilities": {"tools": {"view_image": {"future": "keep"}}}
}))
.unwrap(),
)
.unwrap();
let loaded = load_settings_editor(&paths, SettingsScope::Global).unwrap();
assert!(!loaded.summarizer.auto_start);
let settings = save_settings_editor(
&paths,
SettingsScope::Global,
&[
("agent.compaction.auto.enabled", json!(true)),
("agent.compaction.auto.threshold_percent", json!(70)),
("agent.compaction.auto.threshold_tokens", json!(1000)),
("agent.compaction.auto.max_compactions_per_run", json!(0)),
("agent.context.max_tokens", json!(64000)),
("agent.subagents.execution.absolute_paths", json!(false)),
("agent.subagents.execution.max_depth", json!(2)),
("agent.summarizer.auto_start", json!(true)),
("agent.summarizer.provider", json!("openai-codex")),
("agent.summarizer.model", json!("gpt-5.4")),
("agent.summarizer.reasoning", json!("high")),
("automation.integrations.herdr.enabled", json!(true)),
("capabilities.tools.output_compression.enabled", json!(true)),
("capabilities.tools.view_image.absolute_paths", json!(false)),
("capabilities.tools.view_image.max_image_bytes", json!(1024)),
(
"capabilities.tools.view_image.vision_model.provider",
json!("openai-codex"),
),
(
"capabilities.tools.view_image.vision_model.model",
json!("gpt-5.4"),
),
("interface.tui.subagent_card_rows", json!(8)),
(
"interface.tui.autocomplete.respects_gitignore",
json!(false),
),
("knowledge.skills.additional_paths", json!(["/tmp/skills"])),
(
"providers.streaming.semantic_progress_timeout_seconds",
json!(90),
),
(
"providers.streaming.subagent_semantic_progress_timeout_seconds",
json!(180),
),
("sessions.titles.enabled", json!(true)),
("sessions.titles.provider", json!("openai-codex")),
("sessions.titles.model", json!("gpt-5.4")),
],
)
.unwrap();
assert!(settings.compaction.auto.enabled);
assert_eq!(settings.context.unwrap().max_tokens, 64000);
assert_eq!(settings.tools.subagents.max_depth, 2);
assert!(settings.summarizer.auto_start);
assert_eq!(settings.summarizer.reasoning, Some(ThinkingLevel::High));
assert!(settings.integrations.herdr.enabled);
assert!(settings.tools.output_compression.enabled);
assert_eq!(settings.tools.view_image.max_image_bytes, 1024);
assert_eq!(settings.tui.subagent_card_rows(), 8);
assert!(!settings.file_autocomplete_respects_gitignore);
assert_eq!(
settings.skills.additional_paths,
vec![PathBuf::from("/tmp/skills")]
);
assert_eq!(
settings.provider_stream.semantic_progress_timeout_seconds,
Some(90)
);
assert!(settings.session_titles.enabled);
let raw = read_settings_value(&paths);
assert_eq!(raw["future"]["keep"], 1);
assert_eq!(raw["agent"]["summarizer"]["prompt"], "keep prompt");
assert_eq!(raw["agent"]["summarizer"]["future"], 2);
assert_eq!(raw["sessions"]["retention_days"], 17);
assert_eq!(raw["capabilities"]["tools"]["view_image"]["future"], "keep");
}
#[test]
fn settings_editor_rejects_invalid_batches_without_writing() {
let temp = tempfile::tempdir().unwrap();
let (paths, _) = paths_with_local_settings(&temp);
fs::write(&paths.settings_file, "{\"schema_version\":2,\"future\":1}").unwrap();
let original = fs::read(&paths.settings_file).unwrap();
for (path, value) in [
("providers.custom", json!({})),
("agent.summarizer.auto_start", json!("true")),
("agent.compaction.auto.threshold_percent", json!(101)),
("agent.compaction.provider", json!("openai-codex")),
("agent.compaction.model", json!("gpt-5.4")),
("agent.compaction.provider", json!(42)),
("agent.compaction.model", json!(" ")),
("agent.subagents.execution.max_depth", json!(0)),
("capabilities.tools.view_image.max_image_bytes", json!(0)),
("interface.tui.subagent_card_rows", json!(51)),
(
"providers.streaming.semantic_progress_timeout_seconds",
json!(601),
),
("sessions.titles.model", json!("bad model")),
("knowledge.skills.additional_paths", json!(false)),
("agent.summarizer.reasoning", json!("invalid")),
] {
assert!(
save_settings_editor(
&paths,
SettingsScope::Global,
&[
("automation.integrations.herdr.enabled", json!(true)),
(path, value)
]
)
.is_err(),
"accepted {path}"
);
assert_eq!(
fs::read(&paths.settings_file).unwrap(),
original,
"wrote {path}"
);
}
}
#[test]
fn settings_editor_pins_project_defaults_and_clears_optional_selection() {
let temp = tempfile::tempdir().unwrap();
let (paths, local) = paths_with_local_settings(&temp);
fs::write(
&paths.settings_file,
serde_json::to_vec(&json!({
"schema_version": 2,
"agent": {"summarizer": {"auto_start": true, "model": "global-model"}},
"capabilities": {"tools": {"view_image": {"vision_model": {
"provider": "openai-codex", "model": "global-image"
}}}}
}))
.unwrap(),
)
.unwrap();
let global_before = fs::read(&paths.settings_file).unwrap();
assert!(
load_settings_editor(&paths, SettingsScope::Project)
.unwrap()
.summarizer
.auto_start
);
let settings = save_settings_editor(
&paths,
SettingsScope::Project,
&[
("agent.summarizer.model", Value::Null),
("agent.summarizer.auto_start", json!(false)),
("sessions.titles.enabled", json!(false)),
(
"capabilities.tools.view_image.vision_model.model",
json!("project-image"),
),
],
)
.unwrap();
assert!(!settings.summarizer.auto_start);
assert_eq!(settings.summarizer.model, None);
assert_eq!(
settings.tools.view_image.vision_model.unwrap().provider,
"openai-codex"
);
assert_eq!(fs::read(&paths.settings_file).unwrap(), global_before);
let raw: serde_json::Value = serde_json::from_slice(&fs::read(local).unwrap()).unwrap();
assert_eq!(raw["sessions"]["titles"]["enabled"], false);
assert_eq!(raw["agent"]["summarizer"]["model"], Value::Null);
}
#[test]
fn settings_editor_preserves_changes_made_after_load() {
let temp = tempfile::tempdir().unwrap();
let (paths, _) = paths_with_local_settings(&temp);
load_settings_editor(&paths, SettingsScope::Global).unwrap();
set_fast_mode(&paths, true).unwrap();
save_settings_editor(
&paths,
SettingsScope::Global,
&[("agent.summarizer.auto_start", json!(true))],
)
.unwrap();
assert!(read_settings(&paths).unwrap().fast.enabled);
}
use serde_json::Value;
#[test]
fn settings_editor_clears_image_selection_and_rejects_partial_global_selection() {
let temp = tempfile::tempdir().unwrap();
let (paths, _) = paths_with_local_settings(&temp);
assert!(
save_settings_editor(
&paths,
SettingsScope::Global,
&[(
"capabilities.tools.view_image.vision_model.model",
json!("image-model")
)]
)
.is_err()
);
assert!(!paths.settings_file.exists());
save_settings_editor(
&paths,
SettingsScope::Global,
&[
(
"capabilities.tools.view_image.vision_model.provider",
json!("openai-codex"),
),
(
"capabilities.tools.view_image.vision_model.model",
json!("image-model"),
),
],
)
.unwrap();
let settings = save_settings_editor(
&paths,
SettingsScope::Project,
&[("capabilities.tools.view_image.vision_model", Value::Null)],
)
.unwrap();
assert!(settings.tools.view_image.vision_model.is_none());
}
#[test]
fn settings_editor_rejects_invalid_global_value_even_with_valid_project_override() {
let temp = tempfile::tempdir().unwrap();
let (paths, local) = paths_with_local_settings(&temp);
fs::write(&paths.settings_file, "{}").unwrap();
fs::write(
local,
r#"{"schema_version":2,"interface":{"tui":{"subagent_card_rows":5}}}"#,
)
.unwrap();
assert!(
save_settings_editor(
&paths,
SettingsScope::Global,
&[("interface.tui.subagent_card_rows", json!(100)),]
)
.is_err()
);
assert_eq!(fs::read_to_string(&paths.settings_file).unwrap(), "{}");
}
#[test]
fn settings_editor_requires_title_selection_and_accepts_inherited_selection() {
let temp = tempfile::tempdir().unwrap();
let (paths, _) = paths_with_local_settings(&temp);
fs::write(&paths.settings_file, "{}").unwrap();
assert!(
save_settings_editor(
&paths,
SettingsScope::Global,
&[("sessions.titles.enabled", json!(true))],
)
.is_err()
);
assert_eq!(fs::read_to_string(&paths.settings_file).unwrap(), "{}");
save_settings_editor(
&paths,
SettingsScope::Global,
&[
("sessions.titles.provider", json!("openai-codex")),
("sessions.titles.model", json!("gpt-5.4")),
],
)
.unwrap();
let saved = save_settings_editor(
&paths,
SettingsScope::Project,
&[("sessions.titles.enabled", json!(true))],
)
.unwrap();
assert!(saved.session_titles.eligible_config().unwrap().is_some());
}
#[test]
fn settings_editor_project_load_rejects_hidden_invalid_global_values() {
let temp = tempfile::tempdir().unwrap();
let (paths, local) = paths_with_local_settings(&temp);
for (path, timeout) in [(&paths.settings_file, 601), (&local, 90)] {
fs::write(
path,
serde_json::to_vec(&json!({
"schema_version": 2,
"providers": {"streaming": {"semantic_progress_timeout_seconds": timeout}}
}))
.unwrap(),
)
.unwrap();
}
assert!(load_settings_editor(&paths, SettingsScope::Project).is_err());
assert!(save_settings_editor(&paths, SettingsScope::Project, &[]).is_err());
}
#[test]
fn settings_editor_compaction_pair_roundtrip_inheritance_and_clear() {
let temp = tempfile::tempdir().unwrap();
let (paths, _) = paths_with_local_settings(&temp);
fs::write(
&paths.settings_file,
serde_json::to_vec(&json!({
"schema_version": 2,
"future": "keep",
"agent": {"compaction": {"future": 42, "auto": {"enabled": true, "threshold_percent": 80}}}
}))
.unwrap(),
)
.unwrap();
save_settings_editor(
&paths,
SettingsScope::Global,
&[
("agent.compaction.provider", json!("openai-codex")),
("agent.compaction.model", json!("gpt-5.4")),
],
)
.unwrap();
let inherited = load_settings_editor(&paths, SettingsScope::Project).unwrap();
assert_eq!(
inherited.compaction.provider.as_deref(),
Some("openai-codex")
);
assert_eq!(inherited.compaction.model.as_deref(), Some("gpt-5.4"));
for scope in [SettingsScope::Project, SettingsScope::Global] {
let original = fs::read(&paths.settings_file).unwrap();
assert!(
save_settings_editor(&paths, scope, &[("agent.compaction.provider", Value::Null)])
.is_err()
);
assert_eq!(fs::read(&paths.settings_file).unwrap(), original);
save_settings_editor(
&paths,
scope,
&[
("agent.compaction.provider", Value::Null),
("agent.compaction.model", Value::Null),
],
)
.unwrap();
let cleared = load_settings_editor(&paths, scope).unwrap();
let resolved = cleared
.compaction
.resolve_config("active", "active-model")
.unwrap();
assert_eq!(resolved.provider, "active");
assert_eq!(resolved.model, "active-model");
assert!(cleared.compaction.auto.enabled);
}
let raw = read_settings_value(&paths);
assert_eq!(raw["future"], "keep");
assert_eq!(raw["agent"]["compaction"]["future"], 42);
assert!(raw["agent"]["compaction"]["provider"].is_null());
assert!(raw["agent"]["compaction"]["model"].is_null());
}
#[test]
fn settings_editor_model_exclusions_save_in_selected_scope_and_preserve_unknown_fields() {
let temp = tempfile::tempdir().unwrap();
let (paths, _) = paths_with_local_settings(&temp);
fs::write(&paths.settings_file, serde_json::to_vec(&json!({"schema_version":2,"providers":{"catalog":{"disabled":["provider/old"],"future":7}}})).unwrap()).unwrap();
let global = fs::read(&paths.settings_file).unwrap();
assert_eq!(
load_settings_editor(&paths, SettingsScope::Project)
.unwrap()
.models
.disabled,
vec!["provider/old"]
);
save_settings_editor(
&paths,
SettingsScope::Project,
&[("providers.catalog.disabled", json!([]))],
)
.unwrap();
assert!(read_settings(&paths).unwrap().models.disabled.is_empty());
assert_eq!(fs::read(&paths.settings_file).unwrap(), global);
save_settings_editor(
&paths,
SettingsScope::Global,
&[("providers.catalog.disabled", json!(["provider/new"]))],
)
.unwrap();
let raw = read_settings_value(&paths);
assert_eq!(raw["providers"]["catalog"]["future"], 7);
assert_eq!(
raw["providers"]["catalog"]["disabled"],
json!(["provider/new"])
);
}