cartog 0.32.1

Code graph indexer for LLM coding agents. Map your codebase, navigate by graph.
Documentation
//! Tests for config schema types and their defaults/accessors.

use crate::config::*;
use std::fs;

#[test]
fn redact_secrets_defaults_true_when_absent() {
    let cfg = CartogConfig::default();
    assert!(to_redaction_config(&cfg).enabled);
}

#[test]
fn redact_secrets_can_be_disabled_via_config() {
    let dir = tempfile::TempDir::new().unwrap();
    let cfg_path = dir.path().join("config.toml");
    fs::write(&cfg_path, "[security]\nredact_secrets = false\n").unwrap();
    let cfg = read_config(&cfg_path).expect("should parse");
    assert!(!to_redaction_config(&cfg).enabled);
}

#[test]
fn test_remote_config_valid_minimal() {
    let dir = tempfile::TempDir::new().unwrap();
    let cfg_path = dir.path().join(".cartog.toml");
    fs::write(
        &cfg_path,
        r#"[remote]
url = "s3://team-bucket/cartog/main"
region = "us-east-1"
"#,
    )
    .unwrap();
    let cfg = read_config(&cfg_path).expect("should parse");
    let remote = cfg.remote.expect("remote section parsed");
    assert_eq!(remote.url.as_deref(), Some("s3://team-bucket/cartog/main"));
    assert_eq!(remote.region.as_deref(), Some("us-east-1"));
    assert_eq!(remote.path_style, None);
}

#[test]
fn test_remote_config_full_minio_shape() {
    let dir = tempfile::TempDir::new().unwrap();
    let cfg_path = dir.path().join(".cartog.toml");
    fs::write(
        &cfg_path,
        r#"[remote]
url = "s3://b/k"
region = "us-east-1"
endpoint = "https://minio.local"
path_style = true
"#,
    )
    .unwrap();
    let cfg = read_config(&cfg_path).expect("should parse");
    let r = cfg.remote.unwrap();
    assert_eq!(r.endpoint.as_deref(), Some("https://minio.local"));
    assert_eq!(r.path_style, Some(true));
}

#[test]
fn test_remote_config_rejects_credential_keys() {
    for bad in [
        "access_key = \"AKIA...\"",
        "secret_key = \"...\"",
        "credentials = \"...\"",
        "token = \"...\"",
        "session_token = \"...\"",
        "password = \"...\"",
        "aws_access_key_id = \"...\"",
        "AWS_SECRET = \"...\"",
    ] {
        let dir = tempfile::TempDir::new().unwrap();
        let cfg_path = dir.path().join(".cartog.toml");
        fs::write(&cfg_path, format!("[remote]\nurl = \"s3://b/k\"\n{bad}\n")).unwrap();
        assert!(
            read_config(&cfg_path).is_none(),
            "should reject credential key: {bad}"
        );
    }
}

#[test]
fn test_remote_config_rejects_nested_credential_keys() {
    for bad_section in [
        "[remote.aws]\naccess_key = \"AKIA...\"\n",
        "[remote.creds]\nsecret_key = \"...\"\n",
        "[remote.minio]\naws_session_token = \"...\"\n",
    ] {
        let dir = tempfile::TempDir::new().unwrap();
        let cfg_path = dir.path().join(".cartog.toml");
        fs::write(
            &cfg_path,
            format!("[remote]\nurl = \"s3://b/k\"\n{bad_section}"),
        )
        .unwrap();
        assert!(
            read_config(&cfg_path).is_none(),
            "should reject nested credential: {bad_section}"
        );
    }
}

#[test]
fn test_remote_config_rejects_credential_prefixes() {
    for bad in [
        // Names not in CREDENTIAL_KEYS but caught by prefix.
        "access_token_v2 = \"...\"",
        "secret_value = \"...\"",
        "aws_role_arn = \"...\"",
    ] {
        let dir = tempfile::TempDir::new().unwrap();
        let cfg_path = dir.path().join(".cartog.toml");
        fs::write(&cfg_path, format!("[remote]\nurl = \"s3://b/k\"\n{bad}\n")).unwrap();
        assert!(
            read_config(&cfg_path).is_none(),
            "should reject prefix-matched credential key: {bad}"
        );
    }
}

