cgx-engine 0.4.1

Core engine for cgx — Tree-sitter parsing, DuckDB graph storage, git analysis, and clustering
Documentation
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};

use cgx_engine::{
    export_dot, export_graphml, export_json, export_mermaid, export_svg, Edge, GraphDb, Node,
};

static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);

fn temp_dir() -> PathBuf {
    let count = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
    let dir =
        std::env::temp_dir().join(format!("cgx-export-test-{}-{}", std::process::id(), count));
    std::fs::create_dir_all(&dir).expect("failed to create test dir");
    std::fs::write(dir.join("dummy.txt"), "test").expect("failed to write dummy file");
    dir
}

fn seed_graph(db: &GraphDb) {
    let nodes = vec![
        Node {
            id: "fn:src/auth.ts:login".to_string(),
            kind: "Function".to_string(),
            name: "login".to_string(),
            path: "src/auth.ts".to_string(),
            line_start: 1,
            line_end: 5,
            language: "typescript".to_string(),
            churn: 0.8,
            coupling: 0.5,
            community: 1,
            in_degree: 2,
            out_degree: 1,
            ..Default::default()
        },
        Node {
            id: "cls:src/auth.ts:AuthService".to_string(),
            kind: "Class".to_string(),
            name: "AuthService".to_string(),
            path: "src/auth.ts".to_string(),
            line_start: 3,
            line_end: 20,
            language: "typescript".to_string(),
            churn: 0.3,
            coupling: 0.7,
            community: 1,
            in_degree: 1,
            out_degree: 0,
            ..Default::default()
        },
        Node {
            id: "fn:src/db.ts:query".to_string(),
            kind: "Function".to_string(),
            name: "query".to_string(),
            path: "src/db.ts".to_string(),
            line_start: 1,
            line_end: 3,
            language: "typescript".to_string(),
            churn: 0.0,
            coupling: 0.2,
            community: 2,
            in_degree: 0,
            out_degree: 0,
            ..Default::default()
        },
        Node {
            id: "file:src/auth.ts".to_string(),
            kind: "File".to_string(),
            name: "src/auth.ts".to_string(),
            path: "src/auth.ts".to_string(),
            line_start: 1,
            line_end: 1,
            language: "typescript".to_string(),
            churn: 0.8,
            coupling: 0.7,
            community: 1,
            in_degree: 0,
            out_degree: 0,
            ..Default::default()
        },
        Node {
            id: "file:src/db.ts".to_string(),
            kind: "File".to_string(),
            name: "src/db.ts".to_string(),
            path: "src/db.ts".to_string(),
            line_start: 1,
            line_end: 1,
            language: "typescript".to_string(),
            churn: 0.2,
            coupling: 0.0,
            community: 2,
            in_degree: 0,
            out_degree: 0,
            ..Default::default()
        },
    ];

    let edges = vec![
        Edge {
            id: "fn:src/auth.ts:login|CALLS|fn:src/db.ts:query".to_string(),
            src: "fn:src/auth.ts:login".to_string(),
            dst: "fn:src/db.ts:query".to_string(),
            kind: "CALLS".to_string(),
            weight: 1.0,
            confidence: 1.0,
        },
        Edge {
            id: "fn:src/auth.ts:login|CALLS|cls:src/auth.ts:AuthService".to_string(),
            src: "fn:src/auth.ts:login".to_string(),
            dst: "cls:src/auth.ts:AuthService".to_string(),
            kind: "CALLS".to_string(),
            weight: 0.5,
            confidence: 0.9,
        },
        Edge {
            id: "file:src/auth.ts|IMPORTS|file:src/db.ts".to_string(),
            src: "file:src/auth.ts".to_string(),
            dst: "file:src/db.ts".to_string(),
            kind: "IMPORTS".to_string(),
            weight: 1.0,
            confidence: 1.0,
        },
        Edge {
            id: "file:src/auth.ts|CO_CHANGES|file:src/db.ts".to_string(),
            src: "file:src/auth.ts".to_string(),
            dst: "file:src/db.ts".to_string(),
            kind: "CO_CHANGES".to_string(),
            weight: 0.6,
            confidence: 1.0,
        },
    ];

    db.upsert_nodes(&nodes).expect("upsert nodes failed");
    db.upsert_edges(&edges).expect("upsert edges failed");
}

