use std::path::Path;
use std::process::Command;
use std::sync::Mutex;
static KUZU_LOCK: Mutex<()> = Mutex::new(());
fn commit_files(dir: &Path, fixtures: &[&str]) {
for name in fixtures {
let src = Path::new(FIXTURES).join(name);
std::fs::copy(&src, dir.join(name)).expect("copy fixture");
}
let names: Vec<&str> = fixtures.to_vec();
let mut add_args = vec!["add"];
add_args.extend_from_slice(&names);
let status = Command::new("git")
.args(&add_args)
.current_dir(dir)
.status()
.expect("git add failed");
assert!(status.success(), "git add failed");
let status = Command::new("git")
.args(["commit", "-m", "add fixtures"])
.current_dir(dir)
.status()
.expect("git commit failed");
assert!(status.success(), "git commit failed");
}
fn run_pipeline_multi(
fixtures: &[&str],
) -> (
Vec<gitcortex_core::graph::Node>,
Vec<gitcortex_core::graph::Edge>,
KuzuGraphStore,
) {
let _lock = KUZU_LOCK.lock().expect("lock");
let tmp = tempfile::tempdir().expect("tempdir");
init_repo(tmp.path());
commit_files(tmp.path(), fixtures);
let indexer = IncrementalIndexer::new(tmp.path()).expect("indexer");
let (diff, head_sha) = indexer.run(None).expect("indexer.run");
let mut store = KuzuGraphStore::open(tmp.path()).expect("store");
store.apply_diff("main", &diff).expect("apply_diff");
store
.set_last_indexed_sha("main", &head_sha)
.expect("set sha");
let nodes = store.list_all_nodes("main").expect("list_all_nodes");
let edges = store.list_all_edges("main").expect("list_all_edges");
(nodes, edges, store)
}
use gitcortex_core::store::GraphStore;
use gitcortex_indexer::IncrementalIndexer;
use gitcortex_store::kuzu::KuzuGraphStore;
const FIXTURES: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/integration/fixtures"
);
fn init_repo(dir: &Path) {
for args in [
vec!["init"],
vec!["config", "user.email", "test@test.com"],
vec!["config", "user.name", "Test"],
] {
let status = Command::new("git")
.args(&args)
.current_dir(dir)
.status()
.expect("git failed");
assert!(status.success(), "git {args:?} failed");
}
}
fn commit_file(dir: &Path, src: &Path, dest_name: &str) {
let dest = dir.join(dest_name);
std::fs::copy(src, &dest).expect("copy fixture");
for args in [vec!["add", dest_name], vec!["commit", "-m", "add fixture"]] {
let status = Command::new("git")
.args(&args)
.current_dir(dir)
.status()
.expect("git failed");
assert!(status.success(), "git {args:?} failed");
}
}
fn run_pipeline(
fixture: &str,
) -> (
Vec<gitcortex_core::graph::Node>,
Vec<gitcortex_core::graph::Edge>,
) {
let _lock = KUZU_LOCK.lock().expect("lock");
let tmp = tempfile::tempdir().expect("tempdir");
init_repo(tmp.path());
commit_file(
tmp.path(),
Path::new(FIXTURES).join(fixture).as_path(),
fixture,
);
let indexer = IncrementalIndexer::new(tmp.path()).expect("indexer");
let (diff, head_sha) = indexer.run(None).expect("indexer.run");
let mut store = KuzuGraphStore::open(tmp.path()).expect("store");
store.apply_diff("main", &diff).expect("apply_diff");
store
.set_last_indexed_sha("main", &head_sha)
.expect("set sha");
let nodes = store.list_all_nodes("main").expect("list_all_nodes");
let edges = store.list_all_edges("main").expect("list_all_edges");
(nodes, edges)
}
#[test]
fn rust_fixture_indexes_nodes_and_edges() {
let (nodes, edges) = run_pipeline("sample.rs");
assert!(!nodes.is_empty(), "expected nodes for sample.rs");
assert!(!edges.is_empty(), "expected edges for sample.rs");
let names: Vec<_> = nodes.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"Hello"), "expected struct Hello");
assert!(names.contains(&"Greeter"), "expected trait Greeter");
assert!(
names.contains(&"make_greeting"),
"expected fn make_greeting"
);
}
#[test]
fn python_fixture_indexes_nodes_and_edges() {
let (nodes, edges) = run_pipeline("sample.py");
assert!(!nodes.is_empty(), "expected nodes for sample.py");
assert!(!edges.is_empty(), "expected edges for sample.py");
let names: Vec<_> = nodes.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"Greeter"), "expected class Greeter");
assert!(
names.contains(&"make_greeting"),
"expected fn make_greeting"
);
}
#[test]
fn typescript_fixture_indexes_nodes_and_edges() {
let (nodes, edges) = run_pipeline("sample.ts");
assert!(!nodes.is_empty(), "expected nodes for sample.ts");
assert!(!edges.is_empty(), "expected edges for sample.ts");
let names: Vec<_> = nodes.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"Greeter"), "expected interface Greeter");
assert!(names.contains(&"Hello"), "expected class Hello");
assert!(names.contains(&"makeGreeting"), "expected fn makeGreeting");
}
#[test]
fn go_fixture_indexes_nodes_and_edges() {
let (nodes, edges) = run_pipeline("sample.go");
assert!(!nodes.is_empty(), "expected nodes for sample.go");
assert!(!edges.is_empty(), "expected edges for sample.go");
let names: Vec<_> = nodes.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"Greeter"), "expected interface Greeter");
assert!(names.contains(&"Hello"), "expected struct Hello");
assert!(names.contains(&"MakeGreeting"), "expected fn MakeGreeting");
}
#[test]
fn java_fixture_indexes_nodes_and_edges() {
let (nodes, edges) = run_pipeline("sample.java");
assert!(!nodes.is_empty(), "expected nodes for sample.java");
assert!(!edges.is_empty(), "expected edges for sample.java");
let names: Vec<_> = nodes.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"Greeter"), "expected interface Greeter");
assert!(names.contains(&"Hello"), "expected class Hello");
assert!(names.contains(&"makeGreeting"), "expected fn makeGreeting");
}
use gitcortex_core::schema::{EdgeKind, NodeKind};
fn run_python_comprehensive() -> (
Vec<gitcortex_core::graph::Node>,
Vec<gitcortex_core::graph::Edge>,
) {
run_pipeline("python_comprehensive.py")
}
#[test]
fn python_comprehensive_constants_are_indexed() {
let (nodes, _) = run_python_comprehensive();
let constants: Vec<_> = nodes
.iter()
.filter(|n| n.kind == NodeKind::Constant)
.collect();
let names: Vec<&str> = constants.iter().map(|n| n.name.as_str()).collect();
assert!(
names.contains(&"MAX_RETRIES"),
"expected Constant MAX_RETRIES, got: {names:?}"
);
assert!(
names.contains(&"DEFAULT_TIMEOUT"),
"expected Constant DEFAULT_TIMEOUT, got: {names:?}"
);
assert!(
names.contains(&"API_VERSION"),
"expected Constant API_VERSION, got: {names:?}"
);
}
#[test]
fn python_comprehensive_protocols_become_interfaces() {
let (nodes, _) = run_python_comprehensive();
let interfaces: Vec<_> = nodes
.iter()
.filter(|n| n.kind == NodeKind::Interface)
.collect();
let names: Vec<&str> = interfaces.iter().map(|n| n.name.as_str()).collect();
assert!(
names.contains(&"Serializable"),
"expected Interface Serializable, got: {names:?}"
);
assert!(
names.contains(&"Repository"),
"expected Interface Repository, got: {names:?}"
);
for iface in &interfaces {
assert!(
iface.metadata.is_abstract,
"Protocol node '{}' should have is_abstract=true",
iface.name
);
}
}
#[test]
fn python_comprehensive_plain_classes_are_structs() {
let (nodes, _) = run_python_comprehensive();
let structs: Vec<_> = nodes
.iter()
.filter(|n| n.kind == NodeKind::Struct)
.collect();
let names: Vec<&str> = structs.iter().map(|n| n.name.as_str()).collect();
for expected in &["BaseModel", "User", "AsyncService", "EventSystem"] {
assert!(
names.contains(expected),
"expected Struct {expected}, got: {names:?}"
);
}
}
#[test]
fn python_comprehensive_property_decorator() {
let (nodes, _) = run_python_comprehensive();
let props: Vec<_> = nodes
.iter()
.filter(|n| n.kind == NodeKind::Property)
.collect();
assert!(
props.iter().any(|n| n.name == "display_name"),
"expected Property 'display_name', got: {:?}",
props.iter().map(|n| &n.name).collect::<Vec<_>>()
);
let display_name = props.iter().find(|n| n.name == "display_name").unwrap();
assert!(
display_name.metadata.is_property,
"display_name should have is_property=true"
);
}
#[test]
fn python_comprehensive_staticmethod_and_classmethod() {
let (nodes, _) = run_python_comprehensive();
let methods: Vec<_> = nodes
.iter()
.filter(|n| n.kind == NodeKind::Method)
.collect();
let from_dict = methods.iter().find(|n| n.name == "from_dict");
let anonymous = methods.iter().find(|n| n.name == "anonymous");
assert!(from_dict.is_some(), "expected method 'from_dict'");
assert!(anonymous.is_some(), "expected method 'anonymous'");
assert!(
from_dict.unwrap().metadata.is_static,
"from_dict (@staticmethod) should have is_static=true"
);
assert!(
anonymous.unwrap().metadata.is_static,
"anonymous (@classmethod) should have is_static=true"
);
}
#[test]
fn python_comprehensive_async_methods_flagged() {
let (nodes, _) = run_python_comprehensive();
let methods: Vec<_> = nodes
.iter()
.filter(|n| n.kind == NodeKind::Method)
.collect();
let fetch = methods.iter().find(|n| n.name == "fetch_user");
let save = methods.iter().find(|n| n.name == "save_user");
assert!(fetch.is_some(), "expected method 'fetch_user'");
assert!(save.is_some(), "expected method 'save_user'");
assert!(
fetch.unwrap().metadata.is_async,
"fetch_user should have is_async=true"
);
assert!(
save.unwrap().metadata.is_async,
"save_user should have is_async=true"
);
}
#[test]
fn python_comprehensive_generator_function_flagged() {
let (nodes, _) = run_python_comprehensive();
let fns: Vec<_> = nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
let user_stream = fns.iter().find(|n| n.name == "user_stream");
assert!(user_stream.is_some(), "expected function 'user_stream'");
assert!(
user_stream.unwrap().metadata.is_generator,
"user_stream should have is_generator=true"
);
}
#[test]
fn python_comprehensive_async_generator_flagged() {
let (nodes, _) = run_python_comprehensive();
let fns: Vec<_> = nodes
.iter()
.filter(|n| n.kind == NodeKind::Function)
.collect();
let async_stream = fns.iter().find(|n| n.name == "async_user_stream");
assert!(
async_stream.is_some(),
"expected function 'async_user_stream'"
);
let f = async_stream.unwrap();
assert!(
f.metadata.is_async,
"async_user_stream should have is_async=true"
);
assert!(
f.metadata.is_generator,
"async_user_stream should have is_generator=true"
);
}
#[test]
fn python_comprehensive_nested_classes_indexed() {
let (nodes, edges) = run_python_comprehensive();
let names: Vec<&str> = nodes.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"Event"), "expected nested class 'Event'");
assert!(
names.contains(&"Handler"),
"expected nested class 'Handler'"
);
let event_sys = nodes.iter().find(|n| n.name == "EventSystem").unwrap();
let event_cls = nodes.iter().find(|n| n.name == "Event").unwrap();
let handler_cls = nodes.iter().find(|n| n.name == "Handler").unwrap();
let contains: Vec<_> = edges
.iter()
.filter(|e| e.kind == EdgeKind::Contains)
.collect();
assert!(
contains
.iter()
.any(|e| e.src == event_sys.id && e.dst == event_cls.id),
"expected Contains edge EventSystem → Event"
);
assert!(
contains
.iter()
.any(|e| e.src == event_sys.id && e.dst == handler_cls.id),
"expected Contains edge EventSystem → Handler"
);
}
#[test]
fn python_comprehensive_call_edges_recorded() {
let (nodes, edges) = run_python_comprehensive();
let process = nodes.iter().find(|n| n.name == "process_pipeline").unwrap();
let user_stream = nodes.iter().find(|n| n.name == "user_stream").unwrap();
let create_user = nodes.iter().find(|n| n.name == "create_user").unwrap();
let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
assert!(
calls
.iter()
.any(|e| e.src == process.id && e.dst == user_stream.id),
"expected Calls edge process_pipeline → user_stream"
);
assert!(
calls
.iter()
.any(|e| e.src == process.id && e.dst == create_user.id),
"expected Calls edge process_pipeline → create_user"
);
}
#[test]
fn python_comprehensive_inheritance_edges_present() {
let (_, edges) = run_python_comprehensive();
let implements: Vec<_> = edges
.iter()
.filter(|e| e.kind == EdgeKind::Implements)
.collect();
assert!(
!implements.is_empty(),
"expected at least one Implements edge (User extends BaseModel)"
);
}
#[test]
fn python_comprehensive_private_method_visibility() {
use gitcortex_core::schema::Visibility;
let (nodes, _) = run_python_comprehensive();
let internal = nodes.iter().find(|n| n.name == "_internal_check");
assert!(internal.is_some(), "expected method '_internal_check'");
assert_eq!(
internal.unwrap().metadata.visibility,
Visibility::Private,
"_internal_check should be Private"
);
}
#[test]
fn python_comprehensive_dataclass_is_struct() {
let (nodes, _) = run_python_comprehensive();
let user = nodes.iter().find(|n| n.name == "User");
assert!(user.is_some(), "expected class 'User'");
assert_eq!(
user.unwrap().kind,
NodeKind::Struct,
"@dataclass User should be Struct kind"
);
}
#[test]
fn cross_file_calls_edge_resolved() {
let (nodes, _edges, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
assert!(
nodes.iter().any(|n| n.name == "compute_value"),
"expected 'compute_value' node from xfile_callee.rs"
);
assert!(
nodes.iter().any(|n| n.name == "run"),
"expected 'run' node from xfile_caller.rs"
);
let callers = store
.find_callers("main", "compute_value")
.expect("find_callers");
assert!(
callers.iter().any(|n| n.name == "run"),
"expected 'run' as a caller of 'compute_value' across files; got: {:?}",
callers.iter().map(|n| &n.name).collect::<Vec<_>>()
);
}
#[test]
fn cross_file_calls_edge_multiple_callers() {
let (_nodes, _, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let callers = store
.find_callers("main", "compute_value")
.expect("find_callers");
let caller_names: Vec<&str> = callers.iter().map(|n| n.name.as_str()).collect();
assert!(
caller_names.contains(&"run"),
"expected 'run' in callers; got {caller_names:?}"
);
assert!(
caller_names.contains(&"run_with_branch"),
"expected 'run_with_branch' in callers; got {caller_names:?}"
);
}
#[test]
fn cross_file_implements_edge_resolved() {
let (nodes, edges, _store) = run_pipeline_multi(&["xfile_trait.rs", "xfile_impl.rs"]);
assert!(
nodes.iter().any(|n| n.name == "Processor"),
"expected 'Processor' trait from xfile_trait.rs"
);
assert!(
nodes.iter().any(|n| n.name == "Worker"),
"expected 'Worker' struct from xfile_impl.rs"
);
let impl_edges: Vec<_> = edges
.iter()
.filter(|e| e.kind == EdgeKind::Implements)
.collect();
assert!(
!impl_edges.is_empty(),
"expected at least one Implements edge (Worker → Processor)"
);
}
#[test]
fn module_dependencies_resolves_cross_file() {
let (_, _, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let deps = store
.module_dependencies("main", "xfile_caller")
.expect("module_dependencies");
let names: Vec<&str> = deps.iter().map(|n| n.name.as_str()).collect();
assert!(
names.contains(&"xfile_callee"),
"xfile_caller should depend on xfile_callee, got: {names:?}"
);
assert!(!names.contains(&"xfile_caller"), "self-dependency leaked");
}
#[test]
fn module_dependencies_unknown_module_is_empty() {
let (_, _, store) = run_pipeline_multi(&["sample.rs"]);
let deps = store
.module_dependencies("main", "no_such_module")
.expect("module_dependencies");
assert!(deps.is_empty());
}
#[test]
fn find_type_usages_finds_signature_references() {
let (_, _, store) = run_pipeline_multi(&["python_comprehensive.py"]);
let users = store
.find_type_usages("main", "User")
.expect("find_type_usages");
let names: Vec<&str> = users.iter().map(|n| n.name.as_str()).collect();
assert!(
!users.is_empty(),
"User is referenced in several signatures; expected non-empty usages"
);
assert!(
names.contains(&"create_user") || names.contains(&"find_users"),
"expected create_user or find_users among User's users, got: {names:?}"
);
}
#[test]
fn find_type_usages_unknown_type_is_empty() {
let (_, _, store) = run_pipeline_multi(&["sample.rs"]);
let users = store
.find_type_usages("main", "NoSuchTypeXyz")
.expect("find_type_usages");
assert!(users.is_empty());
}
#[test]
fn tour_no_seed_produces_component_summary() {
let (_, _, store) = run_pipeline_multi(&[
"xfile_callee.rs",
"xfile_caller.rs",
"xfile_trait.rs",
"xfile_impl.rs",
]);
let tour = gitcortex_mcp::mcp::tour::generate(&store, "main", None, Some(12)).expect("tour");
assert!(
!tour.components.is_empty(),
"no-seed tour should produce components"
);
let md = gitcortex_mcp::mcp::tour::render_markdown(&tour);
assert!(
md.contains("# Architecture") && md.contains("## Components"),
"markdown should lead with architecture summary:\n{md}"
);
}
#[test]
fn tour_seeded_has_no_components() {
let (_, _, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let tour =
gitcortex_mcp::mcp::tour::generate(&store, "main", Some("run"), Some(12)).expect("tour");
assert!(
tour.components.is_empty(),
"seeded tour should not compute components"
);
}
#[test]
fn edge_confidence_inferred_cross_file_extracted_structural() {
use gitcortex_core::schema::EdgeConfidence;
let (_, edges, _store) =
run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs", "sample.rs"]);
let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
assert!(
calls
.iter()
.any(|e| e.confidence == EdgeConfidence::Inferred),
"expected at least one Inferred (cross-file) calls edge"
);
let contains: Vec<_> = edges
.iter()
.filter(|e| e.kind == EdgeKind::Contains)
.collect();
assert!(!contains.is_empty(), "expected Contains edges");
assert!(
contains
.iter()
.all(|e| e.confidence == EdgeConfidence::Extracted),
"structural Contains edges must all be Extracted"
);
}
#[test]
fn get_call_sites_records_caller_and_line() {
let (_, _, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let sites = store
.find_call_sites("main", "compute_value")
.expect("find_call_sites");
let callers: Vec<&str> = sites.iter().map(|s| s.caller.name.as_str()).collect();
assert!(
callers.contains(&"run") && callers.contains(&"run_with_branch"),
"expected run and run_with_branch as callers, got: {callers:?}"
);
for s in &sites {
assert!(
s.line.is_some(),
"call site from {} missing a line number",
s.caller.name
);
}
}
#[test]
fn get_call_sites_unknown_function_is_empty() {
let (_, _, store) = run_pipeline_multi(&["sample.rs"]);
let sites = store
.find_call_sites("main", "no_such_fn")
.expect("find_call_sites");
assert!(sites.is_empty());
}
#[test]
fn find_importers_resolves_cross_file_rust_import() {
let (_, _, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let importers = store
.find_importers("main", "compute_value")
.expect("find_importers");
assert!(
!importers.is_empty(),
"compute_value should have at least one importer (xfile_caller module)"
);
assert!(
importers
.iter()
.any(|n| n.file.to_string_lossy().contains("xfile_caller")),
"expected an importer in xfile_caller.rs, got: {:?}",
importers
.iter()
.map(|n| n.file.display().to_string())
.collect::<Vec<_>>()
);
}
#[test]
fn find_importers_unknown_symbol_is_empty() {
let (_, _, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let importers = store
.find_importers("main", "NoSuchSymbolXyz")
.expect("find_importers");
assert!(importers.is_empty());
}
#[test]
fn rust_file_gets_module_node() {
let (nodes, _) = run_pipeline("sample.rs");
assert!(
nodes
.iter()
.any(|n| n.kind == NodeKind::Module && n.name == "sample"),
"expected a file-level module node named 'sample'"
);
}
#[test]
fn type_hierarchy_subtypes_from_trait() {
let (_, _, store) = run_pipeline_multi(&["xfile_trait.rs", "xfile_impl.rs"]);
let h = store
.type_hierarchy("main", "Processor")
.expect("type_hierarchy");
let sub_names: Vec<&str> = h.subtypes.iter().map(|n| n.name.as_str()).collect();
assert!(
sub_names.contains(&"Worker"),
"Processor subtypes should include Worker, got: {sub_names:?}"
);
}
#[test]
fn type_hierarchy_supertypes_from_impl() {
let (_, _, store) = run_pipeline_multi(&["xfile_trait.rs", "xfile_impl.rs"]);
let h = store
.type_hierarchy("main", "Worker")
.expect("type_hierarchy");
let super_names: Vec<&str> = h.supertypes.iter().map(|n| n.name.as_str()).collect();
assert!(
super_names.contains(&"Processor"),
"Worker supertypes should include Processor, got: {super_names:?}"
);
}
#[test]
fn type_hierarchy_unknown_type_is_empty() {
let (_, _, store) = run_pipeline_multi(&["sample.rs"]);
let h = store
.type_hierarchy("main", "NoSuchTypeXyz")
.expect("type_hierarchy");
assert!(h.supertypes.is_empty() && h.subtypes.is_empty());
}
#[test]
fn annotations_captured_as_metadata_for_external_decorators() {
let (nodes, _) = run_pipeline("python_comprehensive.py");
let user = nodes
.iter()
.find(|n| n.name == "User")
.expect("User class node");
assert!(
user.metadata.annotations.iter().any(|a| a == "dataclass"),
"User should carry the dataclass annotation, got: {:?}",
user.metadata.annotations
);
}
#[test]
fn ast_search_by_annotation_finds_decorated() {
use gitcortex_core::store::AttributeFilter;
let (_, _, store) = run_pipeline_multi(&["python_comprehensive.py"]);
let hits = store
.search_by_attributes(
"main",
&AttributeFilter {
annotation: Some("staticmethod".into()),
..Default::default()
},
50,
)
.expect("search_by_attributes");
let names: Vec<&str> = hits.iter().map(|n| n.name.as_str()).collect();
assert!(
names.contains(&"from_dict"),
"@staticmethod search should find from_dict, got: {names:?}"
);
for n in &hits {
assert!(
n.metadata
.annotations
.iter()
.any(|a| a.contains("staticmethod")),
"{} lacks staticmethod annotation",
n.name
);
}
}
#[test]
fn ast_search_async_methods_only() {
use gitcortex_core::store::AttributeFilter;
let (_, _, store) = run_pipeline_multi(&["python_comprehensive.py"]);
let filter = AttributeFilter {
kind: Some(NodeKind::Method),
is_async: Some(true),
..Default::default()
};
let hits = store
.search_by_attributes("main", &filter, 50)
.expect("search_by_attributes");
let names: Vec<&str> = hits.iter().map(|n| n.name.as_str()).collect();
assert!(
names.contains(&"fetch_user"),
"expected async method fetch_user, got: {names:?}"
);
for n in &hits {
assert_eq!(n.kind, NodeKind::Method);
assert!(n.metadata.is_async, "{} not async", n.name);
}
}
#[test]
fn ast_search_kind_filter_excludes_others() {
use gitcortex_core::store::AttributeFilter;
let (_, _, store) = run_pipeline_multi(&["sample.rs"]);
let filter = AttributeFilter {
kind: Some(NodeKind::Trait),
..Default::default()
};
let hits = store
.search_by_attributes("main", &filter, 50)
.expect("search_by_attributes");
assert!(!hits.is_empty(), "expected at least the Greeter trait");
for n in &hits {
assert_eq!(n.kind, NodeKind::Trait, "{} is not a trait", n.name);
}
}
#[test]
fn ast_search_complexity_lower_bound() {
use gitcortex_core::store::AttributeFilter;
let (_, _, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let at_least_2 = store
.search_by_attributes(
"main",
&AttributeFilter {
min_complexity: Some(2),
..Default::default()
},
50,
)
.expect("search");
assert!(
at_least_2.iter().any(|n| n.name == "run_with_branch"),
"complexity≥2 should include run_with_branch"
);
let at_least_3 = store
.search_by_attributes(
"main",
&AttributeFilter {
min_complexity: Some(3),
..Default::default()
},
50,
)
.expect("search");
assert!(
!at_least_3.iter().any(|n| n.name == "run_with_branch"),
"complexity≥3 should exclude run_with_branch (complexity 2)"
);
}
#[test]
fn graph_stats_totals_match_kind_sums() {
let (nodes, edges, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let stats = store.graph_stats("main").expect("graph_stats");
assert_eq!(stats.total_nodes as usize, nodes.len());
assert_eq!(stats.total_edges as usize, edges.len());
let node_sum: u64 = stats.nodes_by_kind.iter().map(|(_, c)| c).sum();
let edge_sum: u64 = stats.edges_by_kind.iter().map(|(_, c)| c).sum();
assert_eq!(node_sum, stats.total_nodes);
assert_eq!(edge_sum, stats.total_edges);
assert!(
stats
.nodes_by_kind
.iter()
.any(|(k, c)| k == "function" && *c >= 2),
"expected ≥2 function nodes, got: {:?}",
stats.nodes_by_kind
);
assert!(
stats
.edges_by_kind
.iter()
.any(|(k, c)| k == "calls" && *c >= 1),
"expected ≥1 calls edge, got: {:?}",
stats.edges_by_kind
);
}
#[test]
fn graph_stats_kind_counts_sorted_descending() {
let (_, _, store) = run_pipeline_multi(&["sample.py", "python_comprehensive.py"]);
let stats = store.graph_stats("main").expect("graph_stats");
for w in stats.nodes_by_kind.windows(2) {
assert!(
w[0].1 >= w[1].1,
"nodes_by_kind not sorted desc: {:?}",
stats.nodes_by_kind
);
}
}
#[test]
fn find_unused_symbols_returns_uncalled_nodes() {
let (_, _, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let unused = store
.find_unused_symbols("main", None)
.expect("find_unused_symbols");
let unused_names: Vec<&str> = unused.iter().map(|n| n.name.as_str()).collect();
assert!(
!unused_names.contains(&"compute_value"),
"compute_value is called by run() — must not appear in unused: {unused_names:?}"
);
assert!(
unused_names.contains(&"DataStore"),
"DataStore has no callers or uses — must appear in unused: {unused_names:?}"
);
}
#[test]
fn find_unused_symbols_kind_filter() {
let (_, _, store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let unused = store
.find_unused_symbols("main", Some(NodeKind::Function))
.expect("find_unused_symbols kind=function");
for node in &unused {
assert!(
matches!(node.kind, NodeKind::Function | NodeKind::Method),
"kind filter returned non-function: {node:?}"
);
}
}
#[test]
fn cyclomatic_complexity_simple_function() {
let (nodes, _) = run_pipeline("sample.rs");
let f = nodes.iter().find(|n| n.name == "make_greeting");
assert!(f.is_some(), "expected 'make_greeting'");
let complexity = f.unwrap().metadata.lld.complexity;
assert_eq!(
complexity,
Some(1),
"linear function should have complexity 1, got {complexity:?}"
);
}
#[test]
fn cyclomatic_complexity_branching_function() {
let (nodes, _, _store) = run_pipeline_multi(&["xfile_callee.rs", "xfile_caller.rs"]);
let f = nodes.iter().find(|n| n.name == "run_with_branch");
assert!(f.is_some(), "expected 'run_with_branch'");
let complexity = f.unwrap().metadata.lld.complexity;
assert_eq!(
complexity,
Some(2),
"function with one `if` should have complexity 2, got {complexity:?}"
);
}