use arrow::array::{Array, RecordBatch, StringArray};
use arrow::compute::cast;
use arrow::datatypes::DataType;
use nusy_codegraph::ingest_pipeline::{ingest_workspace, verify_graph};
use nusy_codegraph::schema::{CodeNode, CodeNodeKind, build_code_nodes_batch};
use nusy_codegraph::search::{CodeSearch, search_nodes};
use std::path::PathBuf;
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.to_path_buf()
}
#[test]
fn phase1_ingest_workspace() {
let result = ingest_workspace(&workspace_root());
assert!(result.total_nodes() > 100, "should have many nodes");
assert!(result.total_edges() > 100, "should have many edges");
assert_eq!(result.total_errors(), 0, "should parse without errors");
let summary = result.summary();
assert!(summary.contains("CodeNodes:"), "summary should show nodes");
eprintln!("{summary}");
}
#[test]
fn phase2_query_functions_by_file() {
let result = ingest_workspace(&workspace_root());
let nodes = result.merged_nodes_batch().expect("nodes batch");
let q = CodeSearch {
file_prefix: Some("crates/nusy-codegraph/src/search.rs".into()),
kind: Some(CodeNodeKind::RustFn),
..Default::default()
};
let found = search_nodes(&nodes, &q);
assert!(
!found.nodes.is_empty(),
"should find functions in search.rs"
);
eprintln!(
"Phase 2: found {} functions in search.rs",
found.nodes.len()
);
}
#[test]
fn phase2_query_test_coverage() {
let result = ingest_workspace(&workspace_root());
let nodes = result.merged_nodes_batch().expect("nodes");
let edges = result.merged_edges_batch().expect("edges");
let edge_preds = cast_col(&edges, 2);
let test_count = (0..edge_preds.len())
.filter(|&i| {
let p = edge_preds.value(i);
p == "test_targets" || p == "tests" || p.contains("test")
})
.count();
eprintln!("Phase 2: {} test-related edges", test_count);
}
#[test]
fn phase2_graph_coherence() {
let result = ingest_workspace(&workspace_root());
let nodes = result.merged_nodes_batch().expect("nodes");
let edges = result.merged_edges_batch().expect("edges");
let violations = verify_graph(&nodes, &edges);
eprintln!(
"Phase 2: coherence — {} duplicates, {} dangling sources, {} dangling targets",
violations.duplicate_node_ids.len(),
violations.dangling_sources.len(),
violations.dangling_targets.len()
);
}
#[test]
fn phase4_hash_embedding_search() {
use nusy_codegraph::embeddings::HashEmbeddingProvider;
use nusy_codegraph::{EmbeddingProvider, cosine_similarity};
let provider = HashEmbeddingProvider;
let v1 = provider.embed("search_nodes function").unwrap();
let v2 = provider.embed("search_nodes function").unwrap();
let v3 = provider.embed("completely unrelated topic").unwrap();
let sim_same = cosine_similarity(&v1, &v2);
assert!(
(sim_same - 1.0).abs() < 0.01,
"identical text should have sim ~1.0, got {sim_same}"
);
let sim_diff = cosine_similarity(&v1, &v3);
assert!(sim_diff < sim_same, "different text should have lower sim");
eprintln!("Phase 4: same={sim_same:.3}, diff={sim_diff:.3}");
}
#[test]
fn phase5_modify_and_revert() {
let original = CodeNode {
id: "rust_fn:test::add".into(),
kind: CodeNodeKind::RustFn,
name: "add".into(),
body: Some("pub fn add(a: i64, b: i64) -> i64 { a + b }".into()),
..Default::default()
};
let nodes = build_code_nodes_batch(&[original.clone()]).expect("nodes");
let update = nusy_codegraph::mcp_tools::NodeUpdate {
body: Some("pub fn add(a: i64, b: i64) -> i64 { a + b + 1 }".into()),
..Default::default()
};
let modified =
nusy_codegraph::mcp_tools::codegraph_update_object(&nodes, "rust_fn:test::add", &update)
.expect("update");
assert_eq!(modified.num_rows(), 1);
let revert = nusy_codegraph::mcp_tools::NodeUpdate {
body: Some("pub fn add(a: i64, b: i64) -> i64 { a + b }".into()),
..Default::default()
};
let reverted =
nusy_codegraph::mcp_tools::codegraph_update_object(&modified, "rust_fn:test::add", &revert)
.expect("revert");
assert_eq!(reverted.num_rows(), 1);
eprintln!("Phase 5: modify → revert — PASS");
}
#[test]
fn phase6_concurrent_graph_access() {
use std::sync::{Arc, RwLock};
let func_a = CodeNode {
id: "rust_fn:shared::alpha".into(),
kind: CodeNodeKind::RustFn,
name: "alpha".into(),
body: Some("fn alpha() {}".into()),
..Default::default()
};
let nodes = build_code_nodes_batch(&[func_a]).expect("nodes");
let shared = Arc::new(RwLock::new(nodes));
let shared_a = Arc::clone(&shared);
let handle_a = std::thread::spawn(move || {
let current = shared_a.read().unwrap().clone();
let mut all_nodes = extract_nodes(¤t);
all_nodes.push(CodeNode {
id: "rust_fn:shared::beta".into(),
kind: CodeNodeKind::RustFn,
name: "beta".into(),
body: Some("fn beta() {}".into()),
..Default::default()
});
let new_batch = build_code_nodes_batch(&all_nodes).expect("rebuild");
*shared_a.write().unwrap() = new_batch;
});
handle_a.join().expect("agent A");
let final_batch = shared.read().unwrap().clone();
let q = CodeSearch {
name_pattern: Some("beta".into()),
..Default::default()
};
let found = search_nodes(&final_batch, &q);
assert_eq!(
found.nodes.len(),
1,
"Agent B should see Agent A's function"
);
assert_eq!(found.nodes[0].name, "beta");
eprintln!("Phase 6: Agent A added beta, Agent B found it — PASS");
}
#[test]
fn phase7_edge_predicate_distribution() {
let result = ingest_workspace(&workspace_root());
let edges = result.merged_edges_batch().expect("edges");
let preds = cast_col(&edges, 2);
let mut counts = std::collections::HashMap::new();
for i in 0..preds.len() {
*counts.entry(preds.value(i).to_string()).or_insert(0u32) += 1;
}
assert!(
counts.contains_key("contains"),
"should have contains edges"
);
assert!(counts.contains_key("imports"), "should have import edges");
let mut sorted: Vec<_> = counts.iter().collect();
sorted.sort_by(|a, b| b.1.cmp(a.1));
eprintln!("Phase 7: Edge distribution:");
for (pred, count) in &sorted {
eprintln!(" {pred}: {count}");
}
}
fn cast_col(batch: &RecordBatch, idx: usize) -> StringArray {
let col = batch.column(idx);
if col.data_type() == &DataType::Utf8 {
col.as_any().downcast_ref::<StringArray>().unwrap().clone()
} else {
let casted = cast(col, &DataType::Utf8).expect("cast");
casted
.as_any()
.downcast_ref::<StringArray>()
.unwrap()
.clone()
}
}
fn extract_nodes(batch: &RecordBatch) -> Vec<CodeNode> {
let q = CodeSearch::default();
search_nodes(batch, &q).nodes
}