use crate::helpers::{fixture_path, AftProcess};
fn setup_inline_fixture() -> (tempfile::TempDir, String) {
let fixtures = fixture_path("inline_symbol");
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)
}
fn configure(aft: &mut AftProcess, root: &str) {
let resp = aft.send(&format!(
r#"{{"id":"cfg","command":"configure","harness":"opencode","project_root":{}}}"#,
crate::helpers::json_string(&root)
));
assert_eq!(
resp["success"], true,
"configure should succeed: {:?}",
resp
);
}
#[test]
fn inline_symbol_exported_function() {
let tmp = tempfile::tempdir().expect("create temp dir");
let file = tmp.path().join("exported.ts");
std::fs::write(
&file,
"export function double(n: number): number {\n return n * 2;\n}\n\nexport function useIt(x: number): number {\n return double(x);\n}\n",
)
.expect("write test file");
let root = tmp.path().display().to_string();
let mut aft = AftProcess::spawn();
configure(&mut aft, &root);
let resp = aft.send(&format!(
r#"{{"id":"1","command":"inline_symbol","file":{},"symbol":"double","call_site_line":6}}"#,
crate::helpers::json_string(&file.display())
));
assert_eq!(
resp["success"], true,
"inline of exported function should succeed: {:?}",
resp
);
assert_eq!(resp["symbol"], "double");
let content = std::fs::read_to_string(&file).expect("read file");
assert!(
!content.contains("double(x)"),
"call should be replaced:\n{}",
content
);
assert!(
content.contains("x * 2"),
"body should be substituted with argument:\n{}",
content
);
aft.shutdown();
}
#[test]
fn inline_symbol_basic_ts() {
let (_tmp, root) = setup_inline_fixture();
let mut aft = AftProcess::spawn();
configure(&mut aft, &root);
let file = format!("{}/sample.ts", root);
let resp = aft.send(&format!(
r#"{{"id":"1","command":"inline_symbol","file":{},"symbol":"add","call_site_line":11}}"#,
crate::helpers::json_string(&file)
));
assert_eq!(resp["success"], true, "inline should succeed: {:?}", resp);
assert_eq!(resp["symbol"], "add");
assert_eq!(resp["call_context"], "assignment");
assert!(
resp["substitutions"].as_u64().unwrap() > 0,
"should have substitutions"
);
let content = std::fs::read_to_string(&file).expect("read file");
assert!(
!content.contains("add(x, y)"),
"call should be replaced:\n{}",
content
);
aft.shutdown();
}
#[test]
fn inline_symbol_expression_body() {
let (_tmp, root) = setup_inline_fixture();
let mut aft = AftProcess::spawn();
configure(&mut aft, &root);
let file = format!("{}/sample.ts", root);
let resp = aft.send(&format!(
r#"{{"id":"1","command":"inline_symbol","file":{},"symbol":"double","call_site_line":18}}"#,
crate::helpers::json_string(&file)
));
assert_eq!(
resp["success"], true,
"inline expression body should succeed: {:?}",
resp
);
assert_eq!(resp["symbol"], "double");
let content = std::fs::read_to_string(&file).expect("read file");
assert!(
!content.contains("double(5)"),
"call should be replaced:\n{}",
content
);
aft.shutdown();
}
#[test]
fn inline_symbol_python() {
let (_tmp, root) = setup_inline_fixture();
let mut aft = AftProcess::spawn();
configure(&mut aft, &root);
let file = format!("{}/sample.py", root);
let resp = aft.send(&format!(
r#"{{"id":"1","command":"inline_symbol","file":{},"symbol":"add","call_site_line":10}}"#,
crate::helpers::json_string(&file)
));
assert_eq!(
resp["success"], true,
"python inline should succeed: {:?}",
resp
);
assert_eq!(resp["symbol"], "add");
let content = std::fs::read_to_string(&file).expect("read file");
assert!(
!content.contains("add(x, y)"),
"call should be replaced:\n{}",
content
);
aft.shutdown();
}
#[test]
fn inline_symbol_multiple_returns() {
let (_tmp, root) = setup_inline_fixture();
let mut aft = AftProcess::spawn();
configure(&mut aft, &root);
let file = format!("{}/sample_multi.ts", root);
let resp = aft.send(&format!(
r#"{{"id":"1","command":"inline_symbol","file":{},"symbol":"multiReturn","call_site_line":9}}"#,
crate::helpers::json_string(&file)
));
assert_eq!(resp["success"], false, "should fail: {:?}", resp);
assert_eq!(resp["code"], "multiple_returns");
assert!(
resp["return_count"].as_u64().unwrap() >= 2,
"should report return count"
);
aft.shutdown();
}
#[test]
fn inline_symbol_scope_conflict() {
let (_tmp, root) = setup_inline_fixture();
let mut aft = AftProcess::spawn();
configure(&mut aft, &root);
let file = format!("{}/sample_conflict.ts", root);
let resp = aft.send(&format!(
r#"{{"id":"1","command":"inline_symbol","file":{},"symbol":"compute","call_site_line":9}}"#,
crate::helpers::json_string(&file)
));
assert_eq!(
resp["success"], false,
"should fail with scope_conflict: {:?}",
resp
);
assert_eq!(resp["code"], "scope_conflict");
let conflicting = resp["conflicting_names"]
.as_array()
.expect("conflicting_names array");
assert!(
!conflicting.is_empty(),
"should report at least one conflicting name: {:?}",
conflicting
);
let suggestions = resp["suggestions"].as_array().expect("suggestions array");
assert!(
!suggestions.is_empty(),
"should include rename suggestions: {:?}",
suggestions
);
for s in suggestions {
assert!(
s["original"].as_str().is_some(),
"suggestion should have 'original': {:?}",
s
);
assert!(
s["suggested"].as_str().is_some(),
"suggestion should have 'suggested': {:?}",
s
);
}
aft.shutdown();
}
#[test]
fn inline_symbol_preserves_call_site_indent() {
let tmp = tempfile::tempdir().expect("temp dir");
let file = tmp.path().join("indent.ts");
std::fs::write(
&file,
"function helper(x: number): number {\n return x * 2;\n}\n\nexport function main() {\n const result = helper(5);\n console.log(result);\n}\n",
)
.expect("write fixture");
let mut aft = AftProcess::spawn();
configure(&mut aft, &tmp.path().display().to_string());
let resp = aft.send(&format!(
r#"{{"id":"1","command":"inline_symbol","file":{},"symbol":"helper","call_site_line":6}}"#,
crate::helpers::json_string(&file.display())
));
assert_eq!(resp["success"], true, "inline should succeed: {:?}", resp);
let content = std::fs::read_to_string(&file).expect("read file");
assert!(
content.contains("\n const result = 5 * 2;\n"),
"expected 2-space indent on inlined line, got:\n{}",
content
);
assert!(
!content.contains(" const result"),
"indent should not be doubled, got:\n{}",
content
);
aft.shutdown();
}
#[test]
fn inline_symbol_does_not_substitute_shadowed_arrow_parameter() {
let tmp = tempfile::tempdir().expect("temp dir");
let file = tmp.path().join("shadowed_arrow.ts");
std::fs::write(
&file,
"function f(x: number): number {\n return x + items.map(x => x + 1)[0];\n}\n\nconst items = [1, 2];\n\nfunction main() {\n const result = f(5);\n}\n",
)
.expect("write fixture");
let mut aft = AftProcess::spawn();
configure(&mut aft, &tmp.path().display().to_string());
let resp = aft.send(&format!(
r#"{{"id":"1","command":"inline_symbol","file":{},"symbol":"f","call_site_line":8}}"#,
crate::helpers::json_string(&file.display())
));
assert_eq!(resp["success"], true, "inline should succeed: {:?}", resp);
let content = std::fs::read_to_string(&file).expect("read file");
let expected = " const result = 5 + items.map(x => x + 1)[0];";
assert!(
content.contains(expected),
"outer `x` should be substituted while nested arrow `x` remains:\n{}",
content
);
assert!(
!content.contains("items.map(5 => 5 + 1)"),
"nested arrow parameter must not be substituted:\n{}",
content
);
aft.shutdown();
}
#[test]
fn inline_symbol_matches_multiline_call_starting_on_target_line() {
let tmp = tempfile::tempdir().expect("temp dir");
let file = tmp.path().join("multiline.ts");
std::fs::write(
&file,
"function helper(a: number, b: number): number {\n return a + b;\n}\n\nexport function main() {\n const result = helper(\n 1,\n 2,\n );\n console.log(result);\n}\n",
)
.expect("write fixture");
let mut aft = AftProcess::spawn();
configure(&mut aft, &tmp.path().display().to_string());
let resp = aft.send(&format!(
r#"{{"id":"multiline","command":"inline_symbol","file":{},"symbol":"helper","call_site_line":6}}"#,
crate::helpers::json_string(&file.display())
));
assert_eq!(
resp["success"], true,
"inline should match multiline call by start line: {:?}",
resp
);
let content = std::fs::read_to_string(&file).expect("read file");
assert!(
!content.contains("helper(\n"),
"multiline call should be replaced:\n{}",
content
);
assert!(
content.contains("\n const result = 1 + 2;\n"),
"expected inlined expression, got:\n{}",
content
);
aft.shutdown();
}
#[test]
fn inline_symbol_preserves_nested_multiline_indentation() {
let tmp = tempfile::tempdir().expect("temp dir");
let file = tmp.path().join("nested_indent.ts");
std::fs::write(
&file,
r#"function helper(items: number[]): void {
for (const item of items) {
if (item > 0) {
console.log(item);
}
}
}
const values = [1, 2];
export function main() {
helper(values);
}
"#,
)
.expect("write fixture");
let mut aft = AftProcess::spawn();
configure(&mut aft, &tmp.path().display().to_string());
let resp = aft.send(&format!(
r#"{{"id":"nested-indent","command":"inline_symbol","file":{},"symbol":"helper","call_site_line":12}}"#,
crate::helpers::json_string(&file.display())
));
assert_eq!(resp["success"], true, "inline should succeed: {resp:?}");
let content = std::fs::read_to_string(&file).expect("read file");
let expected =
" for (const item of values) {\n if (item > 0) {\n console.log(item);\n }\n }";
assert!(
content.contains(expected),
"nested relative indentation should be preserved:\n{content}"
);
aft.shutdown();
}
#[test]
fn inline_symbol_standalone_return_expression_becomes_expression_statement() {
let tmp = tempfile::tempdir().expect("temp dir");
let file = tmp.path().join("standalone_return.ts");
std::fs::write(
&file,
r#"function helper(): number {
audit();
return sideEffect();
}
function audit(): void {}
function sideEffect(): number { return 1; }
export function main() {
helper();
}
"#,
)
.expect("write fixture");
let mut aft = AftProcess::spawn();
configure(&mut aft, &tmp.path().display().to_string());
let resp = aft.send(&format!(
r#"{{"id":"standalone-return","command":"inline_symbol","file":{},"symbol":"helper","call_site_line":10}}"#,
crate::helpers::json_string(&file.display())
));
assert_eq!(resp["success"], true, "inline should succeed: {resp:?}");
let content = std::fs::read_to_string(&file).expect("read file");
assert!(
content.contains(" audit();\n sideEffect();"),
"return expression should be kept as an expression statement:\n{content}"
);
aft.shutdown();
}
#[test]
fn inline_symbol_maps_destructured_default_and_rest_parameters() {
let tmp = tempfile::tempdir().expect("temp dir");
let file = tmp.path().join("params.ts");
std::fs::write(
&file,
r#"type Point = { x: number; y: number };
function describe({ x, y }: Point, [first, second]: number[], label = 'pt', ...rest: string[]): string {
return `${label}:${x + y}:${first + second}:${rest.join(',')}`;
}
const point = { x: 1, y: 2 };
const pair = [3, 4];
export function main() {
const result = describe(point, pair, 'p', 'a', 'b');
console.log(result);
}
"#,
)
.expect("write fixture");
let mut aft = AftProcess::spawn();
configure(&mut aft, &tmp.path().display().to_string());
let resp = aft.send(&format!(
r#"{{"id":"params","command":"inline_symbol","file":{},"symbol":"describe","call_site_line":11}}"#,
crate::helpers::json_string(&file.display())
));
assert_eq!(resp["success"], true, "inline should succeed: {resp:?}");
let content = std::fs::read_to_string(&file).expect("read file");
assert!(
content.contains("point.x + point.y"),
"object destructuring bindings should map to variable properties:\n{content}"
);
assert!(
content.contains("pair[0] + pair[1]"),
"array destructuring bindings should map to indexed accesses:\n{content}"
);
assert!(
content.contains("['a', 'b'].join(',')"),
"rest parameter should map to an array literal of remaining args:\n{content}"
);
assert!(
!content.contains("describe(point"),
"call should be replaced:\n{content}"
);
aft.shutdown();
}
#[test]
fn inline_symbol_refuses_destructuring_from_non_variable_argument() {
let tmp = tempfile::tempdir().expect("temp dir");
let file = tmp.path().join("params_mismatch.ts");
std::fs::write(
&file,
r#"type Point = { x: number; y: number };
function sumPoint({ x, y }: Point): number {
return x + y;
}
export function main() {
const result = sumPoint({ x: 1, y: 2 });
console.log(result);
}
"#,
)
.expect("write fixture");
let mut aft = AftProcess::spawn();
configure(&mut aft, &tmp.path().display().to_string());
let resp = aft.send(&format!(
r#"{{"id":"params-mismatch","command":"inline_symbol","file":{},"symbol":"sumPoint","call_site_line":8}}"#,
crate::helpers::json_string(&file.display())
));
assert_eq!(resp["success"], false, "inline should fail: {resp:?}");
assert_eq!(resp["code"], "param_mismatch", "wrong error: {resp:?}");
aft.shutdown();
}