use super::*;
fn v(s: &str) -> Value {
serde_json::from_str(s).unwrap()
}
#[test]
fn test_trim_context_keeps_analysis_result() {
let input = v(r#"{
"query": "Context for node main",
"results": [
{"rank": 1, "file_path": "src/main.rs", "symbol_name": "main", "language": "rust"}
],
"context": "fn main() { ... }",
"tokens_used": 120,
"processing_time_ms": 5
}"#);
let t = trim_context(&input);
assert_eq!(t["query"], "Context for node main");
assert!(t["results"].is_array());
assert_eq!(t["context"], "fn main() { ... }");
assert_eq!(t["tokens_used"], 120);
assert_eq!(t["processing_time_ms"], 5);
let merged = trim_llm_payload(
"leindex.context",
&v(r#"{
"query": "q", "results": [], "context": "c",
"tokens_used": 0, "processing_time_ms": 0,
"_warning": "stale"
}"#),
);
assert_eq!(merged["_warning"], "stale");
}
#[test]
fn test_trim_symbol_lookup_keeps_actual_fields() {
let input = v(r#"{
"symbol": "main",
"type": "function",
"file": "src/main.rs",
"byte_range": [0, 100],
"complexity": 3,
"language": "rust",
"callers": [{"name": "test_main"}],
"callees": [],
"impact_radius": {"affected_symbols": 5, "affected_files": 2},
"source": "fn main() { ... }"
}"#);
let t = trim_symbol_lookup(&input);
assert_eq!(t["symbol"], "main");
assert_eq!(t["type"], "function");
assert_eq!(t["file"], "src/main.rs");
assert_eq!(t["byte_range"][0], 0);
assert_eq!(t["complexity"], 3);
assert_eq!(t["language"], "rust");
assert!(t["callers"].is_array());
assert!(t["callees"].is_array());
assert_eq!(t["impact_radius"]["affected_symbols"], 5);
assert_eq!(t["source"], "fn main() { ... }");
assert!(t.get("file_path").is_none());
assert!(t.get("signature").is_none());
}
#[test]
fn test_trim_search_drops_verbose_fields() {
let input = v(r#"{
"results": [
{
"rank": 1,
"node_id": "abc123",
"file_path": "/p/src/foo.rs",
"symbol_name": "main",
"symbol_type": "function",
"language": "rust",
"byte_range": [0, 100],
"line_number": 42,
"complexity": 3,
"caller_count": 5,
"dependency_count": 2,
"context": "// first line",
"score": {"overall": 0.85, "neural": 0.9, "text": 0.7, "structural": 0.8}
}
],
"offset": 0,
"count": 1,
"has_more": false,
"suggestion": "nope"
}"#);
let t = trim_search(&input);
let r = &t["results"][0];
assert_eq!(r["file_path"], "/p/src/foo.rs");
assert_eq!(r["symbol"], "main");
assert_eq!(r["symbol_type"], "function");
assert_eq!(r["line_number"], 42);
assert_eq!(r["score"], 0.85);
assert_eq!(r["snippet"], "// first line");
assert!(r.get("node_id").is_none() || r["node_id"].is_null());
assert!(r.get("byte_range").is_none() || r["byte_range"].is_null());
assert!(r.get("complexity").is_none() || r["complexity"].is_null());
assert!(r.get("caller_count").is_none() || r["caller_count"].is_null());
assert!(r.get("language").is_none() || r["language"].is_null());
}
#[test]
fn test_trim_search_falls_back_to_symbol_key() {
let input = v(r#"{
"results": [
{
"rank": 1,
"file_path": "/p/src/foo.rs",
"symbol": "main",
"symbol_type": "function",
"context": "fn main() { return 0; }"
}
]
}"#);
let t = trim_search(&input);
let r = &t["results"][0];
assert_eq!(r["symbol"], "main");
assert_eq!(r["file_path"], "/p/src/foo.rs");
}
#[test]
fn test_trim_search_prefers_symbol_name_when_both_present() {
let input = v(r#"{
"results": [
{
"file_path": "/p/src/foo.rs",
"symbol": "alias",
"symbol_name": "canonical",
"symbol_type": "function",
"context": "x"
}
]
}"#);
let t = trim_search(&input);
let r = &t["results"][0];
assert_eq!(r["symbol"], "canonical");
}
#[test]
fn test_trim_edit_keeps_structured_diff() {
let input = v(r#"{
"preview_token": "tok",
"diff": {"file_path": "src/foo.rs", "additions": 1, "deletions": 1, "hunks": []},
"diff_text": "--- a\n+++ b\n@@ ...",
"affected_symbols": ["main"],
"affected_files": ["src/foo.rs"],
"breaking_changes": [],
"risk_level": "low",
"change_count": 1,
"validation": {"x": 1}
}"#);
let t = trim_edit(&input);
assert_eq!(t["preview_token"], "tok");
assert!(t["diff"].is_object());
assert_eq!(t["risk_level"], "low");
assert_eq!(t["change_count"], 1);
assert_eq!(t["affected_symbols"][0], "main");
assert!(t.get("diff_text").is_some());
assert!(t.get("validation").is_none());
}
#[test]
fn test_trim_edit_keeps_apply_result_fields() {
let input = v(r#"{
"success": true,
"changes_applied": 3,
"file_path": "src/foo.rs",
"edit_region": {"start": 10, "end": 25},
"message": "Applied 3 changes",
"diff": {"file_path": "src/foo.rs", "additions": 3, "deletions": 0, "hunks": []}
}"#);
let t = trim_edit(&input);
assert_eq!(t["success"], true);
assert_eq!(t["changes_applied"], 3);
assert_eq!(t["file_path"], "src/foo.rs");
assert!(t["edit_region"].is_object());
assert_eq!(t["message"], "Applied 3 changes");
}
#[test]
fn test_trim_read_symbol_caps_callers() {
let callers: Vec<Value> = (0..20)
.map(|i| serde_json::json!({"name": format!("c{}", i), "file": "a.rs", "line": i}))
.collect();
let input = v(&format!(
r#"{{
"symbol": "main",
"type": "function",
"file": "src/main.rs",
"language": "rust",
"complexity": 5,
"line_start": 1,
"line_end": 10,
"doc_comment": "/// entry",
"source": "{}",
"callers": {},
"callees": []
}}"#,
"x".repeat(3000),
serde_json::to_string(&callers).unwrap()
));
let t = trim_read_symbol(&input);
let src = t["source"].as_str().unwrap();
assert!(src.chars().count() <= 2003, "got {}", src.chars().count());
assert!(
src.ends_with("..."),
"missing ellipsis: {:?}",
src.chars().rev().take(10).collect::<String>()
);
assert_eq!(t["source_truncated"], true);
assert_eq!(t["callers"].as_array().unwrap().len(), 5);
assert_eq!(t["callers_more"], true);
}
#[test]
fn test_trim_grep_symbols_drops_byte_range() {
let input = v(r#"{
"results": [
{
"name": "main",
"type": "function",
"file": "src/main.rs",
"byte_range": [0, 200],
"complexity": 3,
"language": "rust",
"caller_count": 10,
"callers": [{"name": "a"}, {"name": "b"}, {"name": "c"}, {"name": "d"}, {"name": "e"}, {"name": "f"}, {"name": "g"}],
"callees": [{"name": "x"}]
}
],
"total_matches": 1,
"shown": 1,
"offset": 0,
"mode": "code"
}"#);
let t = trim_grep_symbols(&input);
let r = &t["results"][0];
assert!(r.get("byte_range").is_none());
assert!(r.get("language").is_none());
assert_eq!(r["callers"].as_array().unwrap().len(), 5);
assert_eq!(r["callee_count"], Value::Null); assert_eq!(r["caller_count"], 10);
}
#[test]
fn test_trim_text_search_preserves_context_windows() {
let input = v(r#"{
"count": 1,
"total_matched": 1,
"has_more": false,
"offset": 0,
"results": [
{
"file": "src/foo.rs",
"line": 42,
"content": "let x = 1;",
"before": ["fn main() {", " let y = 2;"],
"after": [" let z = 3;", "}"],
"in_symbol": "main",
"symbol_type": "function"
}
]
}"#);
let t = trim_text_search(&input);
let r = &t["results"][0];
assert!(r.get("before").is_some());
assert!(r.get("after").is_some());
assert_eq!(r["file"], "src/foo.rs");
assert_eq!(r["line"], 42);
}
#[test]
fn test_trim_deep_analyze_caps_results() {
let results: Vec<Value> = (0..15)
.map(|i| {
serde_json::json!({
"rank": i + 1,
"node_id": format!("n{}", i),
"file_path": format!("/p/src/file{}.rs", i),
"symbol_name": format!("sym{}", i),
"symbol_type": "function",
"signature": "fn x()",
"complexity": i as u32,
"context": "ctx",
"score": {"overall": 0.5}
})
})
.collect();
let input = v(&format!(
r#"{{
"query": "what is X",
"tokens_used": 1500,
"processing_time_ms": 250,
"context": "expanded prose here",
"results": {}
}}"#,
serde_json::to_string(&results).unwrap()
));
let t = trim_deep_analyze(&input);
assert_eq!(t["results"].as_array().unwrap().len(), 10);
assert_eq!(t["results_more"], 5);
let r0 = &t["results"][0];
assert!(r0.get("node_id").is_none());
assert!(r0.get("complexity").is_none());
}
#[test]
fn test_trim_write_drops_byte_range() {
let input = v(r#"{
"success": true,
"file_path": "src/new.rs",
"language": "rust",
"symbols": [
{"name": "main", "type": "function", "byte_range": [0, 50]},
{"name": "helper", "type": "function", "byte_range": [50, 100]}
]
}"#);
let t = trim_write(&input);
assert_eq!(t["success"], true);
let syms = t["symbols"].as_array().unwrap();
for s in syms {
assert!(s.get("byte_range").is_none());
}
}
#[test]
fn test_trim_index_collapses_parse_failures() {
let input = v(r#"{
"total_files": 100,
"files_parsed": 95,
"successful_parses": 95,
"failed_parses": 5,
"total_signatures": 200,
"pdg_nodes": 1000,
"pdg_edges": 2000,
"indexed_nodes": 1000,
"indexing_time_ms": 1234,
"external_deps_in_lockfile": 50,
"external_deps_resolved": 45,
"external_deps_unresolved": 5
}"#);
let t = trim_index(&input);
assert_eq!(t["parse_failures"], 5);
assert_eq!(t["total_files"], 100);
assert!(t.get("successful_parses").is_none());
assert_eq!(t["external_deps_unresolved"], 5);
assert!(t.get("external_deps_in_lockfile").is_none());
assert!(t.get("external_deps_resolved").is_none());
}
#[test]
fn test_trim_llm_payload_dispatches() {
let search_data = v(r#"{"results": [{"file_path": "/p.rs", "symbol_name": "f"}]}"#);
let out = trim_llm_payload("leindex.search", &search_data);
assert!(out.get("results").is_some());
assert!(out.get("count").is_some());
let edit_data = v(
r#"{"preview_token": "x", "diff": {"file_path": "a.rs", "hunks": []}, "diff_text": "unified", "validation": {}}"#,
);
let out = trim_llm_payload("leindex.edit_preview", &edit_data);
assert!(out.get("diff").is_some());
assert!(out.get("diff_text").is_some());
assert!(out.get("validation").is_none());
let raw = v(r#"{"any": "value", "x": 1}"#);
let out = trim_llm_payload("leindex.something_new", &raw);
assert_eq!(out, raw);
}
#[test]
fn test_trim_llm_payload_preserves_meta_warning() {
let stale_search = v(r#"{
"count": 1,
"results": [{"file_path": "src/foo.rs", "symbol": "main"}],
"_warning": "index is stale (last update: 60s ago) — call leindex.index to refresh"
}"#);
let out = trim_llm_payload("leindex.search", &stale_search);
assert_eq!(out["count"], 1);
assert!(out.get("results").is_some());
assert_eq!(
out["_warning"],
"index is stale (last update: 60s ago) — call leindex.index to refresh"
);
let stale_edit = v(r#"{
"preview_token": "tok",
"diff": {"file_path": "a.rs", "hunks": []},
"_warning": "stale"
}"#);
let out = trim_llm_payload("leindex.edit_apply", &stale_edit);
assert_eq!(out["_warning"], "stale");
let extra = v(r#"{
"results": [{"file_path": "src/foo.rs"}],
"ignored_warning": "should not appear"
}"#);
let out = trim_llm_payload("leindex.search", &extra);
assert!(out.get("ignored_warning").is_none());
}
#[test]
fn test_trim_symbol_lookup_preserves_batch_results() {
let data = v(r#"{
"batch": true,
"count": 2,
"results": [
{
"symbol": "alpha",
"type": "function",
"file": "src/a.rs",
"byte_range": [10, 50],
"complexity": 3,
"language": "rust",
"callers": [],
"callees": [],
"impact_radius": {"affected_symbols": 0, "affected_files": 0}
},
{
"symbol": "beta",
"type": "function",
"file": "src/b.rs",
"byte_range": [60, 90],
"complexity": 1,
"language": "rust",
"callers": [],
"callees": [],
"impact_radius": {"affected_symbols": 0, "affected_files": 0}
}
]
}"#);
let out = trim_llm_payload("leindex.symbol-lookup", &data);
assert_eq!(out["batch"], true);
assert_eq!(out["count"], 2);
let results = out["results"].as_array().expect("results must be an array");
assert_eq!(results.len(), 2);
assert_eq!(results[0]["symbol"], "alpha");
assert_eq!(results[0]["file"], "src/a.rs");
assert_eq!(results[0]["type"], "function");
assert_eq!(results[0]["byte_range"][0], 10);
assert_eq!(results[1]["symbol"], "beta");
assert_eq!(results[1]["file"], "src/b.rs");
}
#[test]
fn test_trim_symbol_lookup_batch_with_lookup_error_entry() {
let data = v(r#"{
"batch": true,
"count": 2,
"results": [
{"symbol": "ok", "type": "function", "file": "src/ok.rs", "byte_range": [0, 1], "complexity": 1, "language": "rust", "callers": [], "callees": [], "impact_radius": {}},
{"symbol": "missing", "error": "Symbol not found"}
]
}"#);
let out = trim_llm_payload("leindex.symbol-lookup", &data);
let results = out["results"].as_array().unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0]["symbol"], "ok");
assert_eq!(results[0]["file"], "src/ok.rs");
assert_eq!(results[1]["symbol"], "missing");
}
#[test]
fn test_trim_read_symbol_includes_dependencies_when_present() {
let data = v(r#"{
"symbol": "compute",
"type": "function",
"file": "src/lib.rs",
"language": "rust",
"complexity": 4,
"line_start": 10,
"line_end": 25,
"doc_comment": "/// Compute result",
"source": "fn compute() {}",
"callers": [],
"callees": [],
"dependencies": ["fn helper_a()", "fn helper_b()"]
}"#);
let out = trim_llm_payload("leindex.read-symbol", &data);
assert_eq!(out["symbol"], "compute");
let deps = out["dependencies"]
.as_array()
.expect("dependencies must be an array");
assert_eq!(deps.len(), 2);
assert_eq!(deps[0], "fn helper_a()");
assert_eq!(deps[1], "fn helper_b()");
}
#[test]
fn test_trim_search_falls_through_explicit_null_context() {
let data = v(r#"{
"query": "compute",
"results": [
{
"file_path": "src/lib.rs",
"symbol_name": "compute",
"symbol_type": "function",
"score": 0.9,
"context": null,
"content": "fn compute() -> i32 { 42 }",
"signature": "fn compute() -> i32"
}
]
}"#);
let out = trim_llm_payload("leindex.search", &data);
let results = out["results"].as_array().unwrap();
let snippet = &results[0]["snippet"];
let s = snippet
.as_str()
.expect("snippet must be a string, not null");
assert!(
s.contains("fn compute()"),
"snippet should contain content body: {}",
s
);
}
#[test]
fn test_trim_search_falls_through_explicit_null_symbol_name() {
let data = v(r#"{
"query": "x",
"results": [
{
"file_path": "src/lib.rs",
"symbol_name": null,
"symbol": "fallback_name",
"symbol_type": "function",
"score": 0.5,
"content": "fn fallback_name() {}"
}
]
}"#);
let out = trim_llm_payload("leindex.search", &data);
let results = out["results"].as_array().unwrap();
assert_eq!(
results[0]["symbol"], "fallback_name",
"explicit null in symbol_name must fall through to symbol"
);
}
#[test]
fn test_trim_search_preserves_pagination_fields() {
let data = v(r#"{
"query": "find me",
"results": [
{"file": "a.rs", "byte_range": [0, 10], "content": "x"}
],
"count": 1,
"offset": 5,
"has_more": true,
"suggestion": "try a broader query"
}"#);
let out = trim_llm_payload("leindex.search", &data);
assert_eq!(out["count"], 1, "count must round-trip");
assert_eq!(out["offset"], 5, "offset must round-trip");
assert_eq!(out["has_more"], true, "has_more must round-trip");
assert_eq!(
out["suggestion"], "try a broader query",
"suggestion must round-trip"
);
assert_eq!(out["results"].as_array().unwrap().len(), 1);
}
#[test]
fn test_trim_search_preserves_pagination_fields_no_more_no_suggestion() {
let data = v(r#"{
"query": "find me",
"results": [{"file": "a.rs", "byte_range": [0, 10], "content": "x"}],
"count": 1,
"offset": 0,
"has_more": false
}"#);
let out = trim_llm_payload("leindex.search", &data);
assert_eq!(out["count"], 1);
assert_eq!(out["offset"], 0);
assert_eq!(out["has_more"], false);
assert!(
out.get("suggestion").is_none(),
"no suggestion key when absent"
);
}
#[test]
fn test_trim_search_borrows_input_array() {
let big_context = "x".repeat(50_000);
let data = v(&format!(
r#"{{
"query": "find me",
"results": [
{{
"file_path": "src/a.rs",
"symbol_name": "alpha",
"symbol_type": "function",
"context": "{}",
"score": {{"overall": 0.91, "tfidf": 0.5, "neural": 0.95}}
}},
{{
"file_path": "src/b.rs",
"symbol": "beta",
"symbol_type": "struct",
"content": "struct Beta {{ x: u32 }}",
"score": 0.42
}}
],
"count": 2,
"offset": 0,
"has_more": false
}}"#,
big_context
));
let out = trim_llm_payload("leindex.search", &data);
let results = out["results"].as_array().unwrap();
assert_eq!(results.len(), 2);
let snippet0 = results[0]["snippet"].as_str().unwrap();
assert_eq!(
snippet0.len(),
243,
"snippet must be 240 chars + '...' ellipsis"
);
assert!(snippet0.ends_with("..."));
assert!(snippet0[..240].chars().all(|c| c == 'x'));
assert_eq!(results[0]["symbol"], "alpha");
assert_eq!(results[0]["file_path"], "src/a.rs");
assert_eq!(results[0]["symbol_type"], "function");
assert!((results[0]["score"].as_f64().unwrap() - 0.91).abs() < 1e-9);
let snippet1 = results[1]["snippet"].as_str().unwrap();
assert_eq!(snippet1, "struct Beta { x: u32 }");
assert_eq!(results[1]["symbol"], "beta");
assert!((results[1]["score"].as_f64().unwrap() - 0.42).abs() < 1e-9);
}
#[test]
fn test_trim_read_symbol_callers_more_flag_no_clone() {
let mut callers = Vec::new();
for i in 0..7 {
callers.push(serde_json::json!({"name": format!("c{}", i), "file": "x.rs", "type": "fn"}));
}
let data = v(&format!(
r#"{{
"symbol": "main",
"type": "function",
"file": "src/main.rs",
"byte_range": [0, 100],
"language": "rust",
"callers": {},
"callees": []
}}"#,
serde_json::to_string(&callers).unwrap()
));
let out = trim_llm_payload("leindex.read-symbol", &data);
assert_eq!(
out["callers"].as_array().unwrap().len(),
5,
"first 5 callers passed through"
);
assert_eq!(
out["callers_more"], true,
"more than 5 callers => callers_more true"
);
let mut small_callers = Vec::new();
for i in 0..3 {
small_callers.push(serde_json::json!({"name": format!("c{}", i)}));
}
let data = v(&format!(
r#"{{
"symbol": "main",
"callers": {},
"callees": []
}}"#,
serde_json::to_string(&small_callers).unwrap()
));
let out = trim_llm_payload("leindex.read-symbol", &data);
assert_eq!(out["callers"].as_array().unwrap().len(), 3);
assert_eq!(
out["callers_more"], false,
"3 callers => callers_more false"
);
}
#[test]
fn test_trim_deep_analyze_caps_at_10_without_clone() {
let mut results = Vec::new();
for i in 0..15 {
results.push(serde_json::json!({
"rank": i,
"file_path": format!("src/f{}.rs", i),
"symbol_name": format!("sym{}", i),
"symbol_type": "function",
"signature": format!("fn sym{}()", i),
"byte_range": [0, 1000],
"language": "rust",
"complexity": 5,
}));
}
let data = v(&format!(
r#"{{
"query": "x",
"tokens_used": 100,
"processing_time_ms": 5,
"context": "some context",
"results": {}
}}"#,
serde_json::to_string(&results).unwrap()
));
let out = trim_llm_payload("leindex.deep-analyze", &data);
assert_eq!(
out["results"].as_array().unwrap().len(),
10,
"cap at 10 entries"
);
assert_eq!(out["results_more"], 5, "results_more = 15 - 10 = 5");
let first = &out["results"].as_array().unwrap()[0];
assert_eq!(first["rank"], 0);
assert_eq!(first["file_path"], "src/f0.rs");
assert_eq!(first["symbol_name"], "sym0");
assert_eq!(first["symbol_type"], "function");
assert_eq!(first["signature"], "fn sym0()");
assert!(
first.get("byte_range").is_none(),
"byte_range must be dropped"
);
assert!(first.get("language").is_none(), "language must be dropped");
assert!(
first.get("complexity").is_none(),
"complexity must be dropped"
);
let mut small = Vec::new();
for i in 0..5 {
small.push(serde_json::json!({"rank": i, "file_path": "a.rs"}));
}
let data = v(&format!(
r#"{{"query": "x", "results": {}}}"#,
serde_json::to_string(&small).unwrap()
));
let out = trim_llm_payload("leindex.deep-analyze", &data);
assert_eq!(out["results"].as_array().unwrap().len(), 5);
assert_eq!(out["results_more"], 0);
}
#[test]
fn test_take_n_borrows_source() {
let mut arr = Vec::new();
for i in 0..100 {
arr.push(serde_json::json!({"rank": i, "payload": "x".repeat(64)}));
}
let v = Value::Array(arr);
let out = take_n(&v, 3);
let out = out.as_array().unwrap();
assert_eq!(out.len(), 3);
assert_eq!(out[0]["rank"], 0);
assert_eq!(out[1]["rank"], 1);
assert_eq!(out[2]["rank"], 2);
}
#[test]
fn test_thin_symbol_map_caps_callers_callees_at_5() {
let mut big_callers = Vec::new();
let mut big_callees = Vec::new();
for i in 0..20 {
big_callers.push(serde_json::json!({"name": format!("c{}", i)}));
big_callees.push(serde_json::json!({"name": format!("e{}", i)}));
}
let sm = serde_json::json!([
{
"name": "foo",
"complexity": 42,
"callers": big_callers,
"callees": big_callees,
},
{
"name": "bar",
"complexity": 7,
"callers": [{"name": "only"}],
"callees": [],
}
]);
let out = thin_symbol_map(&sm);
let arr = out.as_array().unwrap();
assert_eq!(arr.len(), 2);
let foo = &arr[0];
assert!(
foo.get("complexity").is_none(),
"complexity must be dropped"
);
assert_eq!(foo["name"], "foo");
assert_eq!(
foo["callers"].as_array().unwrap().len(),
5,
"callers must cap at 5 even when source has 20"
);
assert_eq!(foo["callers"][0]["name"], "c0");
assert_eq!(foo["callers"][4]["name"], "c4");
assert_eq!(
foo["callees"].as_array().unwrap().len(),
5,
"callees must cap at 5 even when source has 20"
);
let bar = &arr[1];
assert_eq!(bar["callers"].as_array().unwrap().len(), 1);
assert_eq!(bar["callees"].as_array().unwrap().len(), 0);
}
#[test]
fn test_trim_rename_symbol_caps_diffs_at_25() {
let mut diffs = Vec::new();
for i in 0..50 {
diffs.push(serde_json::json!({
"file": format!("src/f{}.rs", i),
"diff": format!("--- a/src/f{}.rs\n+++ b/src/f{}.rs\n@@ -1,1 +1,1 @@\n-x\n+y", i, i),
"diff_text": format!("long echo of diff {}", i),
}));
}
let data = serde_json::json!({
"old_name": "foo",
"new_name": "bar",
"diffs": diffs,
});
let out = trim_rename_symbol(&data);
let shown = out["diffs"].as_array().unwrap();
assert_eq!(shown.len(), 25, "must cap at 25 when source has 50");
assert_eq!(out["diffs_more"], 25, "diffs_more = total - 25");
assert_eq!(out["old_name"], "foo");
assert_eq!(out["new_name"], "bar");
assert!(shown[0].get("diff_text").is_none());
assert_eq!(shown[0]["file"], "src/f0.rs");
}
#[test]
fn test_trim_write_borrows_symbols_array() {
let input = v(r#"{
"success": true,
"file_path": "src/new.rs",
"language": "rust",
"symbols": [
{
"name": "main",
"type": "function",
"byte_range": [0, 50],
"metadata": {"scope": "crate", "visibility": "pub", "attrs": ["inline"]}
},
{
"name": "helper",
"type": "function",
"byte_range": [50, 100],
"metadata": {"scope": "module", "visibility": "pub(crate)", "attrs": []}
}
]
}"#);
let t = trim_write(&input);
assert_eq!(t["success"], true);
assert_eq!(t["file_path"], "src/new.rs");
assert_eq!(t["language"], "rust");
let syms = t["symbols"].as_array().unwrap();
assert_eq!(syms.len(), 2);
for s in syms {
assert!(s.get("byte_range").is_none());
assert!(s.get("metadata").is_none());
assert!(s.get("name").is_some());
assert!(s.get("type").is_some());
}
assert_eq!(syms[0]["name"], "main");
assert_eq!(syms[0]["type"], "function");
assert_eq!(syms[1]["name"], "helper");
assert_eq!(syms[1]["type"], "function");
}
#[test]
fn test_trim_impact_borrows_impact_array() {
let input = v(r#"{
"symbol": "Foo::bar",
"file": "src/foo.rs",
"change_type": "modify",
"risk_level": "medium",
"direct_callers": ["caller_a", "caller_b"],
"transitive_affected_symbols": ["callee_x", "callee_y", "callee_z"],
"transitive_affected_files": 2,
"transitive_callers": 5,
"summary": "Changing 'Foo::bar' directly affects 3 symbols in 2 files (risk: medium)"
}"#);
let t = trim_impact(&input);
assert_eq!(t["symbol"], "Foo::bar");
assert_eq!(t["file"], "src/foo.rs");
assert_eq!(t["change_type"], "modify");
assert_eq!(t["risk_level"], "medium");
assert!(t.get("direct_callers").is_some());
assert!(t.get("transitive_affected_symbols").is_some());
assert!(t.get("transitive_affected_files").is_some());
assert!(t.get("transitive_callers").is_some());
assert!(t.get("summary").is_some());
let callers = t["direct_callers"].as_array().unwrap();
assert_eq!(callers.len(), 2);
assert_eq!(callers[0], "caller_a");
let affected = t["transitive_affected_symbols"].as_array().unwrap();
assert_eq!(affected.len(), 3);
assert_eq!(affected[0], "callee_x");
assert!(t["summary"].as_str().unwrap().contains("3 symbols"));
}
#[test]
fn test_trim_git_status_preserves_pdg_enrichment() {
let input = v(r#"{
"is_git_repo": true,
"branch": "main",
"summary": {"modified": 1, "staged": 0, "untracked": 0},
"modified_files": ["src/main.rs"],
"staged_files": [],
"untracked_files": [],
"changed_symbols": [
{"file": "src/main.rs", "status": "modified", "symbols": [{"name": "main"}]}
],
"pdg_enrichment": {"available": true},
"impact_summary": {
"total_affected_symbols": 3,
"affected_files": ["src/main.rs"],
"pdg_enriched": true
},
"diff": "some diff"
}"#);
let t = trim_git_status(&input);
assert_eq!(t["branch"], "main");
assert!(t["modified_files"].is_array());
assert!(t["changed_symbols"].is_array());
assert!(t.get("pdg_enrichment").is_some());
assert_eq!(t["pdg_enrichment"]["available"], true);
assert!(t.get("impact_summary").is_some());
assert_eq!(t["impact_summary"]["total_affected_symbols"], 3);
}
#[test]
fn test_trim_git_status_pdg_unavailable() {
let input = v(r#"{
"branch": "main",
"summary": {"modified": 0, "staged": 0, "untracked": 0},
"modified_files": [],
"staged_files": [],
"untracked_files": [],
"changed_symbols": [],
"pdg_enrichment": {
"available": false,
"reason": "PDG load failed",
"error": "database error"
},
"impact_summary": {
"total_affected_symbols": 0,
"affected_files": [],
"pdg_enriched": false
}
}"#);
let t = trim_git_status(&input);
assert_eq!(t["pdg_enrichment"]["available"], false);
assert_eq!(t["pdg_enrichment"]["reason"], "PDG load failed");
assert_eq!(t["pdg_enrichment"]["error"], "database error");
assert_eq!(t["impact_summary"]["pdg_enriched"], false);
}