magi-code 0.80.0

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;
use crate::config::settings::{read_settings, set_mcp_server_enabled};
use serde_json::json;
use std::fs;

fn fixture() -> (tempfile::TempDir, McPaths) {
    let temp = tempfile::TempDir::new().unwrap();
    let paths =
        McPaths::from_root_and_project_dir(temp.path().join("config"), temp.path().join("project"));
    fs::create_dir_all(&paths.root).unwrap();
    fs::create_dir_all(paths.project_settings_file.parent().unwrap()).unwrap();
    (temp, paths)
}

#[test]
fn project_shadowing_and_other_projects_require_separate_approval() {
    let (temp, paths) = fixture();
    let [global, project] = source_paths(&paths).unwrap();
    fs::write(
        &global,
        r#"{"mcpServers":{"shared":{"command":"global","args":["global-only"],"enabled":true}}}"#,
    )
    .unwrap();
    assert!(!read_settings(&paths).unwrap().mcp_servers["shared"].enabled());
    set_mcp_server_enabled(&paths, "shared", true).unwrap();
    assert!(read_settings(&paths).unwrap().mcp_servers["shared"].enabled());
    fs::write(
        &project,
        r#"{"mcpServers":{"shared":{"command":"project","enabled":true}}}"#,
    )
    .unwrap();
    let settings = read_settings(&paths).unwrap();
    let McpServerConfig::Stdio(server) = &settings.mcp_servers["shared"] else {
        panic!()
    };
    assert!(!server.enabled);
    assert!(server.args.is_empty());
    assert_eq!(server.command, "project");
    // Project settings cannot self-approve even using the correct canonical source key.
    fs::write(&paths.project_settings_file, serde_json::to_vec(&json!({
        "schema_version":2, "capabilities":{"mcp_approvals":{project.canonicalize().unwrap().to_str().unwrap():{"shared":true}}}
    })).unwrap()).unwrap();
    assert!(!read_settings(&paths).unwrap().mcp_servers["shared"].enabled());
    set_mcp_server_enabled(&paths, "shared", true).unwrap();
    assert!(read_settings(&paths).unwrap().mcp_servers["shared"].enabled());
    let other = McPaths::from_root_and_project_dir(paths.root.clone(), temp.path().join("other"));
    fs::create_dir_all(temp.path().join("other")).unwrap();
    fs::copy(&project, temp.path().join("other/.mcp.json")).unwrap();
    assert!(!read_settings(&other).unwrap().mcp_servers["shared"].enabled());
    set_mcp_server_enabled(&paths, "shared", false).unwrap();
    assert!(!read_settings(&paths).unwrap().mcp_servers["shared"].enabled());
    fs::remove_file(project).unwrap();
    assert!(read_settings(&paths).unwrap().mcp_servers["shared"].enabled());
}

