use super::*;
#[test]
fn tui_subagent_card_rows_defaults_parses_and_validates_bounds() {
let defaults: Settings = serde_json::from_str("{}").unwrap();
assert_eq!(
defaults.tui.subagent_card_rows(),
DEFAULT_TUI_SUBAGENT_CARD_ROWS
);
let explicit: Settings = serde_json::from_str(r#"{"tui":{"subagent_card_rows":12}}"#).unwrap();
assert_eq!(explicit.tui.subagent_card_rows(), 12);
assert!(parse_validated_settings(r#"{"tui":{"subagent_card_rows":1}}"#).is_ok());
assert!(parse_validated_settings(r#"{"tui":{"subagent_card_rows":50}}"#).is_ok());
for rows in [0, 51] {
let error =
parse_validated_settings(&format!(r#"{{"tui":{{"subagent_card_rows":{rows}}}}}"#))
.unwrap_err()
.to_string();
assert!(error.contains("tui.subagent_card_rows"));
}
let schema = serde_json::to_value(schemars::schema_for!(Settings)).unwrap();
assert_eq!(
schema.pointer("/$defs/InterfaceTuiSettingsSchema/properties/subagent_card_rows/minimum"),
Some(&serde_json::json!(1))
);
assert_eq!(
schema.pointer("/$defs/InterfaceTuiSettingsSchema/properties/subagent_card_rows/maximum"),
Some(&serde_json::json!(50))
);
}
#[test]
fn session_retention_settings_default_thirty_zero_disables_and_custom_values() {
let absent: SessionSettings = serde_json::from_str("{}").unwrap();
assert_eq!(absent.retention_days, DEFAULT_SESSION_RETENTION_DAYS);
let disabled: SessionSettings = serde_json::from_str(r#"{"retention_days":0}"#).unwrap();
assert_eq!(disabled.retention_days, 0);
let custom: SessionSettings = serde_json::from_str(r#"{"retention_days":7}"#).unwrap();
assert_eq!(custom.retention_days, 7);
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#"{"sessions":{"retention_days":7}}"#).unwrap();
let (_, loaded) = read_settings_with_session_settings(&paths).unwrap();
assert_eq!(loaded, custom);
let schema = serde_json::to_value(schemars::schema_for!(Settings)).unwrap();
assert_eq!(
schema.pointer("/properties/sessions/$ref"),
Some(&serde_json::json!("#/$defs/SessionsSettingsSchema"))
);
assert_eq!(
schema.pointer("/$defs/SessionsSettingsSchema/properties/retention_days/default"),
Some(&serde_json::json!(DEFAULT_SESSION_RETENTION_DAYS))
);
}
#[test]
fn subagents_schema_retry_settings_default_bounds_and_update_preservation() {
let default_settings: Settings = serde_json::from_str("{}").unwrap();
assert_eq!(
default_settings.subagents.schema_validation_max_retries(),
DEFAULT_SUBAGENT_SCHEMA_VALIDATION_MAX_RETRIES
);
let too_high = parse_validated_settings(r#"{"subagents":{"schema_validation_max_retries":6}}"#)
.unwrap_err()
.to_string();
assert!(too_high.contains("subagents.schema_validation_max_retries"));
let mut raw = json!({
"subagents": {"schema_validation_max_retries": 5},
"selected_primary_agent": "old"
});
let mut settings: Settings = serde_json::from_value(raw.clone()).unwrap();
let before = settings.clone();
settings.selected_primary_agent = Some("new".to_string());
update_raw_from_settings(&mut raw, &before, &settings, SettingsScope::Global).unwrap();
assert_eq!(
raw["agent"]["subagents"]["schema_validation_max_retries"],
5
);
assert!(KNOWN_SUBAGENTS_KEYS.contains(&"schema_validation_max_retries"));
}
#[test]
fn settings_ttsr_defaults_disabled_and_explicit_true_survives() {
let default_settings: Settings = serde_json::from_str("{}").unwrap();
assert!(!default_settings.ttsr.enabled);
let explicit_enabled: Settings = serde_json::from_str(r#"{"ttsr":{"enabled":true}}"#).unwrap();
assert!(explicit_enabled.ttsr.enabled);
}
#[test]
fn settings_ttsr_rejects_invalid_regex_and_empty_reminder() {
let invalid_regex =
parse_validated_settings(r#"{"ttsr":{"rules":[{"pattern":"(","reminder":"stop"}]}}"#)
.unwrap_err()
.to_string();
assert!(invalid_regex.contains("ttsr.rules[0].pattern"));
let empty_reminder =
parse_validated_settings(r#"{"ttsr":{"rules":[{"pattern":"danger","reminder":" "}]}}"#)
.unwrap_err()
.to_string();
assert!(empty_reminder.contains("ttsr.rules[0].reminder"));
}
#[test]
fn ttsr_settings_survive_update_raw_from_settings() {
let mut raw = json!({
"ttsr": {"rules": [{"pattern": "danger", "reminder": "stop"}]},
"selected_primary_agent": "old"
});
let mut settings: Settings = serde_json::from_value(raw.clone()).unwrap();
let before = settings.clone();
settings.selected_primary_agent = Some("new".to_string());
update_raw_from_settings(&mut raw, &before, &settings, SettingsScope::Global).unwrap();
assert_eq!(raw["agent"]["reminders"]["rules"][0]["pattern"], "danger");
assert_eq!(raw["agent"]["primary_agent"], "new");
}
#[test]
fn provider_stream_settings_defaults_parse_validate_and_resolve() {
let absent: Settings = serde_json::from_str("{}").unwrap();
assert_eq!(absent.provider_stream, ProviderStreamSettings::default());
assert_eq!(
absent.provider_stream.semantic_progress_timeout(),
Duration::from_secs(DEFAULT_PROVIDER_STREAM_SEMANTIC_PROGRESS_TIMEOUT_SECONDS)
);
assert_eq!(
absent.provider_stream.subagent_semantic_progress_timeout(),
Duration::from_secs(DEFAULT_SUBAGENT_STREAM_SEMANTIC_PROGRESS_TIMEOUT_SECONDS)
);
let configured: Settings = serde_json::from_str(
r#"{"provider_stream":{"semantic_progress_timeout_seconds":90,"subagent_semantic_progress_timeout_seconds":180}}"#,
)
.unwrap();
validate_settings(&configured).unwrap();
assert_eq!(
configured.provider_stream.semantic_progress_timeout(),
Duration::from_secs(90)
);
assert_eq!(
configured
.provider_stream
.subagent_semantic_progress_timeout(),
Duration::from_secs(180)
);
let too_low =
parse_validated_settings(r#"{"provider_stream":{"semantic_progress_timeout_seconds":0}}"#)
.unwrap_err()
.to_string();
assert!(too_low.contains("provider_stream.semantic_progress_timeout_seconds"));
let too_high = parse_validated_settings(
r#"{"provider_stream":{"subagent_semantic_progress_timeout_seconds":601}}"#,
)
.unwrap_err()
.to_string();
assert!(too_high.contains("provider_stream.subagent_semantic_progress_timeout_seconds"));
}
#[test]
fn fast_settings_parse_default_invalid_and_schema() {
let absent: Settings = serde_json::from_str("{}").unwrap();
assert!(!absent.fast.enabled);
assert_eq!(absent.openai_codex.text_verbosity, TextVerbosity::Low);
assert!(serde_json::to_value(&absent).unwrap().get("fast").is_none());
let enabled: Settings = serde_json::from_str(
r#"{"fast":{"enabled":true},"openai_codex":{"text_verbosity":"medium"}}"#,
)
.unwrap();
assert!(enabled.fast.enabled);
assert_eq!(enabled.openai_codex.text_verbosity, TextVerbosity::Medium);
assert_eq!(
serde_json::to_value(&enabled).unwrap()["agent"]["fast"]["enabled"],
true
);
assert_eq!(
serde_json::to_value(&enabled).unwrap()["providers"]["openai_codex"]["text_verbosity"],
"medium"
);
let error = serde_json::from_str::<Settings>(r#"{"fast":{"enabled":"yes"}}"#)
.unwrap_err()
.to_string();
assert!(error.contains("boolean"), "{error}");
let schema = serde_json::to_value(schemars::schema_for!(Settings)).unwrap();
assert!(schema.pointer("/properties/agent").is_some());
let enabled_schema = schema
.pointer("/$defs/FastSettings/properties/enabled")
.expect("schema missing fast.enabled");
assert_eq!(enabled_schema["type"], "boolean");
assert_eq!(enabled_schema["default"], false);
}
#[test]
fn custom_provider_fast_mode_schema_matches_runtime_constraints() {
let schema = serde_json::to_value(schemars::schema_for!(Settings)).unwrap();
let fast_mode = schema
.pointer("/$defs/CustomProviderFastMode")
.expect("schema missing CustomProviderFastMode");
let service_tier = &fast_mode["properties"]["service_tier"];
let models = &fast_mode["properties"]["models"];
assert!(service_tier.get("minLength").is_none());
assert!(service_tier.get("maxLength").is_none());
assert_eq!(service_tier["pattern"], r"^\s*[A-Za-z0-9_.-]{1,64}\s*$");
assert_eq!(models["minItems"], 1);
assert_eq!(models["maxItems"], 64);
assert_eq!(models["uniqueItems"], true);
assert!(models["items"].get("minLength").is_none());
assert!(models["items"].get("maxLength").is_none());
assert_eq!(models["items"]["pattern"], r"^\S{1,200}$");
assert_eq!(models["oneOf"][0]["contains"]["pattern"], r"^\*$");
assert_eq!(models["oneOf"][1]["not"]["contains"]["pattern"], r"^\*$");
let is_schema_valid = |service_tier: serde_json::Value, models: serde_json::Value| {
let instance = json!({
"service_tier": service_tier,
"models": models,
});
jsonschema::validator_for(fast_mode)
.unwrap()
.iter_errors(&instance)
.next()
.is_none()
};
let is_runtime_valid = |models: Vec<String>| {
let instance = json!({
"custom_providers": {
"provider": {
"label": "Provider",
"base_url": "https://example.test",
"fast_mode": {
"service_tier": "priority",
"models": models,
},
},
},
});
parse_validated_settings(&instance.to_string()).is_ok()
};
assert!(is_schema_valid(json!("priority"), json!(["gpt-5.5"])));
assert!(is_runtime_valid(vec!["gpt-5.5".to_string()]));
assert!(is_schema_valid(json!(" priority "), json!(["model-a"])));
assert!(is_runtime_valid(vec!["model-a".to_string()]));
assert!(is_schema_valid(json!(" priority "), json!(["*"])));
assert!(is_runtime_valid(vec!["*".to_string()]));
let emoji_200 = "😀".repeat(200);
assert!(is_schema_valid(
json!("priority"),
json!([emoji_200.clone()])
));
assert!(is_runtime_valid(vec![emoji_200]));
let emoji_201 = "😀".repeat(201);
assert!(!is_schema_valid(
json!("priority"),
json!([emoji_201.clone()])
));
assert!(!is_runtime_valid(vec![emoji_201]));
for model in [
" model".to_string(),
"model ".to_string(),
"model id".to_string(),
] {
assert!(
!is_schema_valid(json!("priority"), json!([model.clone()])),
"schema accepted model with whitespace"
);
assert!(!is_runtime_valid(vec![model]));
}
assert!(!is_runtime_valid(vec!["model\u{2003}id".to_string()]));
assert!(!is_runtime_valid(vec!["model\u{0000}id".to_string()]));
assert!(!is_schema_valid(
json!("priority"),
json!(["model", "model"])
));
assert!(!is_runtime_valid(vec![
"model".to_string(),
"model".to_string()
]));
for (label, service_tier, models) in [
("empty models", json!("priority"), json!([])),
("empty model id", json!("priority"), json!([""])),
("blank model id", json!("priority"), json!([" "])),
(
"mixed wildcard models",
json!("priority"),
json!(["*", "gpt-5.5"]),
),
(
"mixed wildcard models with whitespace",
json!("priority"),
json!([" * ", "gpt-5.5"]),
),
(
"model id internal whitespace",
json!("priority"),
json!(["gpt 5.5"]),
),
(
"service tier internal whitespace",
json!("fast tier"),
json!(["gpt-5.5"]),
),
("blank service tier", json!(" "), json!(["gpt-5.5"])),
(
"oversized service tier core",
json!(format!(" {} ", "t".repeat(65))),
json!(["gpt-5.5"]),
),
(
"oversized model core",
json!("priority"),
json!(["m".repeat(201)]),
),
] {
assert!(
!is_schema_valid(service_tier, models),
"schema accepted {label}"
);
}
}
#[test]
fn openai_codex_text_verbosity_settings_parse_default_invalid_and_schema() {
let absent: Settings = serde_json::from_str("{}").unwrap();
assert_eq!(absent.openai_codex.text_verbosity, TextVerbosity::Low);
assert!(
serde_json::to_value(&absent)
.unwrap()
.get("openai_codex")
.is_none()
);
let medium: Settings =
serde_json::from_str(r#"{"openai_codex":{"text_verbosity":"medium"}}"#).unwrap();
assert_eq!(medium.openai_codex.text_verbosity, TextVerbosity::Medium);
assert_eq!(
serde_json::to_value(&medium).unwrap()["providers"]["openai_codex"]["text_verbosity"],
"medium"
);
let error =
serde_json::from_str::<Settings>(r#"{"openai_codex":{"text_verbosity":"verbose"}}"#)
.unwrap_err()
.to_string();
assert!(error.contains("expected one of"), "{error}");
let schema = serde_json::to_value(schemars::schema_for!(Settings)).unwrap();
let schema_text = schema.to_string();
for value in ["openai_codex", "text_verbosity", "low", "medium", "high"] {
assert!(
schema_text.contains(value),
"schema missing {value}: {schema_text}"
);
}
}
#[test]
fn checked_in_settings_schema_contains_fast_enabled_boolean_default() {
let schema_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("state")
.join("settings.schema.json");
let schema: serde_json::Value =
serde_json::from_str(&fs::read_to_string(schema_path).unwrap()).unwrap();
let enabled = schema
.pointer("/$defs/FastSettings/properties/enabled")
.expect("checked-in schema missing fast.enabled");
assert_eq!(enabled["type"], "boolean");
assert_eq!(enabled["default"], false);
}
#[test]
fn openai_responses_text_verbosity_resolves_shared_and_capability_rules() {
let mut settings: Settings = serde_json::from_str(
r#"{
"openai_responses": {"text_verbosity": "medium"},
"openai_codex": {"text_verbosity": "high"},
"custom_providers": {
"capable": {
"label": "Capable",
"base_url": "http://localhost:8080/v1",
"use_responses_endpoint": true,
"supports_text_verbosity": true
},
"unsupported": {
"label": "Unsupported",
"base_url": "http://localhost:8081/v1",
"use_responses_endpoint": true
}
}
}"#,
)
.unwrap();
assert_eq!(
settings.text_verbosity_for(crate::providers::OPENAI_CODEX_PROVIDER),
Some(TextVerbosity::Medium)
);
assert_eq!(
settings.text_verbosity_for("capable"),
Some(TextVerbosity::Medium)
);
assert_eq!(settings.text_verbosity_for("unsupported"), None);
assert_eq!(settings.text_verbosity_for("missing"), None);
settings.openai_responses.text_verbosity = None;
assert_eq!(
settings.text_verbosity_for(crate::providers::OPENAI_CODEX_PROVIDER),
Some(TextVerbosity::High)
);
}
#[test]
fn anthropic_cache_ttl_settings_parse_serialize_and_validate_schema() {
let settings: Settings = serde_json::from_str(r#"{"anthropic_cache_ttl":"1h"}"#).unwrap();
assert_eq!(
settings.anthropic_cache_ttl,
Some(AnthropicCacheTtl::OneHour)
);
assert_eq!(
serde_json::to_value(&settings).unwrap()["providers"]["anthropic"]["cache_ttl"],
"1h"
);
let five_minute_settings: Settings =
serde_json::from_str(r#"{"anthropic_cache_ttl":"5m"}"#).unwrap();
assert_eq!(
five_minute_settings.anthropic_cache_ttl,
Some(AnthropicCacheTtl::FiveMinutes)
);
assert_eq!(
serde_json::to_value(&five_minute_settings).unwrap()["providers"]["anthropic"]["cache_ttl"],
"5m"
);
let default_settings: Settings = serde_json::from_str("{}").unwrap();
assert_eq!(default_settings.anthropic_cache_ttl, None);
assert!(
serde_json::to_value(&default_settings)
.unwrap()
.get("anthropic_cache_ttl")
.is_none()
);
let schema = serde_json::to_string(&schemars::schema_for!(Settings)).unwrap();
assert!(schema.contains("cache_ttl"), "{schema}");
}
#[test]
fn context_model_overrides_parse_serialize_and_validate_keys() {
let settings: Settings = serde_json::from_str(
r#"{"context":{"max_tokens":128000,"model_overrides":{"openai-codex/gpt-5.5":{"max_tokens":400000,"reserve_tokens":32768},"zai/Qwen/Qwen3":{"keep_recent_tokens":50000}}}}"#,
)
.unwrap();
let budget = settings.context.as_ref().unwrap();
let codex = budget.model_overrides.get("openai-codex/gpt-5.5").unwrap();
assert_eq!(codex.max_tokens, Some(400_000));
assert_eq!(codex.reserve_tokens, Some(32_768));
assert_eq!(codex.keep_recent_tokens, None);
assert_eq!(
budget
.model_overrides
.get("zai/Qwen/Qwen3")
.unwrap()
.keep_recent_tokens,
Some(50_000)
);
let serialized = serde_json::to_value(&settings).unwrap();
assert_eq!(
serialized["agent"]["context"]["model_overrides"]["openai-codex/gpt-5.5"]["max_tokens"],
400000
);
for raw in [
r#"{"context":{"model_overrides":{"missing-slash":{"max_tokens":1}}}}"#,
r#"{"context":{"model_overrides":{"/missing-provider":{"max_tokens":1}}}}"#,
r#"{"context":{"model_overrides":{"provider/":{"max_tokens":1}}}}"#,
r#"{"context":{"model_overrides":{"provider /model":{"max_tokens":1}}}}"#,
r#"{"context":{"model_overrides":{"provider/model":{}}}}"#,
] {
assert!(parse_validated_settings(raw).is_err(), "accepted {raw}");
}
}
#[test]
fn instructions_settings_deserialize_additional_markdown_paths() {
let settings: Settings = serde_json::from_str(
r#"{"instructions":{"additional_markdown_paths":["/opt/team.md","/Users/test/review.md"]}}"#,
)
.unwrap();
assert!(!settings.instructions.subdir_discovery);
assert_eq!(
settings.instructions.additional_markdown_paths,
vec![
PathBuf::from("/opt/team.md"),
PathBuf::from("/Users/test/review.md")
]
);
}
#[test]
fn instructions_settings_deserialize_subdir_discovery() {
let default_settings: Settings = serde_json::from_str("{}").unwrap();
assert!(!default_settings.instructions.subdir_discovery);
let enabled: Settings =
serde_json::from_str(r#"{"instructions":{"subdir_discovery":true}}"#).unwrap();
assert!(enabled.instructions.subdir_discovery);
let serialized = serde_json::to_value(&enabled).unwrap();
assert_eq!(
serialized["knowledge"]["instructions"]["subdir_discovery"],
true
);
let schema = serde_json::to_string(&schemars::schema_for!(Settings)).unwrap();
assert!(schema.contains("subdir_discovery"), "{schema}");
}
#[test]
fn skills_settings_deserialize_additional_paths() {
let settings: Settings = serde_json::from_str(
r#"{"skills":{"additional_paths":["/opt/magi-skills","/Users/test/skills"]}}"#,
)
.unwrap();
assert_eq!(
settings.skills.additional_paths,
vec![
PathBuf::from("/opt/magi-skills"),
PathBuf::from("/Users/test/skills")
]
);
}