magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
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"
    );
}