use leankg::db::models::CodeElement;
use leankg::graph::l1_cache::CachingGraphEngine;
use leankg::graph::GraphEngine;
use serde_json::json;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
fn tmp_engine(name: &str) -> (PathBuf, GraphEngine) {
let dir = std::env::temp_dir().join(format!("leankg_l1_{}_{}", name, std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let db = leankg::db::schema::init_db(&dir).expect("init_db");
(dir, GraphEngine::new(db))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cge_clone_is_cheap_and_shares_caches() {
let (_dir, engine) = tmp_engine("clone");
let a = CachingGraphEngine::new(engine.clone());
let b = a.clone();
let _ = (a, b);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cge_invalidate_is_idempotent_and_safe() {
let (_dir, engine) = tmp_engine("invalidate");
let cache = CachingGraphEngine::new(engine);
cache.invalidate().await;
cache.invalidate().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cge_inner_returns_engine_reference() {
let (_dir, engine) = tmp_engine("inner");
let cache = CachingGraphEngine::new(engine.clone());
let inner = cache.inner();
let count = inner.count_code_elements().unwrap_or(0);
let _ = count;
}
#[test]
fn write_tools_excludes_reads() {
let reads = [
"search_code",
"find_function",
"get_context",
"get_dependencies",
"get_dependents",
"get_call_graph",
"find_large_functions",
"get_tested_by",
"get_impact_radius",
"query_file",
"mcp_status",
];
let writes = [
"mcp_init",
"mcp_index",
"mcp_index_docs",
"add_knowledge",
"update_knowledge",
"delete_knowledge",
"add_annotation",
"link_element",
"add_documentation",
"promote_environment",
"embed_control",
"ontology_control",
];
for r in reads {
assert!(
!writes.contains(&r),
"read tool '{}' was misclassified as write",
r
);
}
for w in writes {
assert!(
!reads.contains(&w),
"write tool '{}' was misclassified as read",
w
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cge_search_code_on_empty_engine_returns_empty_results() {
let (_dir, engine) = tmp_engine("empty_search");
let cache = CachingGraphEngine::new(engine);
let v = cache
.search_code("anything", None, "local", 10, 0)
.await
.expect("search_code");
assert!(v.is_object(), "expected JSON object, got {:?}", v);
let obj = v.as_object().unwrap();
assert!(
obj.contains_key("results") || obj.contains_key("files") || obj.contains_key("count"),
"empty-engine search result missing expected keys: {:?}",
obj.keys().collect::<Vec<_>>()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cge_get_dependencies_on_empty_engine_returns_empty() {
let (_dir, engine) = tmp_engine("empty_deps");
let cache = CachingGraphEngine::new(engine);
let v = cache
.get_dependencies("src/does_not_exist.rs")
.await
.expect("get_dependencies");
let obj = v.as_object().expect("object");
let deps = obj
.get("dependencies")
.and_then(|d| d.as_array())
.expect("array");
assert!(deps.is_empty(), "expected empty deps, got {:?}", deps);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cge_find_function_on_empty_engine_returns_empty_functions() {
let (_dir, engine) = tmp_engine("empty_fn");
let cache = CachingGraphEngine::new(engine);
let v = cache.find_function("missing").await.expect("find_function");
let obj = v.as_object().expect("object");
let fns = obj
.get("functions")
.and_then(|f| f.as_array())
.expect("array");
assert!(fns.is_empty(), "expected empty functions, got {:?}", fns);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cge_get_call_graph_on_empty_engine_returns_empty_calls() {
let (_dir, engine) = tmp_engine("empty_cg");
let cache = CachingGraphEngine::new(engine);
let v = cache
.get_call_graph("missing_fn", 2, 30)
.await
.expect("get_call_graph");
let obj = v.as_object().expect("object");
let calls = obj.get("calls").and_then(|c| c.as_array()).expect("array");
assert!(calls.is_empty(), "expected empty calls, got {:?}", calls);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cge_find_large_functions_on_empty_engine_returns_empty() {
let (_dir, engine) = tmp_engine("empty_large");
let cache = CachingGraphEngine::new(engine);
let v = cache
.find_large_functions(50, 10, 0)
.await
.expect("find_large_functions");
let obj = v.as_object().expect("object");
let large = obj
.get("large_functions")
.and_then(|l| l.as_array())
.expect("array");
assert!(
large.is_empty(),
"expected empty large_functions, got {:?}",
large
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cge_get_tested_by_on_empty_engine_returns_empty() {
let (_dir, engine) = tmp_engine("empty_tested");
let cache = CachingGraphEngine::new(engine);
let v = cache
.get_tested_by("src/does_not_exist.rs")
.await
.expect("get_tested_by");
let obj = v.as_object().expect("object");
let tests = obj.get("tests").and_then(|t| t.as_array()).expect("array");
assert!(tests.is_empty(), "expected empty tests, got {:?}", tests);
}
#[test]
fn dispatch_cache_key_is_order_stable() {
use std::collections::BTreeMap;
let mut a: BTreeMap<String, serde_json::Value> = BTreeMap::new();
a.insert("query".into(), json!("Handler"));
a.insert("limit".into(), json!(20));
let mut b: BTreeMap<String, serde_json::Value> = BTreeMap::new();
b.insert("limit".into(), json!(20));
b.insert("query".into(), json!("Handler"));
let sa = serde_json::to_string(&a).unwrap();
let sb = serde_json::to_string(&b).unwrap();
assert_eq!(sa, sb, "BTreeMap iteration order should be stable");
}
#[test]
fn apply_pagination_on_empty_results_is_well_formed() {
let mut obj = serde_json::Map::new();
obj.insert("results".into(), json!([]));
obj.insert("total".into(), json!(0));
let v = serde_json::Value::Object(obj);
let mut out = v.clone();
if let Some(map) = out.as_object_mut() {
if let Some(arr) = map.get("results").and_then(|v| v.as_array()).cloned() {
let total = arr.len();
let limit = 10usize;
let offset = 0usize;
let sliced: Vec<_> = arr.into_iter().skip(offset).take(limit).collect();
map.insert("results".into(), serde_json::Value::Array(sliced.clone()));
map.insert("count".into(), json!(sliced.len()));
map.insert("limit".into(), json!(limit));
map.insert("offset".into(), json!(offset));
map.insert("total".into(), json!(total));
map.insert("has_more".into(), json!(offset + sliced.len() < total));
}
}
assert_eq!(out["count"], json!(0));
assert_eq!(out["has_more"], json!(false));
}
#[test]
fn atomic_counter_smoke() {
let c = AtomicUsize::new(0);
c.fetch_add(1, Ordering::SeqCst);
c.fetch_add(1, Ordering::SeqCst);
assert_eq!(c.load(Ordering::SeqCst), 2);
}
#[test]
fn codeelement_type_path_is_stable() {
fn _takes(_e: &CodeElement) {}
let _f: fn(&CodeElement) = _takes;
}