use std::fs;
use tempfile::tempdir;
use scryer_db::{CodeGraphEdge, Project, ScryerDb, Symbol, SymbolReference};
use scryer_engine::EngineService;
async fn index(files: &[(&str, &str)]) -> anyhow::Result<(ScryerDb, u64)> {
let temp = tempdir()?;
let root = temp.path();
for (path, content) in files {
fs::write(root.join(path), content)?;
}
let db = ScryerDb::new_in_memory().await?;
let project = {
let mut conn = db.lock().await;
Project::create()
.slug("chains".to_string())
.root_path(root.to_string_lossy().to_string())
.default_branch("main".to_string())
.is_active(true)
.created_at("2026-10-08T00:00:00Z".to_string())
.updated_at("2026-10-08T00:00:00Z".to_string())
.exec(&mut *conn)
.await?
};
EngineService::new(db.clone())
.index_project(project.id, root)
.await?;
Ok((db, project.id))
}
async fn call_lines(db: &ScryerDb, project: u64, name: &str) -> anyhow::Result<Vec<u32>> {
let mut conn = db.lock().await;
let ids: Vec<u64> = Symbol::filter_by_project_id(project)
.exec(&mut *conn)
.await?
.into_iter()
.filter(|s| s.name == name)
.map(|s| s.id)
.collect();
let mut lines: Vec<u32> = SymbolReference::filter_by_project_id(project)
.exec(&mut *conn)
.await?
.into_iter()
.filter(|r| r.role == "call" && ids.contains(&r.symbol_id))
.map(|r| r.line_number)
.collect();
lines.sort_unstable();
lines.dedup();
Ok(lines)
}
#[tokio::test]
async fn rust_calls_in_method_chains_are_all_found() -> anyhow::Result<()> {
let (db, project) = index(&[(
"lib.rs",
"pub fn make() -> Option<u32> { Some(1) }\n\
pub fn tail() -> Option<u32> { Some(2) }\n\
pub fn direct() { make(); }\n\
pub fn chain_end() { let _ = tail().map(|x| x); }\n\
pub fn chain_head() { let _ = make().map(|x| x + 1).unwrap_or(0); }\n\
pub fn chain_long() { let _ = make().ok_or(1).map(|x| x).is_ok(); }\n\
pub fn awaited() { let _ = make().map(|x| x).ok(); }\n\
pub fn nested_arg() { let _ = tail().map(|x| make().unwrap_or(x)); }\n\
pub fn turbofish() { let _ = make().into_iter().collect::<Vec<_>>(); }\n",
)])
.await?;
assert_eq!(
call_lines(&db, project, "make").await?,
vec![3, 5, 6, 7, 8, 9]
);
assert_eq!(call_lines(&db, project, "tail").await?, vec![4, 8]);
let mut conn = db.lock().await;
let symbols = Symbol::filter_by_project_id(project)
.exec(&mut *conn)
.await?;
let id = |n: &str| symbols.iter().find(|s| s.name == n).unwrap().id;
let edges = CodeGraphEdge::filter_by_project_id(project)
.exec(&mut *conn)
.await?;
for caller in [
"direct",
"chain_head",
"chain_long",
"awaited",
"nested_arg",
"turbofish",
] {
assert!(
edges.iter().any(|e| e.source_symbol_id == id(caller)
&& e.target_symbol_id == id("make")
&& e.edge_type == "calls"),
"missing edge {caller} -> make"
);
}
Ok(())
}
#[tokio::test]
async fn python_calls_in_method_chains_are_all_found() -> anyhow::Result<()> {
let (db, project) = index(&[(
"m.py",
"def make():\n return []\n\n\
def direct():\n make()\n\n\
def chained():\n return make().copy().pop()\n\n\
def arg():\n return str(make().copy())\n",
)])
.await?;
assert_eq!(call_lines(&db, project, "make").await?, vec![5, 8, 11]);
Ok(())
}
#[tokio::test]
async fn typescript_calls_in_method_chains_are_all_found() -> anyhow::Result<()> {
let (db, project) = index(&[(
"m.ts",
"export function make(): number[] { return []; }\n\
export function direct() { make(); }\n\
export function chained() { return make().slice().pop(); }\n\
export function mapped() { return make().map((x) => x).filter(Boolean); }\n",
)])
.await?;
assert_eq!(call_lines(&db, project, "make").await?, vec![2, 3, 4]);
Ok(())
}