use codescout::agent::Agent;
use codescout::lsp::LspManager;
use codescout::tools::output_buffer::OutputBuffer;
use codescout::tools::section_coverage::SectionCoverage;
use codescout::tools::symbol::{CallGraph, Symbols};
use codescout::tools::{Tool, ToolContext};
use serde_json::{json, Value};
use std::sync::{Arc, Mutex};
use tempfile::tempdir;
fn lsp_available(cmd: &str) -> bool {
std::process::Command::new("which")
.arg(cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
async fn project_with_files(files: &[(&str, &str)]) -> (tempfile::TempDir, ToolContext) {
let dir = tempdir().unwrap();
std::fs::create_dir_all(dir.path().join(".codescout")).unwrap();
for (name, content) in files {
let path = dir.path().join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, content).unwrap();
}
let agent = Agent::new(Some(dir.path().to_path_buf())).await.unwrap();
let ctx = ToolContext {
agent,
lsp: LspManager::new_arc(),
output_buffer: Arc::new(OutputBuffer::new(20)),
progress: None,
peer: None,
section_coverage: Arc::new(Mutex::new(SectionCoverage::new())),
};
(dir, ctx)
}
async fn warmup(ctx: &ToolContext, path: &str) {
let input = json!({ "path": path });
for _ in 0u32..60 {
match Symbols.call(input.clone(), ctx).await {
Ok(v) if v["symbols"].as_array().map(|a| a.len()).unwrap_or(0) > 0 => return,
_ => {}
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
}
fn edges_for(result: &Value, direction: &str) -> Option<Vec<Value>> {
let arr = result.get(direction)?.as_array()?;
if arr.is_empty() {
None
} else {
Some(arr.clone())
}
}
const CARGO_TOML: &str = r#"[package]
name = "call-graph-fixture"
version = "0.1.0"
edition = "2021"
"#;
const LIB_RS: &str = r#"pub fn a() {
b();
}
pub fn b() {
c();
}
pub fn c() {}
"#;
#[tokio::test]
#[ignore] async fn call_graph_rust_callees_depth2() {
if !lsp_available("rust-analyzer") {
eprintln!("Skipping call_graph_rust_callees_depth2: rust-analyzer not installed");
return;
}
let (dir, ctx) =
project_with_files(&[("Cargo.toml", CARGO_TOML), ("src/lib.rs", LIB_RS)]).await;
let _ = &dir;
warmup(&ctx, "src/lib.rs").await;
let result = match CallGraph
.call(
json!({
"symbol": "a",
"path": "src/lib.rs",
"direction": "callees",
"max_depth": 2
}),
&ctx,
)
.await
{
Ok(v) => v,
Err(e) => {
eprintln!(
"Skipping call_graph_rust_callees_depth2: call_graph returned error \
(likely indexing lag): {e}"
);
return;
}
};
let edges = match edges_for(&result, "callees") {
Some(e) => e,
None => {
eprintln!(
"Skipping call_graph_rust_callees_depth2: no callee edges returned \
(likely indexing lag); result={result}"
);
return;
}
};
let callee_names: Vec<&str> = edges
.iter()
.filter_map(|e: &Value| e.get("callee").and_then(|v| v.as_str()))
.collect();
assert!(
callee_names.contains(&"b"),
"expected 'b' among callees of 'a'; callees={callee_names:?}; full result={result}"
);
assert!(
callee_names.contains(&"c"),
"expected 'c' among callees of 'a' at depth 2; callees={callee_names:?}; full result={result}"
);
let has_lsp_edge = edges
.iter()
.any(|e: &Value| e.get("source").and_then(|s| s.as_str()) == Some("lsp"));
assert!(
has_lsp_edge,
"expected at least one lsp-sourced edge; edges={edges:?}"
);
}
#[tokio::test]
#[ignore] async fn call_graph_rust_callers_depth2() {
if !lsp_available("rust-analyzer") {
eprintln!("Skipping call_graph_rust_callers_depth2: rust-analyzer not installed");
return;
}
let (dir, ctx) =
project_with_files(&[("Cargo.toml", CARGO_TOML), ("src/lib.rs", LIB_RS)]).await;
let _ = &dir;
warmup(&ctx, "src/lib.rs").await;
let result = match CallGraph
.call(
json!({
"symbol": "c",
"path": "src/lib.rs",
"direction": "callers",
"max_depth": 2
}),
&ctx,
)
.await
{
Ok(v) => v,
Err(e) => {
eprintln!(
"Skipping call_graph_rust_callers_depth2: call_graph returned error \
(likely indexing lag): {e}"
);
return;
}
};
let edges = match edges_for(&result, "callers") {
Some(e) => e,
None => {
eprintln!(
"Skipping call_graph_rust_callers_depth2: no caller edges returned \
(likely indexing lag); result={result}"
);
return;
}
};
let caller_names: Vec<&str> = edges
.iter()
.filter_map(|e: &Value| e.get("caller").and_then(|v| v.as_str()))
.collect();
assert!(
caller_names.contains(&"b"),
"expected 'b' among callers of 'c'; callers={caller_names:?}; full result={result}"
);
assert!(
caller_names.contains(&"a"),
"expected 'a' among callers of 'c' at depth 2; callers={caller_names:?}; full result={result}"
);
let has_lsp_edge = edges
.iter()
.any(|e: &Value| e.get("source").and_then(|s| s.as_str()) == Some("lsp"));
assert!(
has_lsp_edge,
"expected at least one lsp-sourced edge; edges={edges:?}"
);
}