magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

#[test]
fn mcp_servers_parse_defaults_validate_and_serialize() {
    let settings: Settings = serde_json::from_str(
        r#"{"mcp_servers":{"mock":{"type":"stdio","command":"node","args":["server.js"],"env":{},"enabled":true,"timeout":30}}}"#,
    )
    .unwrap();
    let config = settings.mcp_servers.get("mock").unwrap();
    match config {
        McpServerConfig::Stdio(stdio) => {
            assert_eq!(stdio.command, "node");
            assert_eq!(stdio.args, vec!["server.js"]);
            assert!(stdio.env.is_empty());
            assert!(stdio.enabled);
            assert_eq!(stdio.timeout, Some(30));
        }
        McpServerConfig::Http(_) => panic!("expected stdio config"),
    }

    let absent: Settings = serde_json::from_str("{}").unwrap();
    assert!(absent.mcp_servers.is_empty());
    let serialized = serde_json::to_value(settings).unwrap();
    assert_eq!(serialized["capabilities"]["mcp"]["mock"]["type"], "stdio");
}

#[test]
fn mcp_http_servers_parse_defaults_validate_serialize_and_redact_debug() {
    let settings: Settings = serde_json::from_str(
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp?token=url-secret#frag","headers":{"Authorization":"{env:MCP_TOKEN}","X-Team":"platform"}}}}"#,
    )
    .unwrap();
    let config = settings.mcp_servers.get("remote").unwrap();
    match config {
        McpServerConfig::Http(http) => {
            assert_eq!(
                http.url,
                "https://mcp.example.test/mcp?token=url-secret#frag"
            );
            assert!(http.enabled);
            assert_eq!(http.timeout, None);
            assert_eq!(http.headers["Authorization"], "{env:MCP_TOKEN}");
            assert!(http.oauth.is_none());
            let debug = format!("{http:?}");
            assert!(debug.contains("[REDACTED]"), "{debug}");
            assert!(!debug.contains("platform"), "{debug}");
            assert!(!debug.contains("MCP_TOKEN"), "{debug}");
            assert!(!debug.contains("url-secret"), "{debug}");
            assert!(!debug.contains("token="), "{debug}");
        }
        McpServerConfig::Stdio(_) => panic!("expected http config"),
    }
    let serialized = serde_json::to_value(settings).unwrap();
    assert_eq!(serialized["capabilities"]["mcp"]["remote"]["type"], "http");
}

#[test]
fn mcp_http_servers_reject_invalid_url_headers_and_timeout() {
    for raw in [
        r#"{"mcp_servers":{"remote":{"type":"http","url":"ftp://mcp.example.test/mcp"}}}"#,
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://user:pass@mcp.example.test/mcp"}}}"#,
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","headers":{"Bad:Name":"x"}}}}"#,
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","headers":{"Authorization":"literal"}}}}"#,
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","headers":{"Authorization":"{env:1BAD}"}}}}"#,
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","timeout":301}}}"#,
    ] {
        assert!(parse_validated_settings(raw).is_err(), "accepted {raw}");
    }

    for raw in [
        r#"{"mcp_servers":{"local":{"type":"http","url":"http://localhost:8787/mcp"}}}"#,
        r#"{"mcp_servers":{"local":{"type":"http","url":"http://127.0.0.1:8787/mcp"}}}"#,
        r#"{"mcp_servers":{"local":{"type":"http","url":"http://127.0.0.2:8787/mcp"}}}"#,
        r#"{"mcp_servers":{"local":{"type":"http","url":"http://[::1]:8787/mcp"}}}"#,
    ] {
        assert!(parse_validated_settings(raw).is_ok(), "rejected {raw}");
    }
    let remote =
        r#"{"mcp_servers":{"remote":{"type":"http","url":"http://mcp.example.test/mcp"}}}"#;
    assert!(
        parse_validated_settings(remote).is_err(),
        "accepted {remote}"
    );

    let userinfo = r#"{"mcp_servers":{"remote":{"type":"http","url":"https://user:pass@mcp.example.test/mcp"}}}"#;
    assert!(
        parse_validated_settings(userinfo).is_err(),
        "accepted {userinfo}"
    );
}