#[test]
fn test_remote_config_rejects_unknown_field() {
    let dir = tempfile::TempDir::new().unwrap();
    let cfg_path = dir.path().join(".cartog.toml");
    fs::write(
        &cfg_path,
        "[remote]\nurl = \"s3://b/k\"\npathstyle = true\n",
    )
    .unwrap();
    assert!(read_config(&cfg_path).is_none());
}

#[test]
fn test_remote_config_rejects_endpoint_with_userinfo() {
    for bad in [
        "http://AKIA:secret@minio.local",
        "https://user@s3.example.com",
        // No scheme — still parseable as userinfo + host.
        "AKIA:secret@host:9000",
        "https://user:pass@host/path",
    ] {
        let dir = tempfile::TempDir::new().unwrap();
        let cfg_path = dir.path().join(".cartog.toml");
        fs::write(
            &cfg_path,
            format!("[remote]\nurl = \"s3://b/k\"\nendpoint = \"{bad}\"\n"),
        )
        .unwrap();
        assert!(
            read_config(&cfg_path).is_none(),
            "should reject endpoint with userinfo: {bad}"
        );
    }
}

#[test]
fn test_remote_config_accepts_clean_endpoints() {
    for ok in [
        "https://s3.us-east-1.amazonaws.com",
        "https://minio.example.com:9000",
        "https://r2.cloudflarestorage.com/path",
        "http://localhost:4566",
    ] {
        let dir = tempfile::TempDir::new().unwrap();
        let cfg_path = dir.path().join(".cartog.toml");
        fs::write(
            &cfg_path,
            format!("[remote]\nurl = \"s3://b/k\"\nendpoint = \"{ok}\"\n"),
        )
        .unwrap();
        assert!(
            read_config(&cfg_path).is_some(),
            "should accept clean endpoint: {ok}"
        );
    }
}

#[test]
fn test_embedding_config_defaults() {
    let cfg = EmbeddingConfig::default();
    assert_eq!(cfg.provider(), "local");
    assert!(cfg.dimension.is_none());
    assert!(cfg.model.is_none());
    assert!(cfg.local.is_none());
    assert!(cfg.ollama.is_none());
}

#[test]
fn test_embedding_config_from_toml() {
    let toml_str = r#"
[embedding]
provider = "ollama"
model = "nomic-embed-text"
dimension = 768
"#;
    let cfg: CartogConfig = toml::from_str(toml_str).unwrap();
    let embed = cfg.embedding.unwrap();
    assert_eq!(embed.provider(), "ollama");
    assert_eq!(embed.model.as_deref(), Some("nomic-embed-text"));
    assert_eq!(embed.dimension, Some(768));
}

#[test]
fn test_embedding_config_local_with_prefixes() {
    let toml_str = r#"
[embedding]
provider = "local"
model = "BAAI/bge-small-en-v1.5"

[embedding.local]
query_prefix = "search_query: "
document_prefix = "search_document: "
"#;
    let cfg: CartogConfig = toml::from_str(toml_str).unwrap();
    let embed = cfg.embedding.unwrap();
    assert_eq!(embed.provider(), "local");
    let local = embed.local.unwrap();
    assert_eq!(local.query_prefix.as_deref(), Some("search_query: "));
    assert_eq!(local.document_prefix.as_deref(), Some("search_document: "));
}

#[test]
fn test_ollama_config_defaults() {
    let cfg = OllamaConfig::default();
    assert_eq!(cfg.base_url(), "http://localhost:11434");
    assert_eq!(cfg.model(), "nomic-embed-text");
}