#[test]
fn test_export_json_valid_structure() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");
    seed_graph(&db);

    let json = export_json(&db).expect("json export failed");
    let data: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");

    assert!(data.get("meta").is_some(), "missing meta");
    assert!(data.get("nodes").is_some(), "missing nodes");
    assert!(data.get("edges").is_some(), "missing edges");
    assert!(data.get("communities").is_some(), "missing communities");

    let meta = &data["meta"];
    assert!(meta.get("repo_id").is_some());
    assert!(meta.get("indexed_at").is_some());
    assert!(meta.get("node_count").and_then(|v| v.as_u64()).unwrap_or(0) >= 5);
    assert!(meta.get("edge_count").and_then(|v| v.as_u64()).unwrap_or(0) >= 4);

    let nodes = data["nodes"].as_array().expect("nodes should be an array");
    assert!(nodes.len() >= 5);
    let first_node = &nodes[0];
    assert!(first_node.get("id").is_some());
    assert!(first_node.get("kind").is_some());
    assert!(first_node.get("name").is_some());
    assert!(first_node.get("path").is_some());
    assert!(first_node.get("churn").is_some());
    assert!(first_node.get("coupling").is_some());
    assert!(first_node.get("community").is_some());

    let communities = data["communities"]
        .as_array()
        .expect("communities should be an array");
    assert!(!communities.is_empty());

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_json_round_trip_edges() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");
    seed_graph(&db);

    let json = export_json(&db).expect("json export failed");
    let data: serde_json::Value = serde_json::from_str(&json).expect("json should parse");

    let node_ids: std::collections::HashSet<&str> = data["nodes"]
        .as_array()
        .expect("nodes should be an array")
        .iter()
        .filter_map(|n| n["id"].as_str())
        .collect();

    let edges = data["edges"].as_array().expect("edges should be an array");
    for edge in edges {
        let src = edge["src"].as_str().expect("edge src should be a string");
        let dst = edge["dst"].as_str().expect("edge dst should be a string");
        assert!(
            node_ids.contains(src),
            "edge source '{}' not found in nodes",
            src
        );
        assert!(
            node_ids.contains(dst),
            "edge destination '{}' not found in nodes",
            dst
        );
    }

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_mermaid_valid_syntax() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");
    seed_graph(&db);

    let mermaid = export_mermaid(&db, 100).expect("mermaid export failed");
    assert!(
        mermaid.starts_with("graph TD"),
        "mermaid should start with 'graph TD', got: {}",
        &mermaid[..30]
    );
    assert!(mermaid.contains("-->"), "mermaid should contain arrows");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_mermaid_respects_max_nodes() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");
    seed_graph(&db);

    let mermaid = export_mermaid(&db, 2).expect("mermaid export failed");
    let node_lines: Vec<&str> = mermaid.lines().filter(|l| l.contains("[\"")).collect();
    assert!(
        node_lines.len() <= 2,
        "should cap at 2 nodes, got {}",
        node_lines.len()
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_mermaid_empty_graph() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");

    let mermaid = export_mermaid(&db, 100).expect("mermaid export failed");
    assert!(
        mermaid.contains("No data"),
        "empty graph should show 'No data'"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_dot_valid_syntax() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");
    seed_graph(&db);

    let dot = export_dot(&db).expect("dot export failed");
    assert!(
        dot.starts_with("digraph"),
        "dot should start with 'digraph'"
    );
    assert!(dot.contains("->"), "dot should contain arrows");
    assert!(dot.ends_with("}\n"), "dot should end with closing brace");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_dot_empty_graph() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");

    let dot = export_dot(&db).expect("dot export failed");
    assert!(
        dot.contains("No data"),
        "empty graph should indicate no data"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_svg_valid_xml() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");
    seed_graph(&db);

    let svg = export_svg(&db).expect("svg export failed");
    assert!(svg.starts_with("<svg"), "svg should start with '<svg'");
    assert!(svg.contains("</svg>"), "svg should close with '</svg>'");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_svg_empty_graph() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");

    let svg = export_svg(&db).expect("svg export failed");
    assert!(
        svg.contains("No data") || svg.contains("0 nodes"),
        "should show empty state"
    );

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_graphml_valid_structure() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");
    seed_graph(&db);

    let graphml = export_graphml(&db).expect("graphml export failed");
    assert!(
        graphml.starts_with("<?xml"),
        "GraphML should start with XML declaration"
    );
    assert!(
        graphml.contains("<graphml"),
        "should contain graphml element"
    );
    assert!(graphml.contains("<node"), "should contain nodes");
    assert!(graphml.contains("<edge"), "should contain edges");
    assert!(graphml.contains("</graphml>"), "should close graphml");

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_all_exports_succeed_on_empty_db() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");

    let json = export_json(&db).expect("json failed");
    assert!(json.contains("\"nodes\""));

    let mermaid = export_mermaid(&db, 100).expect("mermaid failed");
    assert!(!mermaid.is_empty());

    let dot = export_dot(&db).expect("dot failed");
    assert!(!dot.is_empty());

    let svg = export_svg(&db).expect("svg failed");
    assert!(!svg.is_empty());

    let graphml = export_graphml(&db).expect("graphml failed");
    assert!(!graphml.is_empty());

    let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn test_export_json_includes_all_kinds() {
    let dir = temp_dir();
    let db = GraphDb::open(&dir).expect("failed to open db");
    seed_graph(&db);

    let json = export_json(&db).expect("json export failed");
    let data: serde_json::Value = serde_json::from_str(&json).expect("json should parse");

    let kinds: std::collections::HashSet<&str> = data["nodes"]
        .as_array()
        .expect("nodes should be an array")
        .iter()
        .filter_map(|n| n["kind"].as_str())
        .collect();

    assert!(kinds.contains("Function"), "should have Function nodes");
    assert!(kinds.contains("Class"), "should have Class nodes");
    assert!(kinds.contains("File"), "should have File nodes");

    let edge_kinds: std::collections::HashSet<&str> = data["edges"]
        .as_array()
        .expect("edges should be an array")
        .iter()
        .filter_map(|e| e["kind"].as_str())
        .collect();

    assert!(edge_kinds.contains("CALLS"));
    assert!(edge_kinds.contains("IMPORTS"));
    assert!(edge_kinds.contains("CO_CHANGES"));

    let _ = std::fs::remove_dir_all(&dir);
}