#[test]
fn mcp_http_oauth_config_parses_validates_serializes_and_redacts_debug() {
    let settings: Settings = serde_json::from_str(
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","headers":{"X-Team":"platform"},"oauth":{"client_id":"public-client","scopes":["search","offline_access"],"authorization_server":"https://auth.example.test"}}}}"#,
    )
    .unwrap();
    let config = settings.mcp_servers.get("remote").unwrap();
    let McpServerConfig::Http(http) = config else {
        panic!("expected http config");
    };
    let oauth = http.oauth.as_ref().unwrap();
    assert_eq!(oauth.client_id.as_deref(), Some("public-client"));
    assert_eq!(oauth.scopes, vec!["search", "offline_access"]);
    assert_eq!(
        oauth.authorization_server.as_deref(),
        Some("https://auth.example.test")
    );
    let debug = format!("{oauth:?}");
    assert!(debug.contains("[REDACTED]"), "{debug}");
    assert!(!debug.contains("public-client"), "{debug}");

    let serialized = serde_json::to_value(settings).unwrap();
    assert_eq!(
        serialized["capabilities"]["mcp"]["remote"]["oauth"]["client_id"],
        "public-client"
    );
}

#[test]
fn mcp_http_oauth_rejects_authorization_headers_and_invalid_fields() {
    for raw in [
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","headers":{"Authorization":"{env:MCP_TOKEN}"},"oauth":{}}}}"#,
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","headers":{"Proxy-Authorization":"{env:MCP_TOKEN}"},"oauth":{}}}}"#,
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","oauth":{"client_id":" "}}}}"#,
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","oauth":{"scopes":[""]}}}}"#,
        r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","oauth":{"scopes":["bad\u0007scope"]}}}}"#,
    ] {
        assert!(parse_validated_settings(raw).is_err(), "accepted {raw}");
    }

    for raw in [
        r#"{"mcp_servers":{"local":{"type":"http","url":"http://localhost:8787/mcp","oauth":{"authorization_server":"http://localhost:8788"}}}}"#,
        r#"{"mcp_servers":{"local":{"type":"http","url":"http://127.0.0.1:8787/mcp","oauth":{"authorization_server":"http://127.0.0.2:8788"}}}}"#,
    ] {
        assert!(parse_validated_settings(raw).is_ok(), "rejected {raw}");
    }
    let remote = r#"{"mcp_servers":{"remote":{"type":"http","url":"https://mcp.example.test/mcp","oauth":{"authorization_server":"http://auth.example.test"}}}}"#;
    assert!(
        parse_validated_settings(remote).is_err(),
        "accepted {remote}"
    );
}

#[test]
fn settings_schema_generation_includes_mcp_http_config() {
    let schema = schemars::schema_for!(Settings);
    let schema_text = serde_json::to_string(&schema).unwrap();
    assert!(schema_text.contains("McpHttpServerConfig"), "{schema_text}");
    assert!(schema_text.contains("url"), "{schema_text}");
    assert!(schema_text.contains("headers"), "{schema_text}");
    assert!(schema_text.contains("McpOAuthConfig"), "{schema_text}");
    assert!(schema_text.contains("client_id"), "{schema_text}");
    assert!(schema_text.contains("scopes"), "{schema_text}");
    assert!(
        schema_text.contains("authorization_server"),
        "{schema_text}"
    );
}

#[test]
fn mcp_servers_reject_invalid_names_command_and_timeout() {
    for raw in [
        r#"{"mcp_servers":{"bad/name":{"type":"stdio","command":"node"}}}"#,
        r#"{"mcp_servers":{"bad__name":{"type":"stdio","command":"node"}}}"#,
        r#"{"mcp_servers":{"bad_":{"type":"stdio","command":"node"}}}"#,
        r#"{"mcp_servers":{"mock":{"type":"stdio","command":" "}}}"#,
        r#"{"mcp_servers":{"mock":{"type":"stdio","command":"node","timeout":0}}}"#,
        r#"{"mcp_servers":{"mock":{"type":"stdio","command":"node","timeout":301}}}"#,
    ] {
        assert!(parse_validated_settings(raw).is_err(), "accepted {raw}");
    }
}

#[test]
fn lsp_settings_parse_defaults_validate_and_serialize() {
    let absent: Settings = serde_json::from_str("{}").unwrap();
    assert_eq!(absent.lsp, LspSettings::default());
    assert!(!absent.lsp.enabled);
    assert!(absent.lsp.inject_diagnostics_on_edit);
    assert_eq!(
        absent.lsp.diagnostics_wait_ms,
        DEFAULT_LSP_DIAGNOSTICS_WAIT_MS
    );
    assert_eq!(
        absent.lsp.idle_shutdown_minutes,
        DEFAULT_LSP_IDLE_SHUTDOWN_MINUTES
    );
    assert!(serde_json::to_value(&absent).unwrap().get("lsp").is_none());

    let settings: Settings = serde_json::from_str(
        r#"{"lsp":{"enabled":true,"inject_diagnostics_on_edit":false,"diagnostics_wait_ms":1500,"idle_shutdown_minutes":30,"servers":{"rust-analyzer":{"command":"rust-analyzer","args":["--log-file","ra.log"],"enabled":false}}}}"#,
    )
    .unwrap();
    validate_settings(&settings).unwrap();
    assert!(settings.lsp.enabled);
    assert!(!settings.lsp.inject_diagnostics_on_edit);
    assert_eq!(settings.lsp.diagnostics_wait_ms, 1500);
    assert_eq!(settings.lsp.idle_shutdown_minutes, 30);
    let rust_analyzer = settings.lsp.servers.get("rust-analyzer").unwrap();
    assert_eq!(rust_analyzer.command, "rust-analyzer");
    assert_eq!(rust_analyzer.args, vec!["--log-file", "ra.log"]);
    assert!(!rust_analyzer.enabled);

    let serialized = serde_json::to_value(settings).unwrap();
    assert_eq!(serialized["capabilities"]["lsp"]["enabled"], true);
    assert_eq!(
        serialized["capabilities"]["lsp"]["servers"]["rust-analyzer"]["command"],
        "rust-analyzer"
    );
}

