use std::fs;
use std::path::Path;
use std::sync::Arc;
use tempfile::tempdir;
use scryer_db::{CodeGraphEdge, Project, ScryerDb, SymbolReference};
use scryer_engine::{
EngineService, ProjectEnginePool, RawScope, RawSymbol, ScmFallbackResolver, StackGraphEngine,
};
#[test]
fn test_minimal_engine() -> anyhow::Result<()> {
let mut engine = StackGraphEngine::new_rust()?;
let path1 = Path::new("src/lib.rs");
let content1 = "pub fn add(a: i32, b: i32) -> i32 { a + b }";
let file1 = engine.add_file(path1, content1)?;
let path2 = Path::new("src/main.rs");
let content2 = "fn main() { add(1, 2); }";
let file2 = engine.add_file(path2, content2)?;
let cancellation = stack_graphs::NoCancellation;
let paths_count1 = engine.precompute_file_paths(file1, &cancellation)?;
let paths_count2 = engine.precompute_file_paths(file2, &cancellation)?;
assert!(paths_count1 > 0);
assert!(paths_count2 > 0);
let resolved = engine.resolve_at_location(path2, 1, 13)?;
assert!(resolved.is_some());
let def = resolved.unwrap();
assert_eq!(def.symbol_name, "add");
assert_eq!(def.file_path, Path::new("src/lib.rs"));
Ok(())
}
#[tokio::test]
async fn test_cross_file_definition_resolution() -> anyhow::Result<()> {
let temp = tempdir()?;
let root = temp.path();
fs::create_dir_all(root.join("src"))?;
fs::write(
root.join("src/lib.rs"),
r#"pub fn calculate_total(price: i32, tax: i32) -> i32 {
price + tax
}
"#,
)?;
fs::write(
root.join("src/main.rs"),
r#"fn main() {
let total = calculate_total(100, 20);
}
"#,
)?;
let db = ScryerDb::new_in_memory().await?;
let project = {
let mut conn = db.lock().await;
Project::create()
.slug("test-cross-file".to_string())
.root_path(root.to_string_lossy().to_string())
.default_branch("main".to_string())
.is_active(true)
.created_at("2026-10-06T00:00:00Z".to_string())
.updated_at("2026-10-06T00:00:00Z".to_string())
.exec(&mut *conn)
.await?
};
let service = EngineService::new(db.clone());
let report = service.index_project(project.id, root).await?;
assert_eq!(report.added_files, 2);
assert!(report.total_symbols >= 2);
assert!(report.total_references >= 1);
assert!(report.total_edges >= 1);
let def = service
.resolve_definition(project.id, root, Path::new("src/main.rs"), 2, 18)
.await?;
assert!(def.is_some());
let def = def.unwrap();
assert_eq!(def.symbol_name, "calculate_total");
assert_eq!(def.file_path, Path::new("src/lib.rs"));
let mut conn = db.lock().await;
let refs = SymbolReference::filter(
SymbolReference::fields()
.project_id()
.eq(project.id)
.and(SymbolReference::fields().role().eq("call")),
)
.exec(&mut *conn)
.await?;
assert!(!refs.is_empty());
let edges = CodeGraphEdge::filter(
CodeGraphEdge::fields()
.project_id()
.eq(project.id)
.and(CodeGraphEdge::fields().edge_type().eq("calls")),
)
.exec(&mut *conn)
.await?;
assert!(!edges.is_empty());
Ok(())
}
#[tokio::test]
async fn test_index_file_updates_stack_graph() -> anyhow::Result<()> {
let temp = tempdir()?;
let root = temp.path();
fs::create_dir_all(root.join("src"))?;
fs::write(root.join("src/lib.rs"), "pub fn existing() {}\n")?;
fs::write(
root.join("src/main.rs"),
"fn main() {\n existing();\n}\n",
)?;
let db = ScryerDb::new_in_memory().await?;
let project = {
let mut conn = db.lock().await;
Project::create()
.slug("test-index-file".to_string())
.root_path(root.to_string_lossy().to_string())
.default_branch("main".to_string())
.is_active(true)
.created_at("2026-10-06T00:00:00Z".to_string())
.updated_at("2026-10-06T00:00:00Z".to_string())
.exec(&mut *conn)
.await?
};
let service = EngineService::new(db.clone());
service.index_project(project.id, root).await?;
fs::write(
root.join("src/util.rs"),
"pub fn helper(x: i32) -> i32 { x }\n",
)?;
fs::write(
root.join("src/main.rs"),
"mod util;\nfn main() {\n let t = helper(1);\n}\n",
)?;
service
.index_file(project.id, root, Path::new("src/util.rs"))
.await?;
service
.index_file(project.id, root, Path::new("src/main.rs"))
.await?;
let def = service
.resolve_definition(project.id, root, Path::new("src/main.rs"), 3, 14)
.await?
.expect("helper should resolve after watcher-driven index_file");
assert_eq!(def.symbol_name, "helper");
assert_eq!(def.file_path, Path::new("src/util.rs"));
Ok(())
}
#[test]
fn test_path_explosion_mitigation() -> anyhow::Result<()> {
let mut engine = StackGraphEngine::new_rust()?;
let file_a = "pub fn a() { b(); }";
let file_b = "pub fn b() { a(); }";
let ha = engine.add_file(Path::new("src/a.rs"), file_a)?;
let hb = engine.add_file(Path::new("src/b.rs"), file_b)?;
let cancellation = stack_graphs::NoCancellation;
engine.precompute_file_paths(ha, &cancellation)?;
engine.precompute_file_paths(hb, &cancellation)?;
let def_a = engine.resolve_at_location(Path::new("src/a.rs"), 1, 14)?;
assert!(def_a.is_some());
assert_eq!(def_a.unwrap().symbol_name, "b");
let def_b = engine.resolve_at_location(Path::new("src/b.rs"), 1, 14)?;
assert!(def_b.is_some());
assert_eq!(def_b.unwrap().symbol_name, "a");
Ok(())
}
#[tokio::test]
async fn test_multi_project_isolation() -> anyhow::Result<()> {
let pool = ProjectEnginePool::new(4)?;
let p1_lock = pool.get_or_create(1).await;
{
let mut eng = p1_lock.lock().await;
eng.add_file(Path::new("src/lib.rs"), "pub fn worker_one() {}")?;
eng.add_file(Path::new("src/main.rs"), "fn main() { worker_one(); }")?;
let cancellation = stack_graphs::NoCancellation;
eng.precompute_all_files(&cancellation)?;
}
let p2_lock = pool.get_or_create(2).await;
{
let mut eng = p2_lock.lock().await;
eng.add_file(Path::new("src/lib.rs"), "pub fn worker_two() {}")?;
eng.add_file(Path::new("src/main.rs"), "fn main() { worker_two(); }")?;
let cancellation = stack_graphs::NoCancellation;
eng.precompute_all_files(&cancellation)?;
}
{
let mut eng = p1_lock.lock().await;
let res = eng.resolve_at_location(Path::new("src/main.rs"), 1, 22)?;
assert!(res.is_some());
assert_eq!(res.unwrap().symbol_name, "worker_one");
}
{
let mut eng = p2_lock.lock().await;
let res = eng.resolve_at_location(Path::new("src/main.rs"), 1, 22)?;
assert!(res.is_some());
assert_eq!(res.unwrap().symbol_name, "worker_two");
}
let p1_again = pool.get_or_create(1).await;
assert!(Arc::ptr_eq(&p1_lock, &p1_again));
Ok(())
}
#[test]
fn test_tier2_scm_fallback() {
let scopes = vec![
RawScope {
local_id: 0,
parent_local_id: None,
scope_kind: "module".to_string(),
start_byte: 0,
end_byte: 200,
start_line: 1,
end_line: 15,
},
RawScope {
local_id: 1,
parent_local_id: Some(0),
scope_kind: "function".to_string(),
start_byte: 30,
end_byte: 120,
start_line: 3,
end_line: 9,
},
];
let symbols = vec![
RawSymbol {
scope_local_id: Some(0),
name: "GLOBAL_CFG".to_string(),
qualified_name: "crate::GLOBAL_CFG".to_string(),
kind: "const".to_string(),
visibility: "public".to_string(),
signature: "pub const GLOBAL_CFG: u32 = 10;".to_string(),
docstring: None,
start_byte: 5,
end_byte: 25,
start_line: 1,
end_line: 1,
},
RawSymbol {
scope_local_id: Some(1),
name: "local_helper".to_string(),
qualified_name: "crate::my_func::local_helper".to_string(),
kind: "fn".to_string(),
visibility: "private".to_string(),
signature: "fn local_helper()".to_string(),
docstring: None,
start_byte: 40,
end_byte: 70,
start_line: 4,
end_line: 6,
},
];
let res = ScmFallbackResolver::resolve_identifier(
"local_helper",
Path::new("src/lib.rs"),
5,
10,
&scopes,
&symbols,
);
assert!(res.is_some());
let def = res.unwrap();
assert_eq!(def.symbol_name, "local_helper");
assert_eq!(def.qualified_name, "crate::my_func::local_helper");
let res_global = ScmFallbackResolver::resolve_identifier(
"GLOBAL_CFG",
Path::new("src/lib.rs"),
7,
10,
&scopes,
&symbols,
);
assert!(res_global.is_some());
assert_eq!(res_global.unwrap().symbol_name, "GLOBAL_CFG");
let unknown = ScmFallbackResolver::resolve_identifier(
"nonexistent",
Path::new("src/lib.rs"),
5,
10,
&scopes,
&symbols,
);
assert!(unknown.is_none());
}