use std::fs;
use std::process::Command;
use super::helpers::AftProcess;
fn is_on_path(binary: &str) -> bool {
Command::new(binary)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok()
}
#[cfg(unix)]
fn install_tsc_stub(dir: &std::path::Path, file_name: &str) {
use std::os::unix::fs::PermissionsExt;
let bin_dir = dir.join("node_modules").join(".bin");
fs::create_dir_all(&bin_dir).unwrap();
let stub = bin_dir.join("tsc");
fs::write(
&stub,
format!(
"#!/bin/sh\nprintf '%s(1,7): error TS2322: Type \\\"string\\\" is not assignable to type \\\"number\\\".\\n' '{}/{file_name}'\nexit 2\n",
dir.display()
),
)
.unwrap();
let mut perms = fs::metadata(&stub).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&stub, perms).unwrap();
}
#[cfg(unix)]
fn prepend_path(existing_path: &std::ffi::OsStr, dir: &std::path::Path) -> std::ffi::OsString {
let mut paths = std::env::split_paths(existing_path).collect::<Vec<_>>();
paths.insert(0, dir.join("node_modules").join(".bin"));
std::env::join_paths(paths).unwrap()
}
fn format_test_dir(test_name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir()
.join("aft_format_tests")
.join(test_name);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn format_integration_applied_rustfmt() {
if !is_on_path("rustfmt") {
eprintln!("SKIP: rustfmt not on PATH");
return;
}
let dir = format_test_dir("applied_rustfmt");
fs::write(dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
let target = dir.join("format_applied.rs");
let _ = fs::remove_file(&target);
let ugly_code = "fn main( ){ let x=1; }";
let path = prepend_path(&std::env::var_os("PATH").unwrap_or_default(), &dir);
let mut aft = AftProcess::spawn_with_env(&[("PATH", path.as_os_str())]);
aft.configure(&dir);
let resp = aft.send(&format!(
r#"{{"id":"fmt-1","command":"write","file":"{}","content":"{}"}}"#,
target.display(),
ugly_code
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
assert_eq!(
resp["formatted"], true,
"rustfmt should have formatted the file"
);
assert!(
resp.get("format_skipped_reason").is_none() || resp["format_skipped_reason"].is_null(),
"no skip reason when formatted"
);
let on_disk = fs::read_to_string(&target).unwrap();
assert!(
!on_disk.contains("fn main"),
"file should be reformatted, got: {}",
on_disk
);
assert!(
on_disk.contains("fn main()"),
"file should contain properly formatted fn main(), got: {}",
on_disk
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn format_integration_unsupported_language() {
let dir = format_test_dir("unsupported_lang");
let target = dir.join("format_unsupported.txt");
let _ = fs::remove_file(&target);
let path = prepend_path(&std::env::var_os("PATH").unwrap_or_default(), &dir);
let mut aft = AftProcess::spawn_with_env(&[("PATH", path.as_os_str())]);
let resp = aft.send(&format!(
r#"{{"id":"fmt-2","command":"write","file":"{}","content":"hello world"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
assert_eq!(
resp["formatted"], false,
"txt files should not be formatted"
);
assert_eq!(
resp["format_skipped_reason"], "unsupported_language",
"skip reason should be unsupported_language"
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn format_integration_no_formatter_configured() {
let dir = format_test_dir("no_formatter_configured");
let target = dir.join("format_no_formatter_configured.py");
let _ = fs::remove_file(&target);
let mut aft = AftProcess::spawn();
let resp = aft.send(&format!(
r#"{{"id":"fmt-3","command":"write","file":"{}","content":"x = 1"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
assert_eq!(
resp["formatted"], false,
"should not be formatted without formatter"
);
assert_eq!(
resp["format_skipped_reason"], "no_formatter_configured",
"skip reason should be no_formatter_configured"
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn format_integration_formatter_not_installed() {
let dir = format_test_dir("formatter_not_installed");
fs::write(dir.join("biome.json"), "{}\n").unwrap();
let target = dir.join("format_formatter_not_installed.ts");
let _ = fs::remove_file(&target);
let path = prepend_path(&std::ffi::OsString::new(), &dir);
let mut aft = AftProcess::spawn_with_env(&[("PATH", path.as_os_str())]);
let cfg = aft.configure(&dir);
assert_eq!(cfg["success"], true, "configure should succeed: {:?}", cfg);
let resp = aft.send(&format!(
r#"{{"id":"fmt-3b","command":"write","file":"{}","content":"const x = 1;\n"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
assert_eq!(resp["formatted"], false);
assert_eq!(
resp["format_skipped_reason"], "formatter_not_installed",
"skip reason should be formatter_not_installed: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn format_integration_add_import_with_format() {
let dir = format_test_dir("add_import");
fs::write(dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
let target = dir.join("format_add_import.rs");
fs::write(&target, "fn main() {\n println!(\"hello\");\n}\n").unwrap();
let mut aft = AftProcess::spawn();
aft.configure(&dir);
let resp = aft.send(&format!(
r#"{{"id":"fmt-4","command":"add_import","file":"{}","module":"std::collections::HashMap"}}"#,
target.display()
));
assert_eq!(
resp["success"], true,
"add_import should succeed: {:?}",
resp
);
assert_eq!(resp["added"], true);
assert!(
resp.get("formatted").is_some() && !resp["formatted"].is_null(),
"formatted field must be present in add_import response: {:?}",
resp
);
let on_disk = fs::read_to_string(&target).unwrap();
assert!(
on_disk.contains("use std::collections::HashMap"),
"import should be in file, got: {}",
on_disk
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn format_integration_edit_symbol_with_format() {
let dir = format_test_dir("edit_symbol");
fs::write(dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
let target = dir.join("format_edit_symbol.rs");
fs::write(&target, "fn greet() {\n println!(\"hi\");\n}\n").unwrap();
let mut aft = AftProcess::spawn();
aft.configure(&dir);
let new_body = r#"fn greet() {\n println!(\"hello world\");\n}"#;
let resp = aft.send(&format!(
r#"{{"id":"fmt-5","command":"edit_symbol","file":"{}","symbol":"greet","operation":"replace","content":"{}"}}"#,
target.display(),
new_body
));
assert_eq!(
resp["success"], true,
"edit_symbol should succeed: {:?}",
resp
);
assert!(
resp.get("formatted").is_some() && !resp["formatted"].is_null(),
"formatted field must be present in edit_symbol response: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn format_integration_fields_always_present() {
let dir = format_test_dir("fields_present");
fs::write(dir.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
let md_target = dir.join("format_fields_check.md");
let _ = fs::remove_file(&md_target);
let mut aft = AftProcess::spawn();
aft.configure(&dir);
let resp = aft.send(&format!(
r#"{{"id":"fmt-6a","command":"write","file":"{}","content":"Hello markdown"}}"#,
md_target.display()
));
assert_eq!(
resp["success"], true,
"write to .md should succeed: {:?}",
resp
);
assert!(
resp.get("formatted").is_some(),
"formatted field must be present even for unsupported languages: {:?}",
resp
);
assert_eq!(resp["formatted"], false);
assert_eq!(resp["format_skipped_reason"], "unsupported_language");
let rs_target = dir.join("format_fields_check.rs");
let _ = fs::remove_file(&rs_target);
let resp2 = aft.send(&format!(
r#"{{"id":"fmt-6b","command":"write","file":"{}","content":"fn main() {{}}"}}"#,
rs_target.display()
));
assert_eq!(
resp2["success"], true,
"write to .rs should succeed: {:?}",
resp2
);
assert!(
resp2.get("formatted").is_some(),
"formatted field must be present for .rs files: {:?}",
resp2
);
let _ = fs::remove_file(&md_target);
let _ = fs::remove_file(&rs_target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn validate_full_default_no_errors() {
let dir = format_test_dir("validate_default");
let target = dir.join("validate_default.rs");
let _ = fs::remove_file(&target);
let mut aft = AftProcess::spawn();
let resp = aft.send(&format!(
r#"{{"id":"val-1","command":"write","file":"{}","content":"fn main() {{}}"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
let has_errors = resp.get("validation_errors").is_some()
&& !resp["validation_errors"].is_null()
&& resp["validation_errors"]
.as_array()
.map_or(false, |a| !a.is_empty());
assert!(
!has_errors,
"validation_errors should be absent or empty without validate:full, got: {:?}",
resp
);
assert!(
resp.get("validate_skipped_reason").is_none() || resp["validate_skipped_reason"].is_null(),
"validate_skipped_reason should not be present without validate:full: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn validate_on_edit_full_from_config_runs_checker() {
if !cfg!(unix) {
eprintln!("SKIP: tsc stub test requires unix executable permissions");
return;
}
let dir = format_test_dir("validate_config_full");
let target = dir.join("validate_config_full.ts");
let _ = fs::remove_file(&target);
fs::write(dir.join("tsconfig.json"), "{}\n").unwrap();
install_tsc_stub(&dir, "validate_config_full.ts");
let mut aft = AftProcess::spawn();
let cfg = aft.send(&format!(
r#"{{"id":"cfg-val-full","command":"configure","project_root":"{}","validate_on_edit":"full","checker":{{"typescript":"tsc"}}}}"#,
dir.display()
));
assert_eq!(cfg["success"], true, "configure should succeed: {:?}", cfg);
let resp = aft.send(&format!(
r#"{{"id":"val-config-full","command":"write","file":"{}","content":"const x: number = \"oops\";\n"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
let errors = resp["validation_errors"]
.as_array()
.expect("validate_on_edit:full should include validation_errors");
assert!(
!errors.is_empty(),
"broken TypeScript types should produce validation_errors: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn validate_on_edit_off_from_config_skips_checker() {
let dir = format_test_dir("validate_config_off");
let target = dir.join("validate_config_off.ts");
let _ = fs::remove_file(&target);
fs::write(dir.join("tsconfig.json"), "{}\n").unwrap();
#[cfg(unix)]
install_tsc_stub(&dir, "validate_config_off.ts");
let mut aft = AftProcess::spawn();
let cfg = aft.send(&format!(
r#"{{"id":"cfg-val-off","command":"configure","project_root":"{}","validate_on_edit":"off"}}"#,
dir.display()
));
assert_eq!(cfg["success"], true, "configure should succeed: {:?}", cfg);
let resp = aft.send(&format!(
r#"{{"id":"val-config-off","command":"write","file":"{}","content":"const x: number = \"oops\";\n"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
let has_errors = resp.get("validation_errors").is_some()
&& !resp["validation_errors"].is_null()
&& resp["validation_errors"]
.as_array()
.map_or(false, |errors| !errors.is_empty());
assert!(
!has_errors,
"validate_on_edit:off should not produce validation_errors: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn validate_full_with_checker() {
if !is_on_path("cargo") {
eprintln!("SKIP: cargo not on PATH");
return;
}
let dir = format_test_dir("validate_valid");
let target = dir.join("validate_valid.rs");
let _ = fs::remove_file(&target);
let mut aft = AftProcess::spawn();
let resp = aft.send(&format!(
r#"{{"id":"val-2","command":"write","file":"{}","content":"fn main() {{}}","validate":"full"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
let has_validation =
resp.get("validation_errors").is_some() || resp.get("validate_skipped_reason").is_some();
assert!(
has_validation,
"validate:full should produce validation_errors or validate_skipped_reason: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn validate_full_unsupported_language() {
let dir = format_test_dir("validate_unsupported");
let target = dir.join("validate_unsupported.txt");
let _ = fs::remove_file(&target);
let mut aft = AftProcess::spawn();
let resp = aft.send(&format!(
r#"{{"id":"val-3","command":"write","file":"{}","content":"hello","validate":"full"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
assert_eq!(
resp["validate_skipped_reason"], "unsupported_language",
"should skip validation for unsupported language: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn validate_full_no_checker_configured() {
let dir = format_test_dir("validate_no_checker_configured");
let target = dir.join("validate_no_checker_configured.ts");
let _ = fs::remove_file(&target);
let mut aft = AftProcess::spawn();
let resp = aft.send(&format!(
r#"{{"id":"val-3b","command":"write","file":"{}","content":"const x = 1;\n","validate":"full"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
assert_eq!(
resp["validate_skipped_reason"], "no_checker_configured",
"should skip validation without checker config: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn validate_full_checker_not_installed() {
let dir = format_test_dir("validate_checker_not_installed");
fs::write(dir.join("tsconfig.json"), "{}\n").unwrap();
let target = dir.join("validate_checker_not_installed.ts");
let _ = fs::remove_file(&target);
let path = prepend_path(&std::ffi::OsString::new(), &dir);
let mut aft = AftProcess::spawn_with_env(&[("PATH", path.as_os_str())]);
let cfg = aft.configure(&dir);
assert_eq!(cfg["success"], true, "configure should succeed: {:?}", cfg);
let resp = aft.send(&format!(
r#"{{"id":"val-3c","command":"write","file":"{}","content":"const x = 1;\n","validate":"full"}}"#,
target.display()
));
assert_eq!(resp["success"], true, "write should succeed: {:?}", resp);
assert_eq!(
resp["validate_skipped_reason"], "checker_not_installed",
"should report missing checker binary: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}
#[test]
fn validate_full_flows_through_add_import() {
let dir = format_test_dir("validate_import");
let target = dir.join("validate_import.rs");
fs::write(&target, "fn main() {\n println!(\"hello\");\n}\n").unwrap();
let mut aft = AftProcess::spawn();
let resp = aft.send(&format!(
r#"{{"id":"val-4","command":"add_import","file":"{}","module":"std::collections::HashMap","validate":"full"}}"#,
target.display()
));
assert_eq!(
resp["success"], true,
"add_import should succeed: {:?}",
resp
);
let has_validation =
resp.get("validation_errors").is_some() || resp.get("validate_skipped_reason").is_some();
assert!(
has_validation,
"validate:full should produce validation_errors or validate_skipped_reason via add_import: {:?}",
resp
);
let _ = fs::remove_file(&target);
let status = aft.shutdown();
assert!(status.success());
}