codebase-graph 1.3.0

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::*;

pub(super) fn setup_fixture_repo(root: &Path) {
    run(
        [
            "install",
            "--repo-root",
            root.to_str().unwrap(),
            "--mode",
            "full",
            "--mcp-client",
            "none",
            "--no-fts",
            "--no-semantic-enrichment",
            "--json",
        ],
        &mut Vec::new(),
    )
    .unwrap();
}

pub(super) fn setup_search_fixture_repo(root: &Path) {
    run(
        [
            "install",
            "--repo-root",
            root.to_str().unwrap(),
            "--mode",
            "full",
            "--mcp-client",
            "none",
            "--no-semantic-enrichment",
            "--json",
        ],
        &mut Vec::new(),
    )
    .unwrap();
}

pub(super) fn managed_active_manifest_path(root: &Path) -> PathBuf {
    let storage_root = root.join(".codebaseGraph/storage");
    let active: serde_json::Value = serde_json::from_slice(
        &fs::read(storage_root.join("active.json")).expect("managed active pointer should exist"),
    )
    .expect("managed active pointer should parse");
    let generation_id = active["generation_id"]
        .as_str()
        .expect("managed active pointer should contain generation_id");
    storage_root
        .join("generations")
        .join(format!("gen-{generation_id}"))
        .join("manifest.json")
}

pub(super) fn assert_managed_generation_paths(
    root: &Path,
    database_path: &Path,
    manifest_path: &Path,
) {
    let expected_manifest_path = managed_active_manifest_path(root);
    let expected_database_path = expected_manifest_path.with_file_name("graph.ldb");
    assert_eq!(
        fs::canonicalize(manifest_path).unwrap(),
        fs::canonicalize(expected_manifest_path).unwrap()
    );
    assert_eq!(
        fs::canonicalize(database_path).unwrap(),
        fs::canonicalize(expected_database_path).unwrap()
    );
}

pub(super) fn test_http_options(root: PathBuf, auth_token: Option<&str>) -> McpHttpOptions {
    McpHttpOptions {
        serve: McpServeOptions {
            repo_root: Some(root),
            config: None,
            db: None,
            manifest: None,
            api: None,
        },
        host: "127.0.0.1".to_string(),
        port: 8765,
        endpoint_path: "/mcp".to_string(),
        allow_remote: false,
        auth_token: auth_token.map(str::to_string),
    }
}

pub(super) fn http_json_request(
    method: &str,
    path: &str,
    headers: &[(&str, &str)],
    payload: serde_json::Value,
) -> HttpRequest {
    let mut header_map = BTreeMap::new();
    for (name, value) in headers {
        header_map.insert(name.to_ascii_lowercase(), value.to_string());
    }
    HttpRequest {
        method: method.to_string(),
        path: path.to_string(),
        headers: header_map,
        body: serde_json::to_vec(&payload).unwrap(),
        body_too_large: false,
    }
}

pub(super) fn assert_json_array_contains(value: &serde_json::Value, expected: &str) {
    assert!(
        json_array_contains(value, expected),
        "expected {value:?} to contain {expected}"
    );
}

pub(super) fn json_array_contains(value: &serde_json::Value, expected: &str) -> bool {
    value
        .as_array()
        .unwrap()
        .iter()
        .any(|item| item.as_str() == Some(expected))
}

pub(super) fn watch_filter_for(root: &Path, extra_args: &[&str]) -> WatchEventFilter {
    fs::create_dir_all(root).unwrap();
    let source_root = root.canonicalize().unwrap();
    let mut args = vec![
        "--source-root".to_string(),
        source_root.to_string_lossy().to_string(),
        "--no-git".to_string(),
    ];
    args.extend(extra_args.iter().map(|arg| arg.to_string()));
    let options = MaterializeOptions::parse_with_command(&args, "watch").unwrap();
    let request = materialize_request(&options, crate::api::OutputFormat::Typed);
    WatchEventFilter::from_request(&source_root, &request).unwrap()
}

pub(super) fn watch_test_event(root: &Path, kind: EventKind, paths: &[&str]) -> Event {
    let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
    Event {
        kind,
        paths: paths.iter().map(|path| root.join(path)).collect(),
        attrs: Default::default(),
    }
}

pub(super) struct WatchTestEnvGuard {
    _guard: std::sync::MutexGuard<'static, ()>,
}

impl Drop for WatchTestEnvGuard {
    fn drop(&mut self) {
        env::remove_var("CODEBASE_GRAPH_WATCH_PROBE_TIMEOUT_MS");
        env::remove_var("CODEBASE_GRAPH_WATCH_PROBE_SKIP_WRITE");
    }
}

pub(super) fn watch_test_env_lock() -> WatchTestEnvGuard {
    static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
    let guard = LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
    env::remove_var("CODEBASE_GRAPH_WATCH_PROBE_TIMEOUT_MS");
    env::remove_var("CODEBASE_GRAPH_WATCH_PROBE_SKIP_WRITE");
    WatchTestEnvGuard { _guard: guard }
}

pub(super) fn set_test_env(key: &str, value: &str) {
    env::set_var(key, value);
}

pub(super) fn unique_temp_dir(prefix: &str) -> PathBuf {
    env::temp_dir().join(format!(
        "{prefix}-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ))
}

pub(super) fn unique_workspace_dir(prefix: &str) -> PathBuf {
    env::current_dir().unwrap().join(format!(
        ".{prefix}-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ))
}

pub(super) fn json_path(path: &Path) -> String {
    path.to_string_lossy()
        .replace('\\', "\\\\")
        .replace('"', "\\\"")
}