#[test]
fn test_ollama_config_from_toml() {
    let toml_str = r#"
[embedding.ollama]
base_url = "http://gpu-server:11434"
model = "mxbai-embed-large"
"#;
    let cfg: CartogConfig = toml::from_str(toml_str).unwrap();
    let ollama = cfg.embedding.unwrap().ollama.unwrap();
    assert_eq!(ollama.base_url(), "http://gpu-server:11434");
    assert_eq!(ollama.model(), "mxbai-embed-large");
}

#[test]
fn test_openai_config_defaults() {
    let cfg = OpenAiConfig::default();
    assert_eq!(cfg.base_url(), "https://api.openai.com/v1");
    assert_eq!(cfg.model(), "text-embedding-3-small");
    assert_eq!(cfg.api_key_env(), "OPENAI_API_KEY");
}

#[test]
fn test_openai_config_from_toml() {
    let toml_str = r#"
[embedding.openai]
base_url = "http://localhost:11434/v1"
model = "nomic-embed-text"
api_key_env = "MY_OPENAI_KEY"
"#;
    let cfg: CartogConfig = toml::from_str(toml_str).unwrap();
    let openai = cfg.embedding.unwrap().openai.unwrap();
    assert_eq!(openai.base_url(), "http://localhost:11434/v1");
    assert_eq!(openai.model(), "nomic-embed-text");
    assert_eq!(openai.api_key_env(), "MY_OPENAI_KEY");
}

#[test]
fn test_reranker_config_defaults() {
    let cfg = RerankerConfig::default();
    assert_eq!(cfg.provider(), "local");
}

#[test]
fn test_reranker_config_none() {
    let toml_str = r#"
[reranker]
provider = "none"
"#;
    let cfg: CartogConfig = toml::from_str(toml_str).unwrap();
    assert_eq!(cfg.reranker.unwrap().provider(), "none");
}

#[test]
fn test_reranker_model_defaults_to_none_in_config() {
    let cfg = RerankerConfig::default();
    assert!(cfg.model.is_none());
}

#[test]
fn test_full_config_backward_compat() {
    let toml_str = r#"
[database]
path = "/tmp/test.db"
"#;
    let cfg: CartogConfig = toml::from_str(toml_str).unwrap();
    assert!(cfg.embedding.is_none());
    assert!(cfg.reranker.is_none());
    assert_eq!(cfg.database.unwrap().path.as_deref(), Some("/tmp/test.db"));
}

#[test]
fn test_config_unknown_fields_ignored() {
    let toml_str = r#"
[embedding]
provider = "local"
unknown_field = "should be ignored"
"#;
    // serde default: unknown fields are silently ignored
    let cfg: CartogConfig = toml::from_str(toml_str).unwrap();
    assert_eq!(cfg.embedding.unwrap().provider(), "local");
}

#[test]
fn to_search_tuning_clamps_zero_retrieval() {
    let cfg = RagConfig {
        retrieval_multiplier: Some(0),
        retrieval_floor: Some(0),
        rerank_max: None,
        rerank_min: None,
    };
    let t = cfg.to_search_tuning();
    assert_eq!(t.retrieval_multiplier, 1);
    assert_eq!(t.retrieval_floor, 1);
}

#[test]
fn to_search_tuning_caps_rerank_min_at_max() {
    let cfg = RagConfig {
        retrieval_multiplier: None,
        retrieval_floor: None,
        rerank_max: Some(10),
        rerank_min: Some(50),
    };
    let t = cfg.to_search_tuning();
    assert_eq!(t.rerank_max, 10);
    assert_eq!(t.rerank_min, 10);
}

#[test]
fn to_search_tuning_passes_valid_values() {
    let cfg = RagConfig {
        retrieval_multiplier: Some(5),
        retrieval_floor: Some(40),
        rerank_max: Some(100),
        rerank_min: Some(10),
    };
    let t = cfg.to_search_tuning();
    assert_eq!(t.retrieval_multiplier, 5);
    assert_eq!(t.retrieval_floor, 40);
    assert_eq!(t.rerank_max, 100);
    assert_eq!(t.rerank_min, 10);
}