use crate::helpers::{fixture_path, AftProcess};
#[test]
fn callgraph_configure_sets_project_root() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
assert_eq!(
resp["success"], true,
"configure should succeed: {:?}",
resp
);
assert_eq!(
resp["project_root"].as_str().unwrap(),
root,
"should echo back the configured root"
);
aft.shutdown();
}
#[test]
fn callgraph_configure_missing_param() {
let mut aft = AftProcess::spawn();
let resp = aft.send(r#"{"id":"1","command":"configure"}"#);
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "invalid_request");
aft.shutdown();
}
#[test]
fn callgraph_call_tree_without_configure() {
let mut aft = AftProcess::spawn();
let resp = aft.send(r#"{"id":"1","command":"call_tree","file":"main.ts","symbol":"main"}"#);
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "not_configured");
aft.shutdown();
}
#[test]
fn callgraph_cross_file_tree() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
assert_eq!(resp["success"], true);
let resp = aft.send(&format!(
r#"{{"id":"2","command":"call_tree","file":"{}/main.ts","symbol":"main","depth":5}}"#,
root
));
assert_eq!(
resp["success"], true,
"call_tree should succeed: {:?}",
resp
);
assert_eq!(resp["name"], "main");
assert_eq!(resp["resolved"], true);
assert_eq!(resp["line"], 3, "main definition line should be 1-based");
let children = resp["children"]
.as_array()
.expect("children should be array");
let process_data = children
.iter()
.find(|c| c["name"] == "processData")
.expect("main should call processData");
assert_eq!(process_data["resolved"], true);
assert_eq!(
process_data["line"], 3,
"processData line should be 1-based"
);
assert!(
process_data["file"].as_str().unwrap().contains("utils.ts"),
"processData should be in utils.ts, got: {}",
process_data["file"]
);
let pd_children = process_data["children"]
.as_array()
.expect("processData children");
let validate = pd_children
.iter()
.find(|c| c["name"] == "validate")
.expect("processData should call validate");
assert_eq!(validate["resolved"], true);
assert_eq!(validate["line"], 1, "validate line should be 1-based");
assert!(
validate["file"].as_str().unwrap().contains("helpers.ts"),
"validate should be in helpers.ts, got: {}",
validate["file"]
);
let v_children = validate["children"].as_array().expect("validate children");
let check_format = v_children.iter().find(|c| c["name"] == "checkFormat");
assert!(
check_format.is_some(),
"validate should call checkFormat, children: {:?}",
v_children
.iter()
.map(|c| c["name"].clone())
.collect::<Vec<_>>()
);
assert_eq!(
check_format.unwrap()["line"],
2,
"checkFormat line should be 1-based (call site, not definition)"
);
aft.shutdown();
}
#[test]
fn callgraph_depth_limit_truncates() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"call_tree","file":"{}/main.ts","symbol":"main","depth":1}}"#,
root
));
assert_eq!(resp["success"], true);
assert_eq!(resp["name"], "main");
let children = resp["children"].as_array().expect("children");
for child in children {
let grandchildren = child["children"].as_array();
match grandchildren {
Some(gc) => assert!(
gc.is_empty(),
"At depth 1, child '{}' should have no grandchildren",
child["name"]
),
None => {} }
}
aft.shutdown();
}
#[test]
fn callgraph_unknown_symbol_error() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"call_tree","file":"{}/main.ts","symbol":"nonexistent"}}"#,
root
));
assert_eq!(
resp["success"], false,
"unknown symbol should fail: {:?}",
resp
);
assert_eq!(resp["code"], "symbol_not_found");
aft.shutdown();
}
#[test]
fn callgraph_aliased_import_resolution() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"call_tree","file":"{}/aliased.ts","symbol":"runCheck","depth":3}}"#,
root
));
assert_eq!(
resp["success"], true,
"aliased call_tree should succeed: {:?}",
resp
);
assert_eq!(resp["name"], "runCheck");
let children = resp["children"].as_array().expect("children");
let resolved_child = children
.iter()
.find(|c| c["resolved"] == true && c["file"].as_str().unwrap_or("").contains("helpers.ts"));
assert!(
resolved_child.is_some(),
"checker alias should resolve to helpers.ts, children: {:?}",
children
);
aft.shutdown();
}
#[test]
fn callgraph_callers_without_configure() {
let mut aft = AftProcess::spawn();
let resp =
aft.send(r#"{"id":"1","command":"callers","file":"helpers.ts","symbol":"validate"}"#);
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "not_configured");
aft.shutdown();
}
#[test]
fn callgraph_callers_cross_file() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
assert_eq!(resp["success"], true);
let resp = aft.send(&format!(
r#"{{"id":"2","command":"callers","file":"{}/helpers.ts","symbol":"validate","depth":1}}"#,
root
));
assert_eq!(resp["success"], true, "callers should succeed: {:?}", resp);
assert_eq!(resp["symbol"], "validate");
assert!(
resp["total_callers"].as_u64().unwrap() > 0,
"validate should have callers"
);
assert!(
resp["scanned_files"].as_u64().unwrap() > 0,
"should report scanned files"
);
let callers = resp["callers"].as_array().expect("callers array");
let utils_group = callers
.iter()
.find(|g| g["file"].as_str().unwrap_or("").contains("utils.ts"));
assert!(
utils_group.is_some(),
"validate should be called from utils.ts, groups: {:?}",
callers
);
let group = utils_group.unwrap();
let entries = group["callers"].as_array().expect("callers entries");
let process_data_caller = entries
.iter()
.find(|e| e["symbol"].as_str().unwrap_or("") == "processData");
assert!(
process_data_caller.is_some(),
"validate should be called by processData, entries: {:?}",
entries
);
assert_eq!(
process_data_caller.unwrap()["line"],
4,
"call site line should be 1-based"
);
aft.shutdown();
}
#[test]
fn callgraph_callers_empty_result() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"callers","file":"{}/main.ts","symbol":"main"}}"#,
root
));
assert_eq!(resp["success"], true, "callers should succeed: {:?}", resp);
assert_eq!(resp["total_callers"], 0, "main should have no callers");
let callers = resp["callers"].as_array().expect("callers array");
assert!(
callers.is_empty(),
"callers should be empty for entry point"
);
aft.shutdown();
}
#[test]
fn callgraph_callers_recursive() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"callers","file":"{}/helpers.ts","symbol":"validate","depth":2}}"#,
root
));
assert_eq!(
resp["success"], true,
"recursive callers should succeed: {:?}",
resp
);
let total = resp["total_callers"].as_u64().unwrap();
assert!(
total >= 2,
"with depth 2, validate should have >= 2 callers (direct + transitive), got {}",
total
);
aft.shutdown();
}
#[test]
fn callgraph_trace_to_not_configured() {
let mut aft = AftProcess::spawn();
let resp =
aft.send(r#"{"id":"1","command":"trace_to","file":"helpers.ts","symbol":"checkFormat"}"#);
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "not_configured");
aft.shutdown();
}
#[test]
fn callgraph_trace_to_symbol_not_found() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_to","file":"{}/helpers.ts","symbol":"nonexistent"}}"#,
root
));
assert_eq!(
resp["success"], false,
"unknown symbol should fail: {:?}",
resp
);
assert_eq!(resp["code"], "symbol_not_found");
aft.shutdown();
}
#[test]
fn callgraph_trace_to_single_path() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_to","file":"{}/helpers.ts","symbol":"checkFormat","depth":10}}"#,
root
));
assert_eq!(resp["success"], true, "trace_to should succeed: {:?}", resp);
assert_eq!(resp["target_symbol"], "checkFormat");
assert!(resp["target_file"].as_str().unwrap().contains("helpers.ts"));
let paths = resp["paths"].as_array().expect("paths should be array");
assert!(
!paths.is_empty(),
"checkFormat should have at least one path to an entry point"
);
let main_path = paths.iter().find(|p| {
let hops = p["hops"].as_array().unwrap();
!hops.is_empty() && hops[0]["symbol"] == "main"
});
assert!(
main_path.is_some(),
"should have a path starting from main, paths: {:?}",
paths
);
let hops = main_path.unwrap()["hops"].as_array().unwrap();
let last = &hops[hops.len() - 1];
assert_eq!(
last["symbol"], "checkFormat",
"path should end at checkFormat"
);
assert_eq!(hops[0]["line"], 3, "entry point line should be 1-based");
assert_eq!(last["line"], 5, "target line should be 1-based");
assert!(resp["total_paths"].as_u64().unwrap() >= 1);
assert!(resp["entry_points_found"].as_u64().unwrap() >= 1);
aft.shutdown();
}
#[test]
fn callgraph_trace_to_multi_path() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_to","file":"{}/helpers.ts","symbol":"validate","depth":10}}"#,
root
));
assert_eq!(resp["success"], true, "trace_to should succeed: {:?}", resp);
assert_eq!(resp["target_symbol"], "validate");
let total_paths = resp["total_paths"].as_u64().unwrap();
assert!(
total_paths >= 2,
"validate should have multiple paths to entry points, got {}",
total_paths
);
let entry_points_found = resp["entry_points_found"].as_u64().unwrap();
assert!(
entry_points_found >= 2,
"validate should have multiple distinct entry points, got {}",
entry_points_found
);
let paths = resp["paths"].as_array().expect("paths should be array");
let entry_names: Vec<&str> = paths
.iter()
.filter_map(|p| {
let hops = p["hops"].as_array()?;
hops.first().and_then(|h| h["symbol"].as_str())
})
.collect();
assert!(
entry_names.contains(&"main") || entry_names.contains(&"handleRequest"),
"should have main or handleRequest as entry point, got: {:?}",
entry_names
);
for path in paths {
let hops = path["hops"].as_array().unwrap();
let last = &hops[hops.len() - 1];
assert_eq!(
last["symbol"], "validate",
"every path should end at validate"
);
}
aft.shutdown();
}
#[test]
fn callgraph_trace_to_no_entry_points() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_to","file":"{}/main.ts","symbol":"main","depth":10}}"#,
root
));
assert_eq!(
resp["success"], true,
"trace_to on entry point should succeed: {:?}",
resp
);
assert_eq!(resp["target_symbol"], "main");
let paths = resp["paths"].as_array().expect("paths should be array");
for path in paths {
let hops = path["hops"].as_array().unwrap();
let has_main = hops.iter().any(|h| h["symbol"] == "main");
assert!(has_main, "any path for main should include main");
}
assert!(resp.get("total_paths").is_some());
assert!(resp.get("entry_points_found").is_some());
assert!(resp.get("truncated_paths").is_some());
aft.shutdown();
}
fn setup_watcher_fixture() -> (tempfile::TempDir, String) {
let fixtures = fixture_path("callgraph");
let tmp = tempfile::tempdir().expect("create temp dir");
for entry in std::fs::read_dir(&fixtures).expect("read fixtures dir") {
let entry = entry.expect("read entry");
let src = entry.path();
if src.is_file() {
let dst = tmp.path().join(entry.file_name());
std::fs::copy(&src, &dst).expect("copy fixture file");
}
}
let root = tmp.path().display().to_string();
(tmp, root)
}
#[test]
fn callgraph_watcher_add_caller() {
let (_tmp, root) = setup_watcher_fixture();
let mut aft = AftProcess::spawn();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
assert_eq!(
resp["success"], true,
"configure should succeed: {:?}",
resp
);
let resp = aft.send(&format!(
r#"{{"id":"2","command":"callers","file":"{}/helpers.ts","symbol":"validate","depth":1}}"#,
root
));
assert_eq!(
resp["success"], true,
"initial callers should succeed: {:?}",
resp
);
let initial_total = resp["total_callers"].as_u64().unwrap();
assert!(initial_total > 0, "validate should have initial callers");
let new_file = std::path::Path::new(&root).join("extra_caller.ts");
std::fs::write(
&new_file,
r#"import { validate } from './helpers';
export function extraCheck(input: string): boolean {
return validate(input);
}
"#,
)
.expect("write new caller file");
std::thread::sleep(std::time::Duration::from_millis(500));
aft.send(r#"{"id":"3","command":"ping"}"#);
let resp = aft.send(&format!(
r#"{{"id":"4","command":"callers","file":"{}/helpers.ts","symbol":"validate","depth":1}}"#,
root
));
assert_eq!(
resp["success"], true,
"callers after add should succeed: {:?}",
resp
);
let new_total = resp["total_callers"].as_u64().unwrap();
assert!(
new_total > initial_total,
"adding a caller should increase total_callers: initial={}, new={}",
initial_total,
new_total
);
let callers = resp["callers"].as_array().expect("callers array");
let extra_group = callers
.iter()
.find(|g| g["file"].as_str().unwrap_or("").contains("extra_caller.ts"));
assert!(
extra_group.is_some(),
"new caller from extra_caller.ts should appear, callers: {:?}",
callers
);
aft.shutdown();
}
#[test]
fn callgraph_watcher_remove_caller() {
let (_tmp, root) = setup_watcher_fixture();
let mut aft = AftProcess::spawn();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
assert_eq!(
resp["success"], true,
"configure should succeed: {:?}",
resp
);
let resp = aft.send(&format!(
r#"{{"id":"2","command":"callers","file":"{}/helpers.ts","symbol":"validate","depth":1}}"#,
root
));
assert_eq!(
resp["success"], true,
"initial callers should succeed: {:?}",
resp
);
let callers = resp["callers"].as_array().expect("callers array");
let utils_group = callers
.iter()
.find(|g| g["file"].as_str().unwrap_or("").contains("utils.ts"));
assert!(
utils_group.is_some(),
"validate should initially be called from utils.ts"
);
let utils_path = std::path::Path::new(&root).join("utils.ts");
std::fs::write(
&utils_path,
r#"export function processData(input: string): string {
// validate call removed
return input.toUpperCase();
}
"#,
)
.expect("rewrite utils.ts");
std::thread::sleep(std::time::Duration::from_millis(500));
aft.send(r#"{"id":"3","command":"ping"}"#);
let resp = aft.send(&format!(
r#"{{"id":"4","command":"callers","file":"{}/helpers.ts","symbol":"validate","depth":1}}"#,
root
));
assert_eq!(
resp["success"], true,
"callers after remove should succeed: {:?}",
resp
);
let callers = resp["callers"].as_array().expect("callers array");
let utils_group = callers
.iter()
.find(|g| g["file"].as_str().unwrap_or("").contains("utils.ts"));
if let Some(group) = utils_group {
let entries = group["callers"].as_array().expect("callers entries");
let validate_caller = entries
.iter()
.find(|e| e["callee"].as_str().unwrap_or("") == "validate");
assert!(
validate_caller.is_none(),
"validate call should be removed from utils.ts, entries: {:?}",
entries
);
}
aft.shutdown();
}
#[test]
fn callgraph_impact_not_configured() {
let mut aft = AftProcess::spawn();
let resp = aft.send(r#"{"id":"1","command":"impact","file":"helpers.ts","symbol":"validate"}"#);
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "not_configured");
aft.shutdown();
}
#[test]
fn callgraph_impact_symbol_not_found() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"impact","file":"{}/helpers.ts","symbol":"nonexistent"}}"#,
root
));
assert_eq!(
resp["success"], false,
"unknown symbol should fail: {:?}",
resp
);
assert_eq!(resp["code"], "symbol_not_found");
aft.shutdown();
}
#[test]
fn callgraph_impact_multi_caller() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"impact","file":"{}/helpers.ts","symbol":"validate","depth":5}}"#,
root
));
assert_eq!(resp["success"], true, "impact should succeed: {:?}", resp);
assert_eq!(resp["symbol"], "validate");
assert!(resp["file"].as_str().unwrap().contains("helpers.ts"));
let total_affected = resp["total_affected"].as_u64().unwrap();
assert!(
total_affected >= 2,
"validate should have at least 2 affected callers, got {}",
total_affected
);
let affected_files = resp["affected_files"].as_u64().unwrap();
assert!(
affected_files >= 2,
"validate should affect at least 2 files, got {}",
affected_files
);
let callers = resp["callers"].as_array().expect("callers array");
assert!(!callers.is_empty(), "callers should not be empty");
for caller in callers {
assert!(
caller.get("caller_symbol").is_some(),
"caller should have caller_symbol: {:?}",
caller
);
assert!(
caller.get("caller_file").is_some(),
"caller should have caller_file: {:?}",
caller
);
assert!(
caller.get("line").is_some(),
"caller should have line: {:?}",
caller
);
assert!(
caller["line"].as_u64().unwrap_or(0) >= 1,
"caller line should be 1-based: {:?}",
caller
);
assert!(
caller.get("is_entry_point").is_some(),
"caller should have is_entry_point: {:?}",
caller
);
assert!(
caller.get("parameters").is_some(),
"caller should have parameters: {:?}",
caller
);
}
let has_entry_point = callers
.iter()
.any(|c| c["is_entry_point"].as_bool() == Some(true));
assert!(
has_entry_point,
"at least one caller should be an entry point, callers: {:?}",
callers
);
assert!(
resp.get("signature").is_some(),
"target should have a signature"
);
let params = resp["parameters"].as_array().expect("parameters array");
assert!(
params.iter().any(|p| p.as_str() == Some("input")),
"validate parameters should include 'input', got: {:?}",
params
);
aft.shutdown();
}
#[test]
fn callgraph_trace_data_not_configured() {
let mut aft = AftProcess::spawn();
let resp = aft.send(
r#"{"id":"1","command":"trace_data","file":"data_flow.ts","symbol":"transformData","expression":"rawInput"}"#,
);
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "not_configured");
aft.shutdown();
}
#[test]
fn callgraph_trace_data_symbol_not_found() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_data","file":"{}/data_flow.ts","symbol":"nonexistent","expression":"x"}}"#,
root
));
assert_eq!(
resp["success"], false,
"unknown symbol should fail: {:?}",
resp
);
assert_eq!(resp["code"], "symbol_not_found");
aft.shutdown();
}
#[test]
fn callgraph_trace_data_assignment_tracking() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_data","file":"{}/data_flow.ts","symbol":"transformData","expression":"rawInput","depth":5}}"#,
root
));
assert_eq!(
resp["success"], true,
"trace_data should succeed: {:?}",
resp
);
assert_eq!(resp["expression"], "rawInput");
assert!(
resp["origin_file"]
.as_str()
.unwrap()
.contains("data_flow.ts"),
"origin_file should reference data_flow.ts"
);
assert_eq!(resp["origin_symbol"], "transformData");
let hops = resp["hops"].as_array().expect("hops array");
assert!(
!hops.is_empty(),
"should have at least one hop (assignment rawInput → cleaned)"
);
let first = &hops[0];
assert_eq!(
first["flow_type"], "assignment",
"first hop should be assignment"
);
assert_eq!(first["variable"], "cleaned", "should track to 'cleaned'");
assert_eq!(
first["approximate"], false,
"direct assignment is not approximate"
);
assert_eq!(first["line"], 4, "assignment line should be 1-based");
aft.shutdown();
}
#[test]
fn callgraph_trace_data_cross_file() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_data","file":"{}/data_flow.ts","symbol":"transformData","expression":"rawInput","depth":5}}"#,
root
));
assert_eq!(
resp["success"], true,
"trace_data should succeed: {:?}",
resp
);
let hops = resp["hops"].as_array().expect("hops array");
assert!(
hops.len() >= 2,
"should have at least 2 hops (assignment + cross-file parameter), got {}: {:?}",
hops.len(),
hops
);
let has_param_hop = hops.iter().any(|h| {
h["flow_type"] == "parameter"
&& h["file"]
.as_str()
.map(|f| f.contains("data_processor.ts"))
.unwrap_or(false)
});
assert!(
has_param_hop,
"should have a parameter hop into data_processor.ts, hops: {:?}",
hops
);
let param_hop = hops.iter().find(|h| {
h["flow_type"] == "parameter"
&& h["file"]
.as_str()
.map(|f| f.contains("data_processor.ts"))
.unwrap_or(false)
});
if let Some(ph) = param_hop {
assert_eq!(
ph["variable"], "input",
"parameter should be 'input' (processInput's parameter)"
);
assert_eq!(ph["approximate"], false);
assert_eq!(ph["line"], 1, "parameter line should be 1-based");
}
aft.shutdown();
}
#[test]
fn callgraph_trace_data_approximation() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_data","file":"{}/data_flow.ts","symbol":"complexFlow","expression":"data","depth":5}}"#,
root
));
assert_eq!(
resp["success"], true,
"trace_data should succeed: {:?}",
resp
);
let hops = resp["hops"].as_array().expect("hops array");
let has_approximate = hops.iter().any(|h| h["approximate"] == true);
assert!(
has_approximate,
"destructuring should produce an approximate hop, hops: {:?}",
hops
);
aft.shutdown();
}
#[test]
fn callgraph_configure_small_repo_does_not_flag_exceeds() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}"}}"#,
root
));
assert_eq!(
resp["success"], true,
"configure should succeed: {:?}",
resp
);
assert_eq!(
resp["source_file_count_exceeds_max"], false,
"small fixture with default cap should NOT be flagged as exceeding"
);
let count = resp["source_file_count"].as_u64().unwrap_or(0);
assert!(
count > 0 && count < 100,
"small fixture should report a real (non-capped) count, got {}",
count
);
aft.shutdown();
}
#[test]
fn callgraph_configure_reports_source_file_count_exceeds_max() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}","max_callgraph_files":1}}"#,
root
));
assert_eq!(
resp["success"], true,
"configure should succeed: {:?}",
resp
);
assert_eq!(
resp["source_file_count_exceeds_max"], true,
"9-file fixture with cap=1 should be flagged as exceeding max"
);
assert_eq!(resp["max_callgraph_files"], 1);
aft.shutdown();
}
#[test]
fn callgraph_callers_project_too_large() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}","max_callgraph_files":1}}"#,
root
));
assert_eq!(resp["success"], true);
let resp = aft.send(&format!(
r#"{{"id":"2","command":"callers","file":"{}/helpers.ts","symbol":"validate","depth":1}}"#,
root
));
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "project_too_large");
let msg = resp["message"].as_str().unwrap_or("");
assert!(
msg.contains("max_callgraph_files"),
"error message should mention max_callgraph_files: {}",
msg
);
aft.shutdown();
}
#[test]
fn callgraph_trace_to_project_too_large() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}","max_callgraph_files":1}}"#,
root
));
assert_eq!(resp["success"], true);
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_to","file":"{}/helpers.ts","symbol":"validate","depth":5}}"#,
root
));
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "project_too_large");
aft.shutdown();
}
#[test]
fn callgraph_impact_project_too_large() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}","max_callgraph_files":1}}"#,
root
));
assert_eq!(resp["success"], true);
let resp = aft.send(&format!(
r#"{{"id":"2","command":"impact","file":"{}/helpers.ts","symbol":"validate"}}"#,
root
));
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "project_too_large");
aft.shutdown();
}
#[test]
fn callgraph_trace_data_project_too_large() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}","max_callgraph_files":1}}"#,
root
));
assert_eq!(resp["success"], true);
let resp = aft.send(&format!(
r#"{{"id":"2","command":"trace_data","file":"{}/data_flow.ts","symbol":"transformData","expression":"rawInput"}}"#,
root
));
assert_eq!(resp["success"], false);
assert_eq!(resp["code"], "project_too_large");
aft.shutdown();
}
#[test]
fn callgraph_configure_rejects_zero_max_callgraph_files() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}","max_callgraph_files":0}}"#,
root
));
assert_eq!(resp["success"], false, "configure should reject 0");
assert_eq!(resp["code"], "invalid_request");
let msg = resp["message"].as_str().unwrap_or("");
assert!(
msg.contains("max_callgraph_files"),
"error message should mention max_callgraph_files: {}",
msg
);
aft.shutdown();
}
#[test]
fn callgraph_configure_rejects_negative_max_callgraph_files() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}","max_callgraph_files":-5}}"#,
root
));
assert_eq!(resp["success"], false, "configure should reject -5");
assert_eq!(resp["code"], "invalid_request");
aft.shutdown();
}
#[test]
fn callgraph_configure_accepts_positive_max_callgraph_files() {
let mut aft = AftProcess::spawn();
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}","max_callgraph_files":42}}"#,
root
));
assert_eq!(resp["success"], true);
assert_eq!(resp["max_callgraph_files"], 42);
aft.shutdown();
}
#[test]
fn callgraph_configure_rejects_non_integer_max_callgraph_files_payloads() {
let fixtures = fixture_path("callgraph");
let root = fixtures.display().to_string();
let rejected_payloads = [
("float", "1.5"),
("string", "\"twenty\""),
("numeric_string", "\"20000\""),
("bool_true", "true"),
("bool_false", "false"),
("null", "null"),
("array", "[]"),
("object", "{}"),
];
for (label, payload) in rejected_payloads {
let mut aft = AftProcess::spawn();
let resp = aft.send(&format!(
r#"{{"id":"1","command":"configure","project_root":"{}","max_callgraph_files":{}}}"#,
root, payload
));
assert_eq!(
resp["success"], false,
"configure should reject {label} payload ({payload})"
);
assert_eq!(
resp["code"], "invalid_request",
"configure should return invalid_request for {label} payload ({payload})"
);
let msg = resp["message"].as_str().unwrap_or("");
assert!(
msg.contains("max_callgraph_files"),
"error message should mention max_callgraph_files for {label}: {msg}"
);
aft.shutdown();
}
}