use codescout::agent::Agent;
use codescout::lsp::{MockLspClient, MockLspProvider, SymbolInfo, SymbolKind};
use codescout::tools::symbol::{EditCode, SymbolAt, Symbols};
use codescout::tools::{Tool, ToolContext};
use serde_json::json;
async fn ctx_with_mock(
files: &[(&str, &str)],
build_mock: impl FnOnce(&std::path::Path) -> MockLspClient,
) -> (tempfile::TempDir, ToolContext) {
let dir = tempfile::tempdir().unwrap();
let root = std::fs::canonicalize(dir.path()).unwrap();
std::fs::create_dir_all(root.join(".codescout")).unwrap();
for (name, content) in files {
let path = root.join(name);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, content).unwrap();
}
let mock = build_mock(&root);
let agent = Agent::new(Some(root.clone())).await.unwrap();
let ctx = ToolContext {
agent,
lsp: MockLspProvider::with_client(mock),
output_buffer: std::sync::Arc::new(codescout::tools::output_buffer::OutputBuffer::new(20)),
progress: None,
peer: None,
section_coverage: std::sync::Arc::new(std::sync::Mutex::new(
codescout::tools::section_coverage::SectionCoverage::new(),
)),
guide_hints_emitted: std::sync::Arc::new(parking_lot::Mutex::new(Default::default())),
workspace_override: None,
};
(dir, ctx)
}
fn sym(
name: &str,
start_line: u32,
end_line: u32,
path: impl Into<std::path::PathBuf>,
) -> SymbolInfo {
SymbolInfo {
name: name.to_string(),
name_path: name.to_string(),
kind: SymbolKind::Function,
file: path.into(),
start_line,
end_line,
start_col: 0,
children: vec![],
range_start_line: None,
detail: None,
}
}
fn sym_with_range(
name: &str,
start_line: u32,
end_line: u32,
range_start: u32,
path: impl Into<std::path::PathBuf>,
) -> SymbolInfo {
SymbolInfo {
name: name.to_string(),
name_path: name.to_string(),
kind: SymbolKind::Function,
file: path.into(),
start_line,
end_line,
start_col: 0,
children: vec![],
range_start_line: Some(range_start),
detail: None,
}
}
#[tokio::test]
async fn replace_symbol_trusts_lsp_start_line() {
let src = " }\n\n fn target() {\n old_body();\n }\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(
file.clone(),
vec![sym("target", 0, 4, file)],
)
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": " fn target() {\n new_body();\n }"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("new_body()"),
"replacement body must be applied; got:\n{result}"
);
assert!(
!result.contains("old_body()"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_trusts_lsp_start_with_paren_close() {
let src = " })\n }\n\n fn target() {\n old_body();\n }\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(
file.clone(),
vec![sym("target", 0, 5, file)],
)
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": " fn target() {\n new_body();\n }"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("new_body()"),
"replacement body must be applied; got:\n{result}"
);
assert!(
!result.contains("old_body()"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_clean_start_line() {
let src = "fn foo() {\n old();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(file.clone(), vec![sym("foo", 0, 2, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "foo",
"action": "replace",
"body": "fn foo() {\n new();\n}"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("new()"),
"replacement must apply; got:\n{result}"
);
assert!(
!result.contains("old()"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_rejects_truncated_end_line() {
let src = "fn target() {\n old_body();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(
file.clone(),
vec![sym("target", 0, 1, file)],
)
})
.await;
let err = EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": "fn target() {\n new_body();\n}"
}),
&ctx,
)
.await
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("suspicious range"),
"expected suspicious range error, got: {msg}"
);
let content = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
content.contains("old_body()"),
"file must be unmodified after truncated-range guard; got:\n{content}"
);
}
#[tokio::test]
async fn replace_symbol_round_trip_preserves_attributes() {
let src = "#[test]\n/// A test function\nfn target() {\n old_body();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("target", 2, 4, 0, file)])
})
.await;
let find_result = Symbols
.call(
json!({
"symbol": "target",
"path": "src/lib.rs",
"include_body": true
}),
&ctx,
)
.await
.unwrap();
let body = find_result["symbols"][0]["body"].as_str().unwrap();
assert!(
body.contains("#[test]"),
"symbols body should include attribute; got:\n{body}"
);
let new_body = body.replace("old_body()", "new_body()");
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": new_body
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("#[test]"),
"attribute must be preserved after round-trip; got:\n{result}"
);
assert!(
result.contains("/// A test function"),
"doc comment must be preserved after round-trip; got:\n{result}"
);
assert!(
result.contains("new_body()"),
"new body must be applied; got:\n{result}"
);
assert!(
!result.contains("old_body()"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_round_trip_preserves_python_decorator() {
let src = "@staticmethod\ndef target():\n old_body()\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.py", src)], |root| {
let file = root.join("src/lib.py");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("target", 1, 2, 0, file)])
})
.await;
let find_result = Symbols
.call(
json!({
"symbol": "target",
"path": "src/lib.py",
"include_body": true
}),
&ctx,
)
.await
.unwrap();
let body = find_result["symbols"][0]["body"].as_str().unwrap();
assert!(
body.contains("@staticmethod"),
"body should include decorator; got:\n{body}"
);
let new_body = body.replace("old_body()", "new_body()");
EditCode
.call(
json!({
"path": "src/lib.py",
"symbol": "target",
"action": "replace",
"body": new_body
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.py")).unwrap();
assert!(
result.contains("@staticmethod"),
"decorator must survive round-trip; got:\n{result}"
);
assert!(
result.contains("new_body()"),
"new body must be applied; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_round_trip_preserves_java_annotation() {
let src = "/** Javadoc comment */\n@Override\npublic void target() {\n oldBody();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/Main.java", src)], |root| {
let file = root.join("src/Main.java");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("target", 2, 4, 0, file)])
})
.await;
let find_result = Symbols
.call(
json!({
"symbol": "target",
"path": "src/Main.java",
"include_body": true
}),
&ctx,
)
.await
.unwrap();
let body = find_result["symbols"][0]["body"].as_str().unwrap();
assert!(
body.contains("@Override"),
"body should include annotation; got:\n{body}"
);
assert!(
body.contains("/** Javadoc"),
"body should include Javadoc; got:\n{body}"
);
let new_body = body.replace("oldBody()", "newBody()");
EditCode
.call(
json!({
"path": "src/Main.java",
"symbol": "target",
"action": "replace",
"body": new_body
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/Main.java")).unwrap();
assert!(
result.contains("@Override"),
"annotation must survive; got:\n{result}"
);
assert!(
result.contains("/** Javadoc"),
"Javadoc must survive; got:\n{result}"
);
assert!(
result.contains("newBody()"),
"new body applied; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_round_trip_no_attributes() {
let src = "fn target() {\n old_body();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(
file.clone(),
vec![sym_with_range("target", 0, 2, 0, file)],
)
})
.await;
let find_result = Symbols
.call(
json!({
"symbol": "target",
"path": "src/lib.rs",
"include_body": true
}),
&ctx,
)
.await
.unwrap();
let body = find_result["symbols"][0]["body"].as_str().unwrap();
let new_body = body.replace("old_body()", "new_body()");
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": new_body
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("new_body()"),
"new body must be applied; got:\n{result}"
);
assert!(
!result.contains("old_body()"),
"old body must be gone; got:\n{result}"
);
assert_eq!(result.lines().count(), src.lines().count());
}
#[tokio::test]
async fn replace_symbol_rejects_body_only_new_body_and_restores_file() {
let src = "fn target() {\n original_body();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("target", 0, 2, 0, file)])
})
.await;
let body_only = " new_body();\n";
let err = EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": body_only
}),
&ctx,
)
.await
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("dropped the symbol definition"),
"error must mention dropped symbol; got: {msg}"
);
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert_eq!(
result, src,
"file must be restored to original after rollback"
);
}
#[tokio::test]
async fn replace_symbol_rejects_body_only_for_nested_method() {
let src = "\
class Foo {
void target() {
originalBody();
}
}
";
let (dir, ctx) = ctx_with_mock(&[("src/Foo.java", src)], |root| {
let file = root.join("src/Foo.java");
let mut sym = sym_with_range("target", 1, 3, 1, file.clone());
sym.name_path = "Foo/target".to_string();
MockLspClient::new().with_symbols(file, vec![sym])
})
.await;
let body_only = " newBody();\n";
let err = EditCode
.call(
json!({
"path": "src/Foo.java",
"symbol": "Foo/target",
"action": "replace",
"body": body_only
}),
&ctx,
)
.await
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("dropped the symbol definition"),
"nested method body-only must be caught; got: {msg}"
);
let result = std::fs::read_to_string(dir.path().join("src/Foo.java")).unwrap();
assert_eq!(
result, src,
"file must be restored to original after rollback"
);
}
#[tokio::test]
async fn replace_symbol_rolls_back_when_sibling_method_would_be_dropped() {
let src = "\
struct Foo;
impl Foo {
fn alpha(&self) -> i32 {
1
}
fn beta(&self) -> i32 {
2
}
}
";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let a = SymbolInfo {
name: "a".to_string(),
name_path: "impl Foo/a".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 3,
end_line: 9, start_col: 4,
children: vec![],
range_start_line: Some(3),
detail: None,
};
let b = SymbolInfo {
name: "b".to_string(),
name_path: "impl Foo/b".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 7,
end_line: 9,
start_col: 4,
children: vec![],
range_start_line: Some(7),
detail: None,
};
let impl_block = SymbolInfo {
name: "impl Foo".to_string(),
name_path: "impl Foo".to_string(),
kind: SymbolKind::Class,
file: file.clone(),
start_line: 2,
end_line: 10,
start_col: 0,
children: vec![a, b],
range_start_line: Some(2),
detail: None,
};
MockLspClient::new().with_symbols(file, vec![impl_block])
})
.await;
let new_body = " fn a(&self) -> i32 {\n 99\n }";
let err = EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "impl Foo/a",
"action": "replace",
"body": new_body
}),
&ctx,
)
.await
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("dropped sibling symbols") || msg.contains("overshot"),
"sibling-drop error expected; got: {msg}"
);
assert!(
msg.contains("Foo/beta") || msg.contains("Foo/alpha"),
"error must name the dropped sibling(s); got: {msg}"
);
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert_eq!(
result, src,
"file must be restored after sibling-drop rollback"
);
}
#[tokio::test]
async fn replace_symbol_retries_on_stale_lsp_positions_until_fresh() {
let src = "\
fn filler1() { one(); }
fn filler2() { two(); }
fn target() {
original();
}
";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let stale = vec![sym_with_range("target", 0, 0, 0, file.clone())];
let fresh = vec![sym_with_range("target", 2, 4, 2, file.clone())];
MockLspClient::new().with_symbols_sequence(file, vec![stale, fresh])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": "fn target() {\n new_body();\n}"
}),
&ctx,
)
.await
.expect("retry must recover from a single stale LSP response");
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("new_body()"),
"edit must apply; got:\n{result}"
);
assert!(
!result.contains("original()"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_surfaces_stale_error_after_max_retries() {
let src = "\
fn filler1() { one(); }
fn filler2() { two(); }
fn target() {
original();
}
";
let (_dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let stale = vec![sym_with_range("target", 0, 0, 0, file.clone())];
MockLspClient::new().with_symbols_sequence(file, vec![stale])
})
.await;
let err = EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": "fn target() {\n new_body();\n}"
}),
&ctx,
)
.await
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("stale"),
"error must still mention staleness when retries are exhausted; got: {msg}"
);
}
#[tokio::test]
async fn replace_symbol_round_trip_agent_changes_attribute() {
let src = "#[test]\nfn target() {\n old_body();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("target", 1, 3, 0, file)])
})
.await;
let find_result = Symbols
.call(
json!({
"symbol": "target",
"path": "src/lib.rs",
"include_body": true
}),
&ctx,
)
.await
.unwrap();
let body = find_result["symbols"][0]["body"].as_str().unwrap();
assert!(body.contains("#[test]"), "body should include attribute");
let new_body = body
.replace("#[test]", "#[tokio::test]")
.replace("old_body()", "new_body()");
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": new_body
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("#[tokio::test]"),
"new attribute must be present; got:\n{result}"
);
assert!(
!result.contains("\n#[test]\n"),
"old attribute must be gone; got:\n{result}"
);
assert!(
result.contains("new_body()"),
"new body must be applied; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_round_trip_agent_changes_doc_comment() {
let src = "/// Old documentation\nfn target() {\n old_body();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("target", 1, 3, 0, file)])
})
.await;
let find_result = Symbols
.call(
json!({
"symbol": "target",
"path": "src/lib.rs",
"include_body": true
}),
&ctx,
)
.await
.unwrap();
let body = find_result["symbols"][0]["body"].as_str().unwrap();
let new_body = body
.replace(
"/// Old documentation",
"/// Updated documentation\n/// With extra detail",
)
.replace("old_body()", "new_body()");
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": new_body
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("/// Updated documentation"),
"new doc must be present; got:\n{result}"
);
assert!(
result.contains("/// With extra detail"),
"extra doc line must be present; got:\n{result}"
);
assert!(
!result.contains("/// Old documentation"),
"old doc must be gone; got:\n{result}"
);
assert!(
result.contains("new_body()"),
"new body must be applied; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_preserves_doc_when_new_body_has_no_doc_comment() {
let src = "/// Doc that lives immediately above the target with no blank line.\npub fn documented() -> &'static str {\n \"before\"\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(
file.clone(),
vec![sym_with_range("documented", 1, 3, 1, file)],
)
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "documented",
"action": "replace",
"body": "pub fn documented() -> &'static str {\n \"after\"\n}",
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("/// Doc that lives immediately above"),
"doc comment must survive replace when new_body omits it; got:\n{result}"
);
assert!(
result.contains("\"after\""),
"new body must be applied; got:\n{result}"
);
assert!(
!result.contains("\"before\""),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn insert_code_before_with_range_start_line_inserts_above_attribute() {
let src = "#[test]\nfn target() {}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("target", 1, 1, 0, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"position": "before",
"action": "insert",
"body": "// inserted above"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
let lines: Vec<&str> = result.lines().collect();
assert_eq!(
lines[0], "// inserted above",
"inserted code must be above #[test]; got:\n{result}"
);
assert_eq!(
lines[1], "",
"blank separator line after inserted code; got:\n{result}"
);
assert_eq!(
lines[2], "#[test]",
"#[test] must follow separator; got:\n{result}"
);
assert!(
lines[3].contains("fn target()"),
"fn must follow #[test]; got:\n{result}"
);
}
#[tokio::test]
async fn symbols_body_start_line_field_with_attributes() {
let src = "#[test]\n/// doc\nfn target() {\n body();\n}\n";
let (_dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("target", 2, 4, 0, file)])
})
.await;
let result = Symbols
.call(
json!({
"symbol": "target",
"path": "src/lib.rs",
"include_body": true
}),
&ctx,
)
.await
.unwrap();
let sym = &result["symbols"][0];
assert_eq!(
sym["body_start_line"].as_u64(),
Some(1),
"body_start_line should point to attribute line"
);
assert_eq!(
sym["start_line"].as_u64(),
Some(3),
"start_line should point to fn keyword"
);
let body = sym["body"].as_str().unwrap();
assert!(
body.starts_with("#[test]"),
"body should start with attribute"
);
}
#[tokio::test]
async fn symbols_no_body_start_line_without_include_body() {
let src = "#[test]\nfn target() {}\n";
let (_dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("target", 1, 1, 0, file)])
})
.await;
let result = Symbols
.call(
json!({
"symbol": "target",
"path": "src/lib.rs",
"include_body": false
}),
&ctx,
)
.await
.unwrap();
let sym = &result["symbols"][0];
assert!(
sym.get("body").is_none(),
"body should not be present with explicit include_body=false"
);
assert!(
sym.get("body_start_line").is_none(),
"body_start_line should not be present with explicit include_body=false"
);
}
#[tokio::test]
async fn insert_code_before_walks_past_attributes_and_doc_comments() {
let src = "/// A useful struct.\n#[derive(Clone)]\npub struct Foo {\n x: u32,\n}\n\nconst SENTINEL: &str = \"survives\";\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(file.clone(), vec![sym("Foo", 2, 4, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "Foo",
"position": "before",
"action": "insert",
"body": "const BEFORE: u32 = 1;\n"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
let before_pos = result.find("BEFORE").unwrap();
let doc_pos = result.find("/// A useful").unwrap();
let derive_pos = result.find("#[derive").unwrap();
assert!(
before_pos < doc_pos,
"const must be inserted before the doc comment, got:\n{result}"
);
assert!(
before_pos < derive_pos,
"const must be inserted before #[derive], got:\n{result}"
);
assert!(
result.contains("const SENTINEL"),
"sentinel must survive; got:\n{result}"
);
}
#[tokio::test]
async fn insert_code_before_trusts_lsp_start() {
let src = " }\n\n fn target() {\n }\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(file.clone(), vec![sym("target", 0, 3, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"position": "before",
"action": "insert",
"body": " // inserted\n"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("// inserted"),
"insertion must be present; got:\n{result}"
);
let insert_pos = result.find("// inserted").unwrap();
let brace_pos = result.find(" }").unwrap();
assert!(
insert_pos < brace_pos,
"with trust LSP, insertion at sym.start_line=0 lands before `}}`; got:\n{result}"
);
}
#[tokio::test]
async fn insert_code_after_lands_past_symbol() {
let src = "fn foo() {\n}\n\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(file.clone(), vec![sym("foo", 0, 1, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "foo",
"position": "after",
"action": "insert",
"body": "fn bar() {}\n"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("fn foo()"),
"original must be present; got:\n{result}"
);
assert!(
result.contains("fn bar()"),
"insertion must be present; got:\n{result}"
);
let foo_pos = result.find("fn foo()").unwrap();
let bar_pos = result.find("fn bar()").unwrap();
assert!(
bar_pos > foo_pos,
"bar must be inserted after foo; got:\n{result}"
);
}
#[tokio::test]
async fn insert_code_after_caps_overextended_lsp_end() {
let src = "fn target() {\n body();\n}\nfn following() {\n inside();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(file.clone(), vec![sym("target", 0, 3, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"position": "after",
"action": "insert",
"body": "// inserted\n"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("// inserted"),
"insertion must be present; got:\n{result}"
);
let insert_pos = result.find("// inserted").unwrap();
let following_fn = result.find("fn following()").unwrap();
assert!(
insert_pos < following_fn,
"insertion should land before fn following(), not inside it; got:\n{result}"
);
}
#[tokio::test]
async fn insert_code_after_last_python_method_keeps_trailing_stmt() {
let src = "class C:\n def m(self):\n x = compute(\n a=1,\n )\n\n assert x\n";
let (dir, ctx) = ctx_with_mock(&[("mod.py", src)], |root| {
let file = root.join("mod.py");
let method = SymbolInfo {
name: "m".to_string(),
name_path: "C/m".to_string(),
kind: SymbolKind::Method,
file: file.clone(),
start_line: 1,
end_line: 6,
start_col: 4,
children: vec![],
range_start_line: None,
detail: None,
};
let class = SymbolInfo {
name: "C".to_string(),
name_path: "C".to_string(),
kind: SymbolKind::Class,
file: file.clone(),
start_line: 0,
end_line: 6,
start_col: 0,
children: vec![method],
range_start_line: None,
detail: None,
};
MockLspClient::new().with_symbols(file, vec![class])
})
.await;
EditCode
.call(
json!({
"path": "mod.py",
"symbol": "C/m",
"position": "after",
"action": "insert",
"body": "\n def added(self):\n assert added_marker\n"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("mod.py")).unwrap();
let assert_pos = result
.find("assert x")
.unwrap_or_else(|| panic!("trailing `assert x` vanished; got:\n{result}"));
let added_pos = result
.find("def added")
.unwrap_or_else(|| panic!("inserted method missing; got:\n{result}"));
assert!(
assert_pos < added_pos,
"trailing `assert x` must remain in method `m`, not leak past the inserted \
method; got:\n{result}"
);
}
#[tokio::test]
async fn replace_last_python_method_replaces_trailing_stmt() {
let src = "class C:\n def m(self):\n x = compute(\n a=1,\n )\n\n assert x\n";
let (dir, ctx) = ctx_with_mock(&[("mod.py", src)], |root| {
let file = root.join("mod.py");
let method = SymbolInfo {
name: "m".to_string(),
name_path: "C/m".to_string(),
kind: SymbolKind::Method,
file: file.clone(),
start_line: 1,
end_line: 6,
start_col: 4,
children: vec![],
range_start_line: None,
detail: None,
};
let class = SymbolInfo {
name: "C".to_string(),
name_path: "C".to_string(),
kind: SymbolKind::Class,
file: file.clone(),
start_line: 0,
end_line: 6,
start_col: 0,
children: vec![method],
range_start_line: None,
detail: None,
};
MockLspClient::new().with_symbols(file, vec![class])
})
.await;
EditCode
.call(
json!({
"path": "mod.py",
"symbol": "C/m",
"action": "replace",
"body": " def m(self):\n return 42"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("mod.py")).unwrap();
assert!(
result.contains("return 42"),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("assert x"),
"old trailing `assert x` must be replaced with the rest of m, not left behind; got:\n{result}"
);
}
#[tokio::test]
async fn remove_last_python_method_removes_trailing_stmt() {
let src = "class C:\n def m(self):\n x = compute(\n a=1,\n )\n\n assert x\n";
let (dir, ctx) = ctx_with_mock(&[("mod.py", src)], |root| {
let file = root.join("mod.py");
let method = SymbolInfo {
name: "m".to_string(),
name_path: "C/m".to_string(),
kind: SymbolKind::Method,
file: file.clone(),
start_line: 1,
end_line: 6,
start_col: 4,
children: vec![],
range_start_line: None,
detail: None,
};
let class = SymbolInfo {
name: "C".to_string(),
name_path: "C".to_string(),
kind: SymbolKind::Class,
file: file.clone(),
start_line: 0,
end_line: 6,
start_col: 0,
children: vec![method],
range_start_line: None,
detail: None,
};
MockLspClient::new().with_symbols(file, vec![class])
})
.await;
EditCode
.call(
json!({
"path": "mod.py",
"symbol": "C/m",
"action": "remove"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("mod.py")).unwrap();
assert!(
!result.contains("def m"),
"method `m` must be fully removed; got:\n{result}"
);
assert!(
!result.contains("assert x"),
"trailing `assert x` must be removed with its method, not orphaned; got:\n{result}"
);
}
#[tokio::test]
async fn insert_code_after_rejects_truncated_end_in_nested_fn() {
let src =
"#[cfg(test)]\nmod tests {\n #[test]\n fn target_test() {\n let x = 1;\n assert_eq!(x, 1);\n }\n}\n";
let (_dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let inner = SymbolInfo {
name: "target_test".to_string(),
name_path: "tests/target_test".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 3,
end_line: 4, start_col: 4,
children: vec![],
range_start_line: None,
detail: None,
};
let module = SymbolInfo {
name: "tests".to_string(),
name_path: "tests".to_string(),
kind: SymbolKind::Module,
file: file.clone(),
start_line: 1,
end_line: 7,
start_col: 0,
children: vec![inner],
range_start_line: None,
detail: None,
};
MockLspClient::new().with_symbols(file, vec![module])
})
.await;
let result = EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "tests/target_test",
"position": "after",
"action": "insert",
"body": " #[test]\n fn new_test() {}\n"
}),
&ctx,
)
.await;
let err = result.expect_err("should fail with RecoverableError for truncated end_line");
let msg = format!("{err:#}");
assert!(
msg.contains("suspicious range"),
"error should mention suspicious range; got: {msg}"
);
}
#[tokio::test]
async fn insert_code_after_refuses_when_ast_cannot_pin_symbol_end() {
let src = "\
struct Foo;
impl Foo {
fn alpha(&self) {}
}
";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let alpha = SymbolInfo {
name: "a".to_string(),
name_path: "impl Foo/a".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 3,
end_line: 10,
start_col: 4,
children: vec![],
range_start_line: Some(3),
detail: None,
};
let impl_block = SymbolInfo {
name: "impl Foo".to_string(),
name_path: "impl Foo".to_string(),
kind: SymbolKind::Class,
file: file.clone(),
start_line: 2,
end_line: 4,
start_col: 0,
children: vec![alpha],
range_start_line: Some(2),
detail: None,
};
MockLspClient::new().with_symbols(file, vec![impl_block])
})
.await;
let err = EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "impl Foo/a",
"position": "after",
"action": "insert",
"body": " fn beta(&self) {}\n"
}),
&ctx,
)
.await
.expect_err("insert-after must refuse when AST cannot pin the symbol's end");
let msg = err.to_string();
assert!(
msg.contains("cannot determine end") && msg.contains("AST parse failed"),
"error must explain why it refused; got: {msg}"
);
let unchanged = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert_eq!(
unchanged, src,
"refused insert-after must leave the file unchanged"
);
}
#[tokio::test]
async fn insert_code_after_refuses_when_ast_fails_and_no_parent_clamp() {
let src = "\
fn alpha() {
println!(\"hello\");
}
";
let (_dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let alpha = SymbolInfo {
name: "a".to_string(),
name_path: "a".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 0,
end_line: 1, start_col: 0,
children: vec![],
range_start_line: Some(0),
detail: None,
};
MockLspClient::new().with_symbols(file, vec![alpha])
})
.await;
let result = EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "a",
"position": "after",
"action": "insert",
"body": "fn beta() {}\n"
}),
&ctx,
)
.await;
let err = result.expect_err("must refuse when AST fails and no parent clamp is available");
let msg = format!("{err:#}");
assert!(
msg.contains("AST parse failed") || msg.contains("cannot determine end"),
"error should explain AST failure; got: {msg}"
);
}
#[tokio::test]
async fn remove_symbol_caps_overextended_lsp_end() {
let src = "fn target() {\n // body\n}\nconst SENTINEL: &str = \"survives\";\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(file.clone(), vec![sym("target", 0, 3, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "remove"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
!result.contains("fn target"),
"function must be removed; got:\n{result}"
);
assert!(
result.contains("SENTINEL"),
"SENTINEL must survive — it is outside the true symbol range; got:\n{result}"
);
}
#[tokio::test]
async fn remove_symbol_uses_range_start_line_to_include_doc_comment() {
let src = "fn preceding() {\n // body\n}\nuse std::fmt;\n\n/// A constant.\nconst TARGET: bool = false;\nfn following() {}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("TARGET", 6, 6, 5, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "TARGET",
"action": "remove"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
!result.contains("TARGET"),
"const must be removed; got:\n{result}"
);
assert!(
!result.contains("A constant"),
"doc comment included in range_start_line — must also be removed; got:\n{result}"
);
assert!(
result.contains("fn preceding()"),
"preceding function must survive; got:\n{result}"
);
assert!(
result.contains("fn following()"),
"following function must survive; got:\n{result}"
);
let use_count = result.matches("use std::fmt;").count();
assert_eq!(
use_count, 1,
"use import must not be duplicated; found {use_count} occurrences in:\n{result}"
);
}
#[tokio::test]
async fn remove_symbol_heuristic_fallback_includes_doc_comment() {
let src = "fn preceding() {\n // body\n}\nuse std::fmt;\n\n/// A constant.\nconst TARGET: bool = false;\nfn following() {}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(file.clone(), vec![sym("TARGET", 6, 6, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "TARGET",
"action": "remove"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
!result.contains("TARGET"),
"const must be removed; got:\n{result}"
);
assert!(
!result.contains("A constant"),
"heuristic should walk back past doc comment; got:\n{result}"
);
assert!(
result.contains("fn preceding()"),
"preceding function must survive; got:\n{result}"
);
assert!(
result.contains("fn following()"),
"following function must survive; got:\n{result}"
);
}
#[tokio::test]
async fn remove_symbol_range_start_line_excludes_doc_comment() {
let src = "fn preceding() {\n // body\n}\nuse std::fmt;\n\n/// A constant.\nconst TARGET: bool = false;\nfn following() {}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new()
.with_symbols(file.clone(), vec![sym_with_range("TARGET", 6, 6, 6, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "TARGET",
"action": "remove"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
!result.contains("TARGET"),
"const must be removed; got:\n{result}"
);
assert!(
!result.contains("A constant"),
"doc comment should also be removed (BUG-031 fix); got:\n{result}"
);
}
#[tokio::test]
async fn symbols_name_path_does_not_return_local_variable_children() {
use codescout::lsp::SymbolKind;
let src = "fn my_fn() {\n let local_var = 1;\n}\n";
let (_dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let child = SymbolInfo {
name: "local_var".to_string(),
name_path: "my_fn/local_var".to_string(),
kind: SymbolKind::Variable,
file: file.clone(),
start_line: 1,
end_line: 1,
start_col: 4,
children: vec![],
range_start_line: None,
detail: None,
};
let parent = SymbolInfo {
name: "my_fn".to_string(),
name_path: "my_fn".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 0,
end_line: 2,
start_col: 0,
children: vec![child],
range_start_line: None,
detail: None,
};
MockLspClient::new().with_symbols(file, vec![parent])
})
.await;
let result = Symbols
.call(
json!({
"symbol": "my_fn",
"path": "src/lib.rs"
}),
&ctx,
)
.await
.unwrap();
let symbols = result["symbols"].as_array().unwrap();
assert_eq!(
symbols.len(),
1,
"name_path lookup must return exactly the matching symbol, not its Variable children; got: {symbols:?}"
);
assert_eq!(symbols[0]["name"], "my_fn");
}
#[tokio::test]
async fn symbol_at_def_unknown_identifier_falls_back_to_first_nonwhitespace() {
let src = " let foo = 1;\n";
let (_dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let def_path = root.join("src/lib.rs");
MockLspClient::new().with_definitions(
0,
4,
vec![lsp_types::Location {
uri: url::Url::from_file_path(&def_path)
.unwrap()
.as_str()
.parse()
.unwrap(),
range: lsp_types::Range {
start: lsp_types::Position {
line: 0,
character: 4,
},
end: lsp_types::Position {
line: 0,
character: 7,
},
},
}],
)
})
.await;
let result = SymbolAt
.call(
json!({
"path": "src/lib.rs",
"line": 1,
"fields": ["def"]
}),
&ctx,
)
.await
.expect("should succeed: omitting identifier must not cause 'identifier not found'");
let defs = result["def"]["definitions"].as_array().unwrap();
assert_eq!(
defs.len(),
1,
"mock should return the pre-configured definition at col=4; \
if col is wrong the mock returns [] and the tool errors instead"
);
}
#[tokio::test]
async fn replace_symbol_works_for_python() {
let src = "def greet():\n return 'old'\n";
let (dir, ctx) = ctx_with_mock(&[("greet.py", src)], |root| {
let file = root.join("greet.py");
MockLspClient::new().with_symbols(file.clone(), vec![sym("greet", 0, 1, file)])
})
.await;
EditCode
.call(
json!({ "path": "greet.py", "symbol": "greet",
"action": "replace",
"body": "def greet():\n return 'new'" }),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("greet.py")).unwrap();
assert!(
result.contains("'new'"),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("'old'"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_works_for_typescript() {
let src = "function greet(): string {\n return 'old';\n}\n";
let (dir, ctx) = ctx_with_mock(&[("greet.ts", src)], |root| {
let file = root.join("greet.ts");
MockLspClient::new().with_symbols(file.clone(), vec![sym("greet", 0, 2, file)])
})
.await;
EditCode
.call(
json!({ "path": "greet.ts", "symbol": "greet",
"action": "replace",
"body": "function greet(): string {\n return 'new';\n}" }),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("greet.ts")).unwrap();
assert!(
result.contains("'new'"),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("'old'"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_works_for_javascript() {
let src = "function greet() {\n return 'old';\n}\n";
let (dir, ctx) = ctx_with_mock(&[("greet.js", src)], |root| {
let file = root.join("greet.js");
MockLspClient::new().with_symbols(file.clone(), vec![sym("greet", 0, 2, file)])
})
.await;
EditCode
.call(
json!({ "path": "greet.js", "symbol": "greet",
"action": "replace",
"body": "function greet() {\n return 'new';\n}" }),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("greet.js")).unwrap();
assert!(
result.contains("'new'"),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("'old'"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_works_for_go() {
let src = "func Greet() string {\n\treturn \"old\"\n}\n";
let (dir, ctx) = ctx_with_mock(&[("greet.go", src)], |root| {
let file = root.join("greet.go");
MockLspClient::new().with_symbols(file.clone(), vec![sym("Greet", 0, 2, file)])
})
.await;
EditCode
.call(
json!({ "path": "greet.go", "symbol": "Greet",
"action": "replace",
"body": "func Greet() string {\n\treturn \"new\"\n}" }),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("greet.go")).unwrap();
assert!(
result.contains("\"new\""),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("\"old\""),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_works_for_java() {
let src = "public String greet() {\n return \"old\";\n}\n";
let (dir, ctx) = ctx_with_mock(&[("Greet.java", src)], |root| {
let file = root.join("Greet.java");
MockLspClient::new().with_symbols(file.clone(), vec![sym("greet", 0, 2, file)])
})
.await;
EditCode
.call(
json!({ "path": "Greet.java", "symbol": "greet",
"action": "replace",
"body": "public String greet() {\n return \"new\";\n}" }),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("Greet.java")).unwrap();
assert!(
result.contains("\"new\""),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("\"old\""),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_works_for_kotlin() {
let src = "fun greet(): String {\n return \"old\"\n}\n";
let (dir, ctx) = ctx_with_mock(&[("Greet.kt", src)], |root| {
let file = root.join("Greet.kt");
MockLspClient::new().with_symbols(file.clone(), vec![sym("greet", 0, 2, file)])
})
.await;
EditCode
.call(
json!({ "path": "Greet.kt", "symbol": "greet",
"action": "replace",
"body": "fun greet(): String {\n return \"new\"\n}" }),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("Greet.kt")).unwrap();
assert!(
result.contains("\"new\""),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("\"old\""),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_works_for_c() {
let src = "int greet() {\n return 0;\n}\n";
let (dir, ctx) = ctx_with_mock(&[("greet.c", src)], |root| {
let file = root.join("greet.c");
MockLspClient::new().with_symbols(file.clone(), vec![sym("greet", 0, 2, file)])
})
.await;
EditCode
.call(
json!({ "path": "greet.c", "symbol": "greet",
"action": "replace",
"body": "int greet() {\n return 1;\n}" }),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("greet.c")).unwrap();
assert!(
result.contains("return 1"),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("return 0"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_works_for_cpp() {
let src = "std::string greet() {\n return \"old\";\n}\n";
let (dir, ctx) = ctx_with_mock(&[("greet.cpp", src)], |root| {
let file = root.join("greet.cpp");
MockLspClient::new().with_symbols(file.clone(), vec![sym("greet", 0, 2, file)])
})
.await;
EditCode
.call(
json!({ "path": "greet.cpp", "symbol": "greet",
"action": "replace",
"body": "std::string greet() {\n return \"new\";\n}" }),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("greet.cpp")).unwrap();
assert!(
result.contains("\"new\""),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("\"old\""),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_works_for_ruby() {
let src = "def greet\n 'old'\nend\n";
let (dir, ctx) = ctx_with_mock(&[("greet.rb", src)], |root| {
let file = root.join("greet.rb");
MockLspClient::new().with_symbols(file.clone(), vec![sym("greet", 0, 2, file)])
})
.await;
EditCode
.call(
json!({ "path": "greet.rb", "symbol": "greet",
"action": "replace",
"body": "def greet\n 'new'\nend" }),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("greet.rb")).unwrap();
assert!(
result.contains("'new'"),
"new body must be present; got:\n{result}"
);
assert!(
!result.contains("'old'"),
"old body must be gone; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_caps_overextended_lsp_end() {
let src = "fn target() {\n old_body();\n}\nfn following() {\n inside();\n}\n";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
MockLspClient::new().with_symbols(file.clone(), vec![sym("target", 0, 3, file)])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "target",
"action": "replace",
"body": "fn target() {\n new_body();\n}"
}),
&ctx,
)
.await
.unwrap();
let result = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
result.contains("fn following()"),
"fn following() must still be present; got:\n{result}"
);
assert!(
result.contains("new_body()"),
"replacement body must be present; got:\n{result}"
);
assert!(
!result.contains("old_body()"),
"old body must be gone; got:\n{result}"
);
let replaced_pos = result.find("new_body()").unwrap();
let following_pos = result.find("fn following()").unwrap();
assert!(
following_pos > replaced_pos,
"fn following() must come after replacement; got:\n{result}"
);
}
#[tokio::test]
async fn replace_symbol_child_in_mod_tests_preserves_module_header() {
let src = "\
#[cfg(test)]
mod tests {
#[test]
fn first_test() {
assert!(true);
}
#[test]
fn second_test() {
assert!(false);
}
}
";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let mut parent = SymbolInfo {
name: "tests".to_string(),
name_path: "tests".to_string(),
kind: SymbolKind::Module,
file: file.clone(),
start_line: 1,
end_line: 11,
start_col: 0,
children: vec![],
range_start_line: Some(0),
detail: None,
};
let child1 = SymbolInfo {
name: "first_test".to_string(),
name_path: "tests/first_test".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 3,
end_line: 5,
start_col: 4,
children: vec![],
range_start_line: Some(0), detail: None,
};
let child2 = SymbolInfo {
name: "second_test".to_string(),
name_path: "tests/second_test".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 8,
end_line: 10,
start_col: 4,
children: vec![],
range_start_line: Some(7),
detail: None,
};
parent.children = vec![child1, child2];
MockLspClient::new().with_symbols(file, vec![parent])
})
.await;
let new_body = " #[test]\n fn first_test() {\n assert_eq!(1, 1);\n }";
let result = EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "tests/first_test",
"action": "replace",
"body": new_body,
}),
&ctx,
)
.await
.unwrap();
let content = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
content.contains("#[cfg(test)]"),
"BUG-034: #[cfg(test)] must be preserved; got:\n{content}"
);
assert!(
content.contains("mod tests {"),
"BUG-034: mod tests {{ must be preserved; got:\n{content}"
);
assert!(
content.contains("assert_eq!(1, 1)"),
"new body must be applied; got:\n{content}"
);
assert!(
content.contains("second_test"),
"second_test must be preserved; got:\n{content}"
);
let replaced = result["replaced_lines"].as_str().unwrap();
let start_line: usize = replaced.split('-').next().unwrap().parse().unwrap();
assert!(
start_line >= 3, "BUG-034: replaced_lines should start at or after #[test] (line 3), got: {replaced}"
);
}
#[tokio::test]
async fn bug034_guard_rust_child_in_impl_block_stale_range() {
let src = "\
impl Foo {
/// Does something.
fn method(&self) {
old_body();
}
fn other(&self) {}
}
";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let mut parent = SymbolInfo {
name: "Foo".to_string(),
name_path: "Foo".to_string(),
kind: SymbolKind::Object,
file: file.clone(),
start_line: 0,
end_line: 7,
start_col: 0,
children: vec![],
range_start_line: Some(0),
detail: None,
};
let child1 = SymbolInfo {
name: "method".to_string(),
name_path: "Foo/method".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 2,
end_line: 4,
start_col: 4,
children: vec![],
range_start_line: Some(0), detail: None,
};
let child2 = SymbolInfo {
name: "other".to_string(),
name_path: "Foo/other".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 6,
end_line: 6,
start_col: 4,
children: vec![],
range_start_line: Some(6),
detail: None,
};
parent.children = vec![child1, child2];
MockLspClient::new().with_symbols(file, vec![parent])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "Foo/method",
"action": "replace",
"body": " /// Does something.\n fn method(&self) {\n new_body();\n }",
}),
&ctx,
)
.await
.unwrap();
let content = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
content.contains("impl Foo {"),
"impl Foo {{ must be preserved; got:\n{content}"
);
assert!(
content.contains("new_body()"),
"new body must be applied; got:\n{content}"
);
assert!(
content.contains("fn other"),
"sibling must be preserved; got:\n{content}"
);
}
#[tokio::test]
async fn bug034_guard_rust_impl_correct_range_no_overclamping() {
let src = "\
impl Foo {
/// A doc comment.
#[inline]
fn method(&self) {
old_body();
}
}
";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let mut parent = SymbolInfo {
name: "Foo".to_string(),
name_path: "Foo".to_string(),
kind: SymbolKind::Object,
file: file.clone(),
start_line: 0,
end_line: 6,
start_col: 0,
children: vec![],
range_start_line: Some(0),
detail: None,
};
let child = SymbolInfo {
name: "method".to_string(),
name_path: "Foo/method".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 3,
end_line: 5,
start_col: 4,
children: vec![],
range_start_line: Some(1), detail: None,
};
parent.children = vec![child];
MockLspClient::new().with_symbols(file, vec![parent])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "Foo/method",
"action": "replace",
"body": " /// Updated doc.\n #[inline]\n fn method(&self) {\n new_body();\n }",
}),
&ctx,
)
.await
.unwrap();
let content = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
content.contains("impl Foo {"),
"impl header must be preserved; got:\n{content}"
);
assert!(
content.contains("Updated doc"),
"new doc comment must be present; got:\n{content}"
);
assert!(
!content.contains("A doc comment"),
"old doc comment must be replaced; got:\n{content}"
);
}
#[tokio::test]
async fn bug034_guard_python_decorated_method_stale_range() {
let src = "\
class MyService:
@staticmethod
def handle(request):
return old_response()
def other(self):
pass
";
let (dir, ctx) = ctx_with_mock(&[("service.py", src)], |root| {
let file = root.join("service.py");
let mut parent = SymbolInfo {
name: "MyService".to_string(),
name_path: "MyService".to_string(),
kind: SymbolKind::Class,
file: file.clone(),
start_line: 0,
end_line: 6,
start_col: 0,
children: vec![],
range_start_line: Some(0),
detail: None,
};
let child1 = SymbolInfo {
name: "handle".to_string(),
name_path: "MyService/handle".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 2,
end_line: 3,
start_col: 4,
children: vec![],
range_start_line: Some(0), detail: None,
};
let child2 = SymbolInfo {
name: "other".to_string(),
name_path: "MyService/other".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 5,
end_line: 6,
start_col: 4,
children: vec![],
range_start_line: Some(5),
detail: None,
};
parent.children = vec![child1, child2];
MockLspClient::new().with_symbols(file, vec![parent])
})
.await;
EditCode
.call(
json!({
"path": "service.py",
"symbol": "MyService/handle",
"action": "replace",
"body": " @staticmethod\n def handle(request):\n return new_response()",
}),
&ctx,
)
.await
.unwrap();
let content = std::fs::read_to_string(dir.path().join("service.py")).unwrap();
assert!(
content.contains("class MyService:"),
"class header must be preserved; got:\n{content}"
);
assert!(
content.contains("new_response()"),
"new body must be applied; got:\n{content}"
);
assert!(
content.contains("def other"),
"sibling must be preserved; got:\n{content}"
);
}
#[tokio::test]
async fn bug034_guard_typescript_method_stale_range() {
let src = "\
export class UserService {
private validate(input: string): boolean {
return false;
}
public greet(): string {
return 'hello';
}
}
";
let (dir, ctx) = ctx_with_mock(&[("service.ts", src)], |root| {
let file = root.join("service.ts");
let mut parent = SymbolInfo {
name: "UserService".to_string(),
name_path: "UserService".to_string(),
kind: SymbolKind::Class,
file: file.clone(),
start_line: 0,
end_line: 8,
start_col: 0,
children: vec![],
range_start_line: Some(0),
detail: None,
};
let child1 = SymbolInfo {
name: "validate".to_string(),
name_path: "UserService/validate".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 1,
end_line: 3,
start_col: 4,
children: vec![],
range_start_line: Some(0), detail: None,
};
let child2 = SymbolInfo {
name: "greet".to_string(),
name_path: "UserService/greet".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 5,
end_line: 7,
start_col: 4,
children: vec![],
range_start_line: Some(5),
detail: None,
};
parent.children = vec![child1, child2];
MockLspClient::new().with_symbols(file, vec![parent])
})
.await;
EditCode
.call(
json!({
"path": "service.ts",
"symbol": "UserService/validate",
"action": "replace",
"body": " private validate(input: string): boolean {\n return true;\n }",
}),
&ctx,
)
.await
.unwrap();
let content = std::fs::read_to_string(dir.path().join("service.ts")).unwrap();
assert!(
content.contains("export class UserService {"),
"class header must be preserved; got:\n{content}"
);
assert!(
content.contains("return true"),
"new body must be applied; got:\n{content}"
);
assert!(
content.contains("public greet"),
"sibling must be preserved; got:\n{content}"
);
}
#[tokio::test]
async fn bug034_guard_java_annotated_method_stale_range() {
let src = "\
public class Handler {
@Override
public void process(Request req) {
oldLogic();
}
public void other() {}
}
";
let (dir, ctx) = ctx_with_mock(&[("Handler.java", src)], |root| {
let file = root.join("Handler.java");
let mut parent = SymbolInfo {
name: "Handler".to_string(),
name_path: "Handler".to_string(),
kind: SymbolKind::Class,
file: file.clone(),
start_line: 0,
end_line: 7,
start_col: 0,
children: vec![],
range_start_line: Some(0),
detail: None,
};
let child1 = SymbolInfo {
name: "process".to_string(),
name_path: "Handler/process".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 2,
end_line: 4,
start_col: 4,
children: vec![],
range_start_line: Some(0), detail: None,
};
let child2 = SymbolInfo {
name: "other".to_string(),
name_path: "Handler/other".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 6,
end_line: 6,
start_col: 4,
children: vec![],
range_start_line: Some(6),
detail: None,
};
parent.children = vec![child1, child2];
MockLspClient::new().with_symbols(file, vec![parent])
})
.await;
EditCode
.call(
json!({
"path": "Handler.java",
"symbol": "Handler/process",
"action": "replace",
"body": " @Override\n public void process(Request req) {\n newLogic();\n }",
}),
&ctx,
)
.await
.unwrap();
let content = std::fs::read_to_string(dir.path().join("Handler.java")).unwrap();
assert!(
content.contains("public class Handler {"),
"class header must be preserved; got:\n{content}"
);
assert!(
content.contains("newLogic()"),
"new body must be applied; got:\n{content}"
);
assert!(
content.contains("public void other"),
"sibling must be preserved; got:\n{content}"
);
}
#[tokio::test]
async fn bug034_guard_kotlin_annotated_method_stale_range() {
let src = "\
class Repository {
@Throws(IOException::class)
fun load(id: String): Data {
return oldLoad(id)
}
fun save(data: Data) {}
}
";
let (dir, ctx) = ctx_with_mock(&[("Repository.kt", src)], |root| {
let file = root.join("Repository.kt");
let mut parent = SymbolInfo {
name: "Repository".to_string(),
name_path: "Repository".to_string(),
kind: SymbolKind::Class,
file: file.clone(),
start_line: 0,
end_line: 7,
start_col: 0,
children: vec![],
range_start_line: Some(0),
detail: None,
};
let child1 = SymbolInfo {
name: "load".to_string(),
name_path: "Repository/load".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 2,
end_line: 4,
start_col: 4,
children: vec![],
range_start_line: Some(0), detail: None,
};
let child2 = SymbolInfo {
name: "save".to_string(),
name_path: "Repository/save".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 6,
end_line: 6,
start_col: 4,
children: vec![],
range_start_line: Some(6),
detail: None,
};
parent.children = vec![child1, child2];
MockLspClient::new().with_symbols(file, vec![parent])
})
.await;
EditCode
.call(
json!({
"path": "Repository.kt",
"symbol": "Repository/load",
"action": "replace",
"body": " @Throws(IOException::class)\n fun load(id: String): Data {\n return newLoad(id)\n }",
}),
&ctx,
)
.await
.unwrap();
let content = std::fs::read_to_string(dir.path().join("Repository.kt")).unwrap();
assert!(
content.contains("class Repository {"),
"class header must be preserved; got:\n{content}"
);
assert!(
content.contains("newLoad(id)"),
"new body must be applied; got:\n{content}"
);
assert!(
content.contains("fun save"),
"sibling must be preserved; got:\n{content}"
);
}
#[tokio::test]
async fn bug034_guard_rust_deeply_nested_fn_in_impl_in_mod() {
let src = "\
mod inner {
pub struct Bar;
impl Bar {
pub fn do_thing(&self) {
old();
}
}
}
";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let method = SymbolInfo {
name: "do_thing".to_string(),
name_path: "inner/Bar/do_thing".to_string(),
kind: SymbolKind::Function,
file: file.clone(),
start_line: 4,
end_line: 6,
start_col: 8,
children: vec![],
range_start_line: Some(0), detail: None,
};
let impl_block = SymbolInfo {
name: "Bar".to_string(),
name_path: "inner/Bar".to_string(),
kind: SymbolKind::Object,
file: file.clone(),
start_line: 3,
end_line: 7,
start_col: 4,
children: vec![method],
range_start_line: Some(3),
detail: None,
};
let struct_sym = SymbolInfo {
name: "Bar".to_string(),
name_path: "inner/Bar".to_string(),
kind: SymbolKind::Struct,
file: file.clone(),
start_line: 1,
end_line: 1,
start_col: 4,
children: vec![],
range_start_line: Some(1),
detail: None,
};
let module = SymbolInfo {
name: "inner".to_string(),
name_path: "inner".to_string(),
kind: SymbolKind::Module,
file: file.clone(),
start_line: 0,
end_line: 8,
start_col: 0,
children: vec![struct_sym, impl_block],
range_start_line: Some(0),
detail: None,
};
MockLspClient::new().with_symbols(file, vec![module])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "inner/Bar/do_thing",
"action": "replace",
"body": " pub fn do_thing(&self) {\n new();\n }",
}),
&ctx,
)
.await
.unwrap();
let content = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
content.contains("mod inner {"),
"module header must be preserved; got:\n{content}"
);
assert!(
content.contains("impl Bar {"),
"impl header must be preserved; got:\n{content}"
);
assert!(
content.contains("new()"),
"new body must be applied; got:\n{content}"
);
}
#[tokio::test]
async fn bug034_guard_top_level_function_no_parent_no_regression() {
let src = "\
/// Top-level function.
pub fn standalone() {
old_impl();
}
pub fn other() {}
";
let (dir, ctx) = ctx_with_mock(&[("src/lib.rs", src)], |root| {
let file = root.join("src/lib.rs");
let sym1 = sym_with_range("standalone", 1, 3, 0, file.clone());
let sym2 = sym_with_range("other", 5, 5, 5, file.clone());
MockLspClient::new().with_symbols(file, vec![sym1, sym2])
})
.await;
EditCode
.call(
json!({
"path": "src/lib.rs",
"symbol": "standalone",
"action": "replace",
"body": "/// Top-level function.\npub fn standalone() {\n new_impl();\n}",
}),
&ctx,
)
.await
.unwrap();
let content = std::fs::read_to_string(dir.path().join("src/lib.rs")).unwrap();
assert!(
content.contains("new_impl()"),
"new body must be applied; got:\n{content}"
);
assert!(
!content.contains("old_impl()"),
"old body must be gone; got:\n{content}"
);
assert!(
content.contains("pub fn other"),
"sibling must be preserved; got:\n{content}"
);
}