#[test]
fn approval_writes_preserve_definitions_and_unknown_settings() {
    let (_temp, paths) = fixture();
    let definition = r#"{"mcpServers":{"remote":{"type":"http","url":"https://example.test/mcp","headers":{"Authorization":"Bearer private"}}}}"#;
    fs::write(paths.root.join(".mcp.json"), definition).unwrap();
    fs::write(&paths.settings_file, r#"{"future":{"keep":true}}"#).unwrap();
    set_mcp_server_enabled(&paths, "remote", true).unwrap();
    let raw = fs::read_to_string(&paths.settings_file).unwrap();
    assert!(!raw.contains("Bearer private"));
    assert!(!raw.contains("https://"));
    assert_eq!(
        serde_json::from_str::<Value>(&raw).unwrap()["future"]["keep"],
        true
    );
    assert_eq!(
        fs::read_to_string(paths.root.join(".mcp.json")).unwrap(),
        definition
    );
    assert!(read_settings(&paths).unwrap().mcp_servers["remote"].enabled());
}

#[test]
fn environment_expansion_supports_variables_defaults_and_missing_errors() {
    assert_eq!(
        expand_environment("pre-${SET}/${MISSING:-fallback}/${SET}", |name| (name
            == "SET")
            .then(|| "value".into()))
        .unwrap(),
        "pre-value/fallback/value"
    );
    for input in ["${MISSING}", "${BAD", "${}", "${1BAD}"] {
        assert!(expand_environment(input, |_| None).is_err());
    }
    let mut config = parse_server(json!({"command":"${MAGI_TEST_UNSET_7373:-node}", "args":["${MAGI_TEST_UNSET_7373:-script}"],"env":{"KEY":"${MAGI_TEST_UNSET_7373:-value}"}})).unwrap();
    expand_server(&mut config).unwrap();
    let McpServerConfig::Stdio(stdio) = config else {
        panic!()
    };
    assert_eq!(stdio.command, "node");
    assert_eq!(stdio.args, ["script"]);
    assert_eq!(stdio.env["KEY"], "value");
    let mut config = parse_server(json!({"type":"http", "url":"https://${MAGI_TEST_UNSET_7373:-example.test}/mcp", "headers":{"Authorization":"Bearer ${MAGI_TEST_UNSET_7373:-secret}"}})).unwrap();
    expand_server(&mut config).unwrap();
    let McpServerConfig::Http(http) = config else {
        panic!()
    };
    assert_eq!(http.url, "https://example.test/mcp");
    assert_eq!(http.headers["Authorization"], "Bearer secret");
}

#[test]
fn invalid_files_and_sse_report_source_without_secret_values() {
    let (_temp, paths) = fixture();
    let path = paths.root.join(".mcp.json");
    fs::write(
        &path,
        r#"{"mcpServers":{"legacy":{"type":"sse","url":"https://private.example/secret"}}}"#,
    )
    .unwrap();
    let error = format!("{:#}", read_settings(&paths).unwrap_err());
    assert!(error.contains("legacy SSE transport is unsupported"));
    assert!(error.contains(".mcp.json"));
    assert!(!error.contains("private.example"));
    fs::write(&path, "invalid json").unwrap();
    assert!(
        read_settings(&paths)
            .unwrap_err()
            .to_string()
            .contains(".mcp.json")
    );
    fs::write(
        &path,
        vec![b' '; crate::config::settings_storage::MAX_SETTINGS_FILE_BYTES + 1],
    )
    .unwrap();
    assert!(read_settings(&paths).is_err());
}

#[test]
fn settings_server_definitions_are_ignored_and_runtime_map_is_not_serialized() {
    let (_temp, paths) = fixture();
    fs::write(&paths.settings_file, r#"{"schema_version":2,"capabilities":{"mcp":{"bad":{"command":"ignored","enabled":true}}}}"#).unwrap();
    assert!(read_settings(&paths).unwrap().mcp_servers.is_empty());
    fs::write(
        paths.root.join(".mcp.json"),
        r#"{"mcpServers":{"real":{"command":"node"}}}"#,
    )
    .unwrap();
    let settings = read_settings(&paths).unwrap();
    assert_eq!(settings.mcp_servers.len(), 1);
    let wire = serde_json::to_value(settings).unwrap();
    assert!(wire["capabilities"].get("mcp").is_none());
    assert!(wire.get("mcp_servers").is_none());
}

#[test]
fn startup_loads_mcp_without_project_settings() {
    let (_temp, paths) = fixture();
    fs::write(
        paths.root.join(".mcp.json"),
        r#"{"mcpServers":{"real":{"command":"node"}}}"#,
    )
    .unwrap();
    let (_, settings, _, _) = crate::config::settings::load_startup_config_with_settings(
        paths,
        crate::config::CliConfigOverrides::default(),
    )
    .unwrap();
    assert_eq!(settings.mcp_servers.len(), 1);
    assert!(!settings.mcp_servers["real"].enabled());
}

#[cfg(unix)]
#[test]
fn mcp_file_symlinks_are_rejected() {
    let (temp, paths) = fixture();
    let target = temp.path().join("target.json");
    fs::write(&target, r#"{"mcpServers":{"real":{"command":"node"}}}"#).unwrap();
    std::os::unix::fs::symlink(target, paths.root.join(".mcp.json")).unwrap();
    assert!(read_settings(&paths).is_err());
}

#[test]
fn missing_credentials_do_not_block_discovery_startup_or_disabling() {
    let env = crate::test_support::env::env_lock();
    let token = "MAGI_MCP_DISABLED_TEST_TOKEN";
    env.remove_var(token);
    let (_temp, paths) = fixture();
    let path = paths.root.join(".mcp.json");
    fs::write(&path, r#"{"mcpServers":{"remote":{"type":"http","url":"${MAGI_MCP_DISABLED_TEST_TOKEN}","headers":{"Authorization":"Bearer ${MAGI_MCP_DISABLED_TEST_TOKEN}"}},"local":{"command":"${MAGI_MCP_DISABLED_TEST_TOKEN}","args":["${MAGI_MCP_DISABLED_TEST_TOKEN}"],"env":{"TOKEN":"${MAGI_MCP_DISABLED_TEST_TOKEN}"}}}}"#).unwrap();
    let settings = read_settings(&paths).unwrap();
    assert_eq!(settings.mcp_servers.len(), 2);
    assert!(
        settings
            .mcp_servers
            .values()
            .all(|config| !config.enabled())
    );
    super::super::validation::validate_settings(&settings).unwrap();
    crate::config::settings::load_startup_config_with_settings(
        paths.clone(),
        crate::config::CliConfigOverrides::default(),
    )
    .unwrap();
    fs::write(&paths.settings_file, r#"{"future":{"keep":true}}"#).unwrap();
    let before = fs::read(&paths.settings_file).unwrap();
    for name in ["remote", "local"] {
        let error = format!(
            "{:#}",
            set_mcp_server_enabled(&paths, name, true).unwrap_err()
        );
        assert!(error.contains(token), "{error}");
        assert_eq!(fs::read(&paths.settings_file).unwrap(), before);
    }
    // Approval intentionally survives definition edits; revocation must still work.
    fs::write(
        &path,
        r#"{"mcpServers":{"remote":{"type":"http","url":"https://example.test/mcp"}}}"#,
    )
    .unwrap();
    set_mcp_server_enabled(&paths, "remote", true).unwrap();
    fs::write(
        &path,
        r#"{"mcpServers":{"remote":{"type":"http","url":"${MAGI_MCP_DISABLED_TEST_TOKEN}"}}}"#,
    )
    .unwrap();
    assert!(read_settings(&paths).is_err());
    set_mcp_server_enabled(&paths, "remote", false).unwrap();
    assert!(!read_settings(&paths).unwrap().mcp_servers["remote"].enabled());
}

#[test]
fn enabling_validates_expanded_values_before_persisting() {
    let env = crate::test_support::env::env_lock();
    env.remove_var("MAGI_MCP_ENABLE_TEST_VALUE");
    let (_temp, paths) = fixture();
    fs::write(&paths.settings_file, "{}").unwrap();
    for definition in [
        json!({"type":"http", "url":"${MAGI_MCP_ENABLE_TEST_VALUE:-ftp://example.test}"}),
        json!({"type":"http", "url":"https://example.test", "headers":{"Authorization":"${MAGI_MCP_ENABLE_TEST_VALUE:-secret\nvalue}"}}),
        json!({"command":"${MAGI_MCP_ENABLE_TEST_VALUE:- }"}),
    ] {
        fs::write(
            paths.root.join(".mcp.json"),
            serde_json::to_vec(&json!({"mcpServers":{"server":definition}})).unwrap(),
        )
        .unwrap();
        let before = fs::read(&paths.settings_file).unwrap();
        assert!(set_mcp_server_enabled(&paths, "server", true).is_err());
        assert_eq!(fs::read(&paths.settings_file).unwrap(), before);
    }
    fs::write(paths.root.join(".mcp.json"), r#"{"mcpServers":{"server":{"type":"http","url":"${MAGI_MCP_ENABLE_TEST_VALUE:-https://example.test/mcp}","headers":{"Authorization":"{env:1BAD}"}}}}"#).unwrap();
    set_mcp_server_enabled(&paths, "server", true).unwrap();
    let settings = read_settings(&paths).unwrap();
    let McpServerConfig::Http(server) = &settings.mcp_servers["server"] else {
        panic!()
    };
    assert!(server.enabled);
    assert_eq!(server.url, "https://example.test/mcp");
    assert_eq!(server.headers["Authorization"], "{env:1BAD}");
}