use super::*;
#[test]
fn herdr_settings_default_enabled_and_unknown_fields() {
let absent: Settings = serde_json::from_str("{}").unwrap();
assert!(absent.integrations.herdr.is_default());
assert!(!absent.integrations.herdr.enabled);
let enabled: Settings =
serde_json::from_str(r#"{"integrations":{"herdr":{"enabled":true,"future":"kept"}}}"#)
.unwrap();
assert!(enabled.integrations.herdr.enabled);
let value = serde_json::to_value(&enabled).unwrap();
assert_eq!(
value["automation"]["integrations"]["herdr"]["enabled"],
true
);
assert_eq!(
value["automation"]["integrations"]["herdr"]["future"],
"kept"
);
let default_value = serde_json::to_value(Settings::default()).unwrap();
assert!(default_value.get("integrations").is_none());
}
#[test]
fn auto_compaction_defaults_and_round_trips_json_shape() {
let defaults: Settings = serde_json::from_str("{}").unwrap();
assert_eq!(defaults.compaction.auto, AutoCompactionSettings::default());
assert!(matches!(
defaults.compaction.auto.compaction_limit(),
AutoCompactionLimit::Limited(limit)
if limit.get() == usize::from(DEFAULT_AUTO_COMPACTIONS_PER_RUN)
));
let settings: Settings = serde_json::from_str(
r#"{"compaction":{"auto":{"enabled":true,"threshold_percent":50,"threshold_tokens":10,"max_compactions_per_run":7}}}"#,
)
.unwrap();
assert!(settings.compaction.auto.is_enabled());
assert_eq!(
settings.compaction.auto.threshold_display().as_deref(),
Some("50% or 10 tokens")
);
assert!(matches!(
settings.compaction.auto.compaction_limit(),
AutoCompactionLimit::Limited(limit) if limit.get() == 7
));
let json = serde_json::to_value(&settings).unwrap();
assert_eq!(json["agent"]["compaction"]["auto"]["threshold_percent"], 50);
assert_eq!(json["agent"]["compaction"]["auto"]["threshold_tokens"], 10);
assert_eq!(
json["agent"]["compaction"]["auto"]["max_compactions_per_run"],
7
);
let round_trip: Settings = serde_json::from_value(json).unwrap();
validate_settings(&round_trip).unwrap();
assert_eq!(round_trip, settings);
let no_count_cap: Settings =
serde_json::from_str(r#"{"compaction":{"auto":{"max_compactions_per_run":0}}}"#).unwrap();
assert_eq!(
no_count_cap.compaction.auto.max_compactions_per_run,
Some(0)
);
assert_eq!(
no_count_cap.compaction.auto.compaction_limit(),
AutoCompactionLimit::NoCountCap
);
let no_count_cap_json = serde_json::to_value(&no_count_cap).unwrap();
assert_eq!(
no_count_cap_json["agent"]["compaction"]["auto"]["max_compactions_per_run"],
0
);
let no_count_cap_round_trip: Settings = serde_json::from_value(no_count_cap_json).unwrap();
validate_settings(&no_count_cap_round_trip).unwrap();
assert_eq!(no_count_cap_round_trip, no_count_cap);
let schema = serde_json::to_value(schemars::schema_for!(Settings)).unwrap();
let pointer = "/$defs/AutoCompactionSettings/properties/max_compactions_per_run";
assert_eq!(
schema.pointer(&format!("{pointer}/minimum")),
Some(&serde_json::json!(0))
);
assert_eq!(
schema.pointer(&format!("{pointer}/maximum")),
Some(&serde_json::json!(255))
);
assert_eq!(
schema.pointer(&format!("{pointer}/default")),
Some(&serde_json::json!(DEFAULT_AUTO_COMPACTIONS_PER_RUN))
);
assert_eq!(
schema.pointer(&format!("{pointer}/description")),
Some(&serde_json::json!(
"Maximum automatic compactions per run. When omitted, the effective runtime default is 4. Set to 0 to remove only this per-run compaction-count cap."
))
);
}
#[test]
fn auto_compaction_validation_requires_at_least_one_valid_threshold() {
for raw in [
r#"{"compaction":{"auto":{"enabled":true}}}"#,
r#"{"compaction":{"auto":{"threshold_percent":0}}}"#,
r#"{"compaction":{"auto":{"threshold_percent":101}}}"#,
r#"{"compaction":{"auto":{"threshold_tokens":0}}}"#,
r#"{"compaction":{"auto":{"max_compactions_per_run":256}}}"#,
r#"{"compaction":{"auto":{"enabled":true,"threshold_percent":0,"threshold_tokens":10}}}"#,
r#"{"compaction":{"auto":{"enabled":true,"threshold_percent":50,"threshold_tokens":0}}}"#,
] {
assert!(parse_validated_settings(raw).is_err(), "accepted {raw}");
}
assert!(
parse_validated_settings(
r#"{"compaction":{"auto":{"enabled":true,"threshold_percent":50,"threshold_tokens":10}}}"#
)
.is_ok()
);
assert!(parse_validated_settings(r#"{"compaction":{"auto":{"enabled":false}}}"#).is_ok());
assert!(
parse_validated_settings(
r#"{"compaction":{"auto":{"enabled":true,"threshold_percent":100}}}"#
)
.is_ok()
);
assert!(
parse_validated_settings(
r#"{"compaction":{"auto":{"enabled":true,"threshold_tokens":1}}}"#
)
.is_ok()
);
for max_compactions_per_run in [0, 1, 255] {
let raw = serde_json::json!({
"compaction": {
"auto": {
"threshold_tokens": 1,
"max_compactions_per_run": max_compactions_per_run,
}
}
})
.to_string();
assert!(parse_validated_settings(&raw).is_ok());
}
}
#[test]
fn auto_compaction_trigger_uses_safe_percentage_math_and_boundaries() {
let percent_first = AutoCompactionSettings {
enabled: true,
threshold_percent: Some(50),
threshold_tokens: Some(900),
max_compactions_per_run: None,
};
assert!(!percent_first.triggered(499, 1_000));
assert!(percent_first.triggered(500, 1_000));
let token_first = AutoCompactionSettings {
enabled: true,
threshold_percent: Some(80),
threshold_tokens: Some(300),
max_compactions_per_run: None,
};
assert!(!token_first.triggered(299, 1_000));
assert!(token_first.triggered(300, 1_000));
let tie = AutoCompactionSettings {
enabled: true,
threshold_percent: Some(50),
threshold_tokens: Some(500),
max_compactions_per_run: None,
};
assert!(!tie.triggered(499, 1_000));
assert!(tie.triggered(500, 1_000));
let disabled = AutoCompactionSettings {
enabled: false,
threshold_percent: Some(50),
threshold_tokens: Some(300),
max_compactions_per_run: None,
};
assert!(!disabled.triggered(u64::MAX, u64::MAX));
let percent = AutoCompactionSettings {
enabled: true,
threshold_percent: Some(50),
threshold_tokens: None,
max_compactions_per_run: None,
};
assert!(!percent.triggered(49, 100));
assert!(percent.triggered(50, 100));
assert!(percent.triggered(u64::MAX, u64::MAX));
let tokens = AutoCompactionSettings {
enabled: true,
threshold_percent: None,
threshold_tokens: Some(u64::MAX),
max_compactions_per_run: None,
};
assert!(!tokens.triggered(u64::MAX - 1, u64::MAX));
assert!(tokens.triggered(u64::MAX, 1));
}
#[test]
fn summarizer_settings_round_trip_and_validate_overrides() {
let defaults: Settings = serde_json::from_str("{}").unwrap();
assert!(!defaults.summarizer.auto_start);
assert!(defaults.summarizer.provider.is_none());
assert!(defaults.summarizer.model.is_none());
assert!(defaults.summarizer.reasoning.is_none());
assert!(defaults.summarizer.prompt.is_none());
let configured = json!({"schema_version": 2, "agent": {"summarizer": {
"auto_start": true, "provider": "anthropic", "model": "summary-model",
"reasoning": "high", "prompt": "Summarize decisions.\nKeep open questions."
}}});
let settings: Settings = serde_json::from_value(configured.clone()).unwrap();
validate_settings(&settings).unwrap();
assert_eq!(settings.summarizer.reasoning, Some(ThinkingLevel::High));
let serialized = serde_json::to_value(&settings).unwrap();
assert_eq!(
serialized["agent"]["summarizer"],
configured["agent"]["summarizer"]
);
assert_eq!(
serde_json::from_value::<Settings>(serialized).unwrap(),
settings
);
for invalid in [
json!({"provider": " "}),
json!({"model": "bad model"}),
json!({"prompt": "\n "}),
json!({"reasoning": "invalid"}),
json!({"auto_start": "yes"}),
] {
assert!(
parse_validated_settings(&json!({"agent": {"summarizer": invalid}}).to_string())
.is_err()
);
}
for partial in [
json!({"provider": "anthropic"}),
json!({"model": "summary-model"}),
] {
parse_validated_settings(&json!({"agent": {"summarizer": partial}}).to_string()).unwrap();
}
}
#[test]
fn summarizer_project_merge_and_mutations_preserve_unknown_fields() {
let temp = tempfile::TempDir::new().unwrap();
let (paths, local_settings) = paths_with_local_settings(&temp);
fs::write(&paths.settings_file, json!({"schema_version": 2, "agent": {"summarizer": {
"auto_start": true, "provider": "anthropic", "reasoning": "low", "prompt": "Global prompt",
"future": {"keep": true}
}}}).to_string()).unwrap();
fs::write(
&local_settings,
json!({"schema_version": 2, "agent": {"summarizer": {
"auto_start": false, "model": "project-model", "prompt": "Project prompt"
}}})
.to_string(),
)
.unwrap();
let merged = read_settings(&paths).unwrap();
assert!(!merged.summarizer.auto_start);
assert_eq!(merged.summarizer.provider.as_deref(), Some("anthropic"));
assert_eq!(merged.summarizer.model.as_deref(), Some("project-model"));
assert_eq!(merged.summarizer.reasoning, Some(ThinkingLevel::Low));
assert_eq!(merged.summarizer.prompt.as_deref(), Some("Project prompt"));
update_settings_preserving_unknown_top_level_fields(&paths, |settings| {
settings.summarizer.auto_start = false;
settings.summarizer.prompt = Some("Updated global prompt".to_string());
})
.unwrap();
let saved = read_settings_value(&paths);
assert_eq!(saved["agent"]["summarizer"]["auto_start"], false);
assert_eq!(saved["agent"]["summarizer"]["future"]["keep"], true);
assert_eq!(
saved["agent"]["summarizer"]["prompt"],
"Updated global prompt"
);
assert_eq!(
read_settings(&paths).unwrap().summarizer.prompt.as_deref(),
Some("Project prompt")
);
update_settings_for_scope_preserving_unknown_top_level_fields(
&paths,
SettingsScope::Project,
|settings| settings.summarizer = SummarizerSettings::default(),
)
.unwrap();
let cleared = read_settings(&paths).unwrap().summarizer;
assert_eq!(cleared, SummarizerSettings::default());
let project: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&local_settings).unwrap()).unwrap();
assert_eq!(
project["agent"]["summarizer"]["prompt"],
serde_json::Value::Null
);
assert_eq!(
read_settings_value(&paths)["agent"]["summarizer"]["prompt"],
"Updated global prompt"
);
}