vtcode-config 0.181.0

Config loader components shared across VT Code and downstream adopters
Documentation
use super::*;

#[test]
fn sparse_config_omits_default_primary_agent_when_unmodified() {
    let config = VTCodeConfig::default();
    let value = ConfigManager::sparse_config_value(&config).expect("sparse config");

    assert!(value.get("default_primary_agent").is_none());
}

#[test]
fn sparse_config_persists_non_default_primary_agent() {
    let config = VTCodeConfig {
        default_primary_agent: "auto".to_string(),
        ..Default::default()
    };

    let value = ConfigManager::sparse_config_value(&config).expect("sparse config");

    assert_eq!(value.get("default_primary_agent").and_then(toml::Value::as_str), Some("auto"));
}

#[test]
fn has_explicit_top_level_key_matches_effective_config() {
    let temp_dir = tempfile::tempdir().expect("temp dir");
    let workspace = temp_dir.path().join("workspace");
    fs::create_dir_all(workspace.join(".vtcode")).expect("workspace dot dir");

    let manager = crate::loader::ConfigBuilder::new()
        .workspace(workspace.clone())
        .build()
        .expect("manager");
    assert!(!manager.has_explicit_top_level_key("default_primary_agent"));
    assert!(
        manager
            .effective_config()
            .as_table()
            .is_none_or(|table| !table.contains_key("default_primary_agent"))
    );

    let overridden = crate::loader::ConfigBuilder::new()
        .workspace(workspace)
        .cli_override("default_primary_agent".to_owned(), toml::Value::String("duck".to_owned()))
        .build()
        .expect("overridden manager");
    assert!(overridden.has_explicit_top_level_key("default_primary_agent"));
    assert!(
        overridden
            .effective_config()
            .as_table()
            .is_some_and(|table| table.contains_key("default_primary_agent"))
    );
    assert!(!overridden.has_explicit_top_level_key("definitely_missing_key"));
}

#[test]
fn save_config_migrates_legacy_permissions_auto_permission_table() {
    let temp_dir = tempfile::tempdir().expect("temp dir");
    let config_path = temp_dir.path().join("vtcode.toml");
    fs::write(
        &config_path,
        r#"
[permissions.auto_permission]
model = "legacy-reviewer"
max_consecutive_denials = 2
"#,
    )
    .expect("write legacy config");

    let mut config = VTCodeConfig::default();
    config.permissions.auto_permission.model = "legacy-reviewer".to_string();
    config.permissions.auto_permission.max_consecutive_denials = 2;

    ConfigManager::save_config_to_path(&config_path, &config).expect("save config");

    let saved_content = fs::read_to_string(&config_path).expect("read saved config");
    assert!(
        saved_content.contains("[permissions.auto]"),
        "canonical auto permission table should be persisted. Got:\n{saved_content}"
    );
    assert!(
        !saved_content.contains("auto_permission"),
        "legacy auto_permission table should be removed. Got:\n{saved_content}"
    );

    let reloaded = ConfigManager::load_from_file(&config_path).expect("reload saved config");
    assert_eq!(reloaded.config().permissions.auto_permission.model, "legacy-reviewer");
    assert_eq!(reloaded.config().permissions.auto_permission.max_consecutive_denials, 2);
}

#[test]
fn save_config_removes_legacy_permissions_auto_permission_when_sparse_default() {
    let temp_dir = tempfile::tempdir().expect("temp dir");
    let config_path = temp_dir.path().join("vtcode.toml");
    fs::write(
        &config_path,
        r#"
[permissions.auto_permission]
max_consecutive_denials = 3
"#,
    )
    .expect("write legacy config");

    ConfigManager::save_config_to_path(&config_path, &VTCodeConfig::default()).expect("save config");

    let saved_content = fs::read_to_string(&config_path).expect("read saved config");
    assert!(
        !saved_content.contains("auto_permission"),
        "legacy auto_permission table should be removed. Got:\n{saved_content}"
    );
    assert!(
        !saved_content.contains("[permissions.auto]"),
        "default auto permission config should remain sparse. Got:\n{saved_content}"
    );

    ConfigManager::load_from_file(&config_path).expect("reload saved config");
}

#[test]
fn inaccessible_optional_global_layers_are_skipped() {
    let error = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
    assert!(should_skip_optional_global_error(
        &ConfigLayerSource::System { file: PathBuf::from("/etc/vtcode/vtcode.toml") },
        &error
    ));
    assert!(should_skip_optional_global_error(
        &ConfigLayerSource::User {
            file: PathBuf::from("/home/user/.config/vtcode/vtcode.toml")
        },
        &error
    ));
    assert!(!should_skip_optional_global_error(
        &ConfigLayerSource::Workspace { file: PathBuf::from("/workspace/vtcode.toml") },
        &error
    ));
}

#[test]
fn disabled_layer_reason_tracks_concrete_error_type() {
    let temp_dir = tempfile::tempdir().expect("temp dir");
    let parse_path = temp_dir.path().join("invalid.toml");
    fs::write(&parse_path, "key = [").expect("write invalid TOML");
    let parse_error = ConfigManager::load_toml_from_file(&parse_path).expect_err("invalid TOML should fail");
    let parse_layer =
        ConfigManager::disabled_layer_from_error(ConfigLayerSource::Project { file: parse_path }, parse_error);

    assert_eq!(parse_layer.disabled_reason, Some(LayerDisabledReason::ParseError));

    let read_path = temp_dir.path().join("parse");
    fs::create_dir(&read_path).expect("create directory at config path");
    let read_error = ConfigManager::load_toml_from_file(&read_path).expect_err("reading a directory should fail");
    assert!(read_error.to_string().contains("parse"));
    let read_layer =
        ConfigManager::disabled_layer_from_error(ConfigLayerSource::Project { file: read_path }, read_error);

    assert_eq!(read_layer.disabled_reason, Some(LayerDisabledReason::LoadError));
}