#[test]
fn lsp_settings_reject_invalid_values() {
    for raw in [
        r#"{"lsp":{"diagnostics_wait_ms":0}}"#,
        r#"{"lsp":{"diagnostics_wait_ms":30001}}"#,
        r#"{"lsp":{"idle_shutdown_minutes":0}}"#,
        r#"{"lsp":{"idle_shutdown_minutes":241}}"#,
        r#"{"lsp":{"servers":{"":{"command":"rust-analyzer"}}}}"#,
        r#"{"lsp":{"servers":{"bad__name":{"command":"rust-analyzer"}}}}"#,
        r#"{"lsp":{"servers":{"rust-analyzer":{"command":" "}}}}"#,
    ] {
        assert!(parse_validated_settings(raw).is_err(), "accepted {raw}");
    }
}

#[test]
fn lsp_settings_preserved_and_removed_by_settings_update() {
    let mut raw = json!({
        "lsp": {
            "enabled": true,
            "servers": {"rust-analyzer": {"command": "rust-analyzer"}}
        },
        "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["capabilities"]["lsp"]["enabled"], true);
    assert_eq!(
        raw["capabilities"]["lsp"]["servers"]["rust-analyzer"]["command"],
        "rust-analyzer"
    );
    assert_eq!(raw["agent"]["primary_agent"], "new");

    let before = settings.clone();
    settings.lsp = LspSettings::default();
    update_raw_from_settings(&mut raw, &before, &settings, SettingsScope::Global).unwrap();
    assert!(raw.get("lsp").is_none());
}

#[test]
fn mcp_servers_preserved_by_settings_update() {
    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#"{"mcp_servers":{"mock":{"type":"stdio","command":"node","args":["server.js"],"env":{},"enabled":true,"timeout":30}},"future_setting":true}"#,
    )
    .unwrap();

    set_selected_model(&paths, "provider", "model").unwrap();

    let value: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(&paths.settings_file).unwrap()).unwrap();
    assert_eq!(value["capabilities"]["mcp"]["mock"]["command"], "node");
    assert_eq!(value["future_setting"], true);
}

#[test]
fn set_mcp_server_enabled_updates_stdio_http_and_preserves_unknowns() {
    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#"{
            "future_setting":{"keep":true},
            "mcp_servers":{
                "mock":{"type":"stdio","command":"node","enabled":true},
                "remote":{"type":"http","url":"https://mcp.example.test/mcp","enabled":false}
            }
        }"#,
    )
    .unwrap();

    set_mcp_server_enabled(&paths, "mock", false).unwrap();
    set_mcp_server_enabled(&paths, "remote", true).unwrap();

    let value = read_settings_value(&paths);
    assert_eq!(value["future_setting"]["keep"], true);
    assert_eq!(value["capabilities"]["mcp"]["mock"]["enabled"], false);
    assert_eq!(value["capabilities"]["mcp"]["remote"]["enabled"], true);
}

#[test]
fn set_mcp_server_enabled_rejects_missing_or_invalid_name() {
    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#"{"mcp_servers":{}}"#).unwrap();

    let missing = set_mcp_server_enabled(&paths, "missing", true)
        .unwrap_err()
        .to_string();
    assert!(
        missing.contains("mcp server not found: missing"),
        "{missing}"
    );

    let invalid = set_mcp_server_enabled(&paths, "bad/name", true)
        .unwrap_err()
        .to_string();
    assert!(invalid.contains("mcp server name"), "{invalid}");
    let trailing = set_mcp_server_enabled(&paths, "bad_", true)
        .unwrap_err()
        .to_string();
    assert!(trailing.contains("must not end with '_'"), "{trailing}");
    assert!(trailing.contains("ambiguous"), "{trailing}");
}