//! `edit` 工具的单元测试。 //! Unit tests for the `edit` tool. use focus_core::tool::Tool; use focus_json::JsonValue; use focus_tools::EditTool; use std::fs; use std::path::PathBuf; fn temp_dir(tag: &str) -> PathBuf { let d = std::env::temp_dir().join(format!("focus-tools-edit-{}-{}", tag, std::process::id())); let _ = fs::remove_dir_all(&d); fs::create_dir_all(&d).unwrap(); d } fn edit_args(path: &str, edits: Vec) -> JsonValue { let mut args = JsonValue::obj(); args.insert("path", path.into()).ok(); args.insert("edits", JsonValue::Arr(edits)).ok(); args } fn pair(old: &str, new: &str) -> JsonValue { let mut e = JsonValue::obj(); e.insert("oldString", old.into()).ok(); e.insert("newString", new.into()).ok(); e } #[test] fn applies_single_replacement() { let dir = temp_dir("single"); fs::write(dir.join("f.txt"), "foo bar foo").unwrap(); let tool = EditTool::new(&dir); let r = tool .execute("id", &edit_args("f.txt", vec![pair("bar", "baz")]), None) .unwrap(); assert!(r.content[0].as_text().unwrap().text.contains("1 edit")); assert_eq!( fs::read_to_string(dir.join("f.txt")).unwrap(), "foo baz foo" ); } #[test] fn applies_multiple_edits_in_order() { let dir = temp_dir("multi"); fs::write(dir.join("f.txt"), "a b c").unwrap(); let tool = EditTool::new(&dir); let r = tool .execute( "id", &edit_args( "f.txt", vec![pair("a", "1"), pair("b", "2"), pair("c", "3")], ), None, ) .unwrap(); assert!(r.content[0].as_text().unwrap().text.contains("3 edit")); assert_eq!(fs::read_to_string(dir.join("f.txt")).unwrap(), "1 2 3"); } #[test] fn ambiguous_match_requires_occurrence() { let dir = temp_dir("ambig"); fs::write(dir.join("f.txt"), "x x x").unwrap(); let tool = EditTool::new(&dir); // 无 occurrence → 报错。 // No occurrence → error. let err = tool.execute("id", &edit_args("f.txt", vec![pair("x", "y")]), None); assert!(err.is_err()); let msg = err.unwrap_err().to_string(); assert!(msg.contains("3 times"), "got: {}", msg); // 带 occurrence → 只替换第 2 个。 // With occurrence → replace only the 2nd. let mut e = pair("x", "y"); e.insert("occurrence", 2u64.into()).ok(); tool.execute("id", &edit_args("f.txt", vec![e]), None) .unwrap(); assert_eq!(fs::read_to_string(dir.join("f.txt")).unwrap(), "x y x"); } #[test] fn not_found_reports_descriptively() { let dir = temp_dir("nf"); fs::write(dir.join("f.txt"), "hello").unwrap(); let tool = EditTool::new(&dir); let err = tool .execute("id", &edit_args("f.txt", vec![pair("zzz", "y")]), None) .unwrap_err(); assert!(err.to_string().contains("not found"), "got: {}", err); } #[test] fn preserves_crlf_when_not_matched() { let dir = temp_dir("crlf"); fs::write(dir.join("f.txt"), "a\r\nb\r\nc\r\n").unwrap(); let tool = EditTool::new(&dir); tool.execute("id", &edit_args("f.txt", vec![pair("b", "B")]), None) .unwrap(); // 未匹配的 CRLF 原样保留。 // Unmatched CRLF bytes are preserved as-is. assert_eq!(fs::read(dir.join("f.txt")).unwrap(), b"a\r\nB\r\nc\r\n"); }