diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 5f84c0c96d..e7920e7891 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -417,7 +417,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection { thread.add_tool(ThinkingTool); thread.add_tool(FindPathTool::new(project.clone())); thread.add_tool(ReadFileTool::new(project.clone(), action_log)); - thread.add_tool(EditFileTool::new(project.clone(), cx.entity())); + thread.add_tool(EditFileTool::new(cx.entity())); thread }); diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs index db743c8429..f13cd1bd67 100644 --- a/crates/agent2/src/agent2.rs +++ b/crates/agent2/src/agent2.rs @@ -9,5 +9,6 @@ mod tests; pub use agent::*; pub use native_agent_server::NativeAgentServer; +pub use templates::*; pub use thread::*; pub use tools::*; diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index 7913f9a24c..26cf38741b 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -1,5 +1,4 @@ use super::*; -use crate::templates::Templates; use acp_thread::AgentConnection; use agent_client_protocol::{self as acp}; use anyhow::Result; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 0d6bb1608c..31ace16124 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1,4 +1,4 @@ -use crate::templates::{SystemPromptTemplate, Template, Templates}; +use crate::{SystemPromptTemplate, Template, Templates}; use acp_thread::Diff; use agent_client_protocol as acp; use anyhow::{anyhow, Context as _, Result}; @@ -133,12 +133,13 @@ pub struct Thread { project_context: Rc>, templates: Arc, pub selected_model: Arc, + project: Entity, action_log: Entity, } impl Thread { pub fn new( - _project: Entity, + project: Entity, project_context: Rc>, action_log: Entity, templates: Arc, @@ -153,10 +154,19 @@ impl Thread { project_context, templates, selected_model: default_model, + project, action_log, } } + pub fn project(&self) -> &Entity { + &self.project + } + + pub fn action_log(&self) -> &Entity { + &self.action_log + } + pub fn set_mode(&mut self, mode: CompletionMode) { self.completion_mode = mode; } @@ -323,10 +333,6 @@ impl Thread { events_rx } - pub fn action_log(&self) -> &Entity { - &self.action_log - } - pub fn build_system_message(&self) -> AgentMessage { log::debug!("Building system message"); let prompt = SystemPromptTemplate { @@ -901,6 +907,29 @@ pub struct ToolCallEventStream { } impl ToolCallEventStream { + #[cfg(test)] + pub fn test() -> ( + Self, + mpsc::UnboundedReceiver>, + ) { + let (events_tx, events_rx) = + mpsc::unbounded::>(); + + let stream = ToolCallEventStream::new( + &LanguageModelToolUse { + id: "test_id".into(), + name: "test_tool".into(), + raw_input: String::new(), + input: serde_json::Value::Null, + is_input_complete: true, + }, + acp::ToolKind::Other, + AgentResponseEventStream(events_tx), + ); + + (stream, events_rx) + } + fn new( tool_use: &LanguageModelToolUse, kind: acp::ToolKind, @@ -934,38 +963,3 @@ impl ToolCallEventStream { ) } } - -#[cfg(test)] -pub struct TestToolCallEventStream { - stream: ToolCallEventStream, - _events_rx: mpsc::UnboundedReceiver>, -} - -#[cfg(test)] -impl TestToolCallEventStream { - pub fn new() -> Self { - let (events_tx, events_rx) = - mpsc::unbounded::>(); - - let stream = ToolCallEventStream::new( - &LanguageModelToolUse { - id: "test_id".into(), - name: "test_tool".into(), - raw_input: String::new(), - input: serde_json::Value::Null, - is_input_complete: true, - }, - acp::ToolKind::Other, - AgentResponseEventStream(events_tx), - ); - - Self { - stream, - _events_rx: events_rx, - } - } - - pub fn stream(&self) -> ToolCallEventStream { - self.stream.clone() - } -} diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 4fc547ff46..3cb325f459 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -75,11 +75,6 @@ pub struct EditFileToolInput { /// When a file already exists or you just created it, prefer editing /// it as opposed to recreating it from scratch. pub mode: EditFileMode, - - /// The new content for the file (required for create and overwrite modes) - /// For edit mode, this field is not used - edits happen through the edit agent - #[serde(default)] - pub content: Option, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -91,13 +86,12 @@ pub enum EditFileMode { } pub struct EditFileTool { - project: Entity, thread: Entity, } impl EditFileTool { - pub fn new(project: Entity, thread: Entity) -> Self { - Self { project, thread } + pub fn new(thread: Entity) -> Self { + Self { thread } } fn authorize( @@ -136,7 +130,8 @@ impl EditFileTool { // Check if path is inside the global config directory // First check if it's already inside project - if not, try to canonicalize - let project_path = self.project.read(cx).find_project_path(&input.path, cx); + let thread = self.thread.read(cx); + let project_path = thread.project().read(cx).find_project_path(&input.path, cx); // If the path is inside the project, and it's not one of the above edge cases, // then no confirmation is necessary. Otherwise, confirmation is necessary. @@ -170,12 +165,12 @@ impl AgentTool for EditFileTool { event_stream: ToolCallEventStream, cx: &mut App, ) -> Task> { - let project_path = match resolve_path(&input, self.project.clone(), cx) { + let project = self.thread.read(cx).project().clone(); + let project_path = match resolve_path(&input, project.clone(), cx) { Ok(path) => path, Err(err) => return Task::ready(Err(anyhow!(err))).into(), }; - let project = self.project.clone(); let request = self.thread.update(cx, |thread, cx| { thread.build_completion_request(CompletionIntent::ToolResults, cx) }); @@ -410,1213 +405,1205 @@ fn resolve_path( } } -// todo! restore tests -// #[cfg(test)] -// mod tests { -// use super::*; -// use ::fs::Fs; -// use client::TelemetrySettings; -// use gpui::{TestAppContext, UpdateGlobal}; -// use language_model::fake_provider::FakeLanguageModel; -// use serde_json::json; -// use settings::SettingsStore; -// use std::fs; -// use util::path; - -// #[gpui::test] -// async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { -// init_test(cx); - -// let fs = project::FakeFs::new(cx.executor()); -// fs.insert_tree("/root", json!({})).await; -// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; -// let action_log = cx.new(|_| ActionLog::new(project.clone())); -// let model = Arc::new(FakeLanguageModel::default()); -// let result = cx -// .update(|cx| { -// let input = serde_json::to_value(EditFileToolInput { -// display_description: "Some edit".into(), -// path: "root/nonexistent_file.txt".into(), -// mode: EditFileMode::Edit, -// }) -// .unwrap(); -// Arc::new(EditFileTool) -// .run( -// input, -// Arc::default(), -// project.clone(), -// action_log, -// model, -// None, -// cx, -// ) -// .output -// }) -// .await; -// assert_eq!( -// result.unwrap_err().to_string(), -// "Can't edit file: path not found" -// ); -// } - -// #[gpui::test] -// async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) { -// let mode = &EditFileMode::Create; - -// let result = test_resolve_path(mode, "root/new.txt", cx); -// assert_resolved_path_eq(result.await, "new.txt"); - -// let result = test_resolve_path(mode, "new.txt", cx); -// assert_resolved_path_eq(result.await, "new.txt"); - -// let result = test_resolve_path(mode, "dir/new.txt", cx); -// assert_resolved_path_eq(result.await, "dir/new.txt"); - -// let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx); -// assert_eq!( -// result.await.unwrap_err().to_string(), -// "Can't create file: file already exists" -// ); - -// let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx); -// assert_eq!( -// result.await.unwrap_err().to_string(), -// "Can't create file: parent directory doesn't exist" -// ); -// } - -// #[gpui::test] -// async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) { -// let mode = &EditFileMode::Edit; - -// let path_with_root = "root/dir/subdir/existing.txt"; -// let path_without_root = "dir/subdir/existing.txt"; -// let result = test_resolve_path(mode, path_with_root, cx); -// assert_resolved_path_eq(result.await, path_without_root); - -// let result = test_resolve_path(mode, path_without_root, cx); -// assert_resolved_path_eq(result.await, path_without_root); - -// let result = test_resolve_path(mode, "root/nonexistent.txt", cx); -// assert_eq!( -// result.await.unwrap_err().to_string(), -// "Can't edit file: path not found" -// ); - -// let result = test_resolve_path(mode, "root/dir", cx); -// assert_eq!( -// result.await.unwrap_err().to_string(), -// "Can't edit file: path is a directory" -// ); -// } - -// async fn test_resolve_path( -// mode: &EditFileMode, -// path: &str, -// cx: &mut TestAppContext, -// ) -> anyhow::Result { -// init_test(cx); - -// let fs = project::FakeFs::new(cx.executor()); -// fs.insert_tree( -// "/root", -// json!({ -// "dir": { -// "subdir": { -// "existing.txt": "hello" -// } -// } -// }), -// ) -// .await; -// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - -// let input = EditFileToolInput { -// display_description: "Some edit".into(), -// path: path.into(), -// mode: mode.clone(), -// }; - -// let result = cx.update(|cx| resolve_path(&input, project, cx)); -// result -// } - -// fn assert_resolved_path_eq(path: anyhow::Result, expected: &str) { -// let actual = path -// .expect("Should return valid path") -// .path -// .to_str() -// .unwrap() -// .replace("\\", "/"); // Naive Windows paths normalization -// assert_eq!(actual, expected); -// } - -// #[test] -// fn still_streaming_ui_text_with_path() { -// let input = json!({ -// "path": "src/main.rs", -// "display_description": "", -// "old_string": "old code", -// "new_string": "new code" -// }); - -// assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs"); -// } - -// #[test] -// fn still_streaming_ui_text_with_description() { -// let input = json!({ -// "path": "", -// "display_description": "Fix error handling", -// "old_string": "old code", -// "new_string": "new code" -// }); - -// assert_eq!( -// EditFileTool.still_streaming_ui_text(&input), -// "Fix error handling", -// ); -// } - -// #[test] -// fn still_streaming_ui_text_with_path_and_description() { -// let input = json!({ -// "path": "src/main.rs", -// "display_description": "Fix error handling", -// "old_string": "old code", -// "new_string": "new code" -// }); - -// assert_eq!( -// EditFileTool.still_streaming_ui_text(&input), -// "Fix error handling", -// ); -// } - -// #[test] -// fn still_streaming_ui_text_no_path_or_description() { -// let input = json!({ -// "path": "", -// "display_description": "", -// "old_string": "old code", -// "new_string": "new code" -// }); - -// assert_eq!( -// EditFileTool.still_streaming_ui_text(&input), -// DEFAULT_UI_TEXT, -// ); -// } - -// #[test] -// fn still_streaming_ui_text_with_null() { -// let input = serde_json::Value::Null; - -// assert_eq!( -// EditFileTool.still_streaming_ui_text(&input), -// DEFAULT_UI_TEXT, -// ); -// } - -// fn init_test(cx: &mut TestAppContext) { -// cx.update(|cx| { -// let settings_store = SettingsStore::test(cx); -// cx.set_global(settings_store); -// language::init(cx); -// TelemetrySettings::register(cx); -// agent_settings::AgentSettings::register(cx); -// Project::init_settings(cx); -// }); -// } - -// fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) { -// cx.update(|cx| { -// // Set custom data directory (config will be under data_dir/config) -// paths::set_custom_data_dir(data_dir.to_str().unwrap()); - -// let settings_store = SettingsStore::test(cx); -// cx.set_global(settings_store); -// language::init(cx); -// TelemetrySettings::register(cx); -// agent_settings::AgentSettings::register(cx); -// Project::init_settings(cx); -// }); -// } - -// #[gpui::test] -// async fn test_format_on_save(cx: &mut TestAppContext) { -// init_test(cx); - -// let fs = project::FakeFs::new(cx.executor()); -// fs.insert_tree("/root", json!({"src": {}})).await; - -// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - -// // Set up a Rust language with LSP formatting support -// let rust_language = Arc::new(language::Language::new( -// language::LanguageConfig { -// name: "Rust".into(), -// matcher: language::LanguageMatcher { -// path_suffixes: vec!["rs".to_string()], -// ..Default::default() -// }, -// ..Default::default() -// }, -// None, -// )); - -// // Register the language and fake LSP -// let language_registry = project.read_with(cx, |project, _| project.languages().clone()); -// language_registry.add(rust_language); - -// let mut fake_language_servers = language_registry.register_fake_lsp( -// "Rust", -// language::FakeLspAdapter { -// capabilities: lsp::ServerCapabilities { -// document_formatting_provider: Some(lsp::OneOf::Left(true)), -// ..Default::default() -// }, -// ..Default::default() -// }, -// ); - -// // Create the file -// fs.save( -// path!("/root/src/main.rs").as_ref(), -// &"initial content".into(), -// language::LineEnding::Unix, -// ) -// .await -// .unwrap(); - -// // Open the buffer to trigger LSP initialization -// let buffer = project -// .update(cx, |project, cx| { -// project.open_local_buffer(path!("/root/src/main.rs"), cx) -// }) -// .await -// .unwrap(); - -// // Register the buffer with language servers -// let _handle = project.update(cx, |project, cx| { -// project.register_buffer_with_language_servers(&buffer, cx) -// }); - -// const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n"; -// const FORMATTED_CONTENT: &str = -// "This file was formatted by the fake formatter in the test.\n"; - -// // Get the fake language server and set up formatting handler -// let fake_language_server = fake_language_servers.next().await.unwrap(); -// fake_language_server.set_request_handler::({ -// |_, _| async move { -// Ok(Some(vec![lsp::TextEdit { -// range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)), -// new_text: FORMATTED_CONTENT.to_string(), -// }])) -// } -// }); - -// let action_log = cx.new(|_| ActionLog::new(project.clone())); -// let model = Arc::new(FakeLanguageModel::default()); - -// // First, test with format_on_save enabled -// cx.update(|cx| { -// SettingsStore::update_global(cx, |store, cx| { -// store.update_user_settings::( -// cx, -// |settings| { -// settings.defaults.format_on_save = Some(FormatOnSave::On); -// settings.defaults.formatter = -// Some(language::language_settings::SelectedFormatter::Auto); -// }, -// ); -// }); -// }); - -// // Have the model stream unformatted content -// let edit_result = { -// let edit_task = cx.update(|cx| { -// let input = serde_json::to_value(EditFileToolInput { -// display_description: "Create main function".into(), -// path: "root/src/main.rs".into(), -// mode: EditFileMode::Overwrite, -// }) -// .unwrap(); -// Arc::new(EditFileTool) -// .run( -// input, -// Arc::default(), -// project.clone(), -// action_log.clone(), -// model.clone(), -// None, -// cx, -// ) -// .output -// }); - -// // Stream the unformatted content -// cx.executor().run_until_parked(); -// model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); -// model.end_last_completion_stream(); - -// edit_task.await -// }; -// assert!(edit_result.is_ok()); - -// // Wait for any async operations (e.g. formatting) to complete -// cx.executor().run_until_parked(); - -// // Read the file to verify it was formatted automatically -// let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); -// assert_eq!( -// // Ignore carriage returns on Windows -// new_content.replace("\r\n", "\n"), -// FORMATTED_CONTENT, -// "Code should be formatted when format_on_save is enabled" -// ); - -// let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); - -// assert_eq!( -// stale_buffer_count, 0, -// "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \ -// This causes the agent to think the file was modified externally when it was just formatted.", -// stale_buffer_count -// ); - -// // Next, test with format_on_save disabled -// cx.update(|cx| { -// SettingsStore::update_global(cx, |store, cx| { -// store.update_user_settings::( -// cx, -// |settings| { -// settings.defaults.format_on_save = Some(FormatOnSave::Off); -// }, -// ); -// }); -// }); - -// // Stream unformatted edits again -// let edit_result = { -// let edit_task = cx.update(|cx| { -// let input = serde_json::to_value(EditFileToolInput { -// display_description: "Update main function".into(), -// path: "root/src/main.rs".into(), -// mode: EditFileMode::Overwrite, -// }) -// .unwrap(); -// Arc::new(EditFileTool) -// .run( -// input, -// Arc::default(), -// project.clone(), -// action_log.clone(), -// model.clone(), -// None, -// cx, -// ) -// .output -// }); - -// // Stream the unformatted content -// cx.executor().run_until_parked(); -// model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); -// model.end_last_completion_stream(); - -// edit_task.await -// }; -// assert!(edit_result.is_ok()); - -// // Wait for any async operations (e.g. formatting) to complete -// cx.executor().run_until_parked(); - -// // Verify the file was not formatted -// let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); -// assert_eq!( -// // Ignore carriage returns on Windows -// new_content.replace("\r\n", "\n"), -// UNFORMATTED_CONTENT, -// "Code should not be formatted when format_on_save is disabled" -// ); -// } - -// #[gpui::test] -// async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { -// init_test(cx); - -// let fs = project::FakeFs::new(cx.executor()); -// fs.insert_tree("/root", json!({"src": {}})).await; - -// // Create a simple file with trailing whitespace -// fs.save( -// path!("/root/src/main.rs").as_ref(), -// &"initial content".into(), -// language::LineEnding::Unix, -// ) -// .await -// .unwrap(); - -// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; -// let action_log = cx.new(|_| ActionLog::new(project.clone())); -// let model = Arc::new(FakeLanguageModel::default()); - -// // First, test with remove_trailing_whitespace_on_save enabled -// cx.update(|cx| { -// SettingsStore::update_global(cx, |store, cx| { -// store.update_user_settings::( -// cx, -// |settings| { -// settings.defaults.remove_trailing_whitespace_on_save = Some(true); -// }, -// ); -// }); -// }); - -// const CONTENT_WITH_TRAILING_WHITESPACE: &str = -// "fn main() { \n println!(\"Hello!\"); \n}\n"; - -// // Have the model stream content that contains trailing whitespace -// let edit_result = { -// let edit_task = cx.update(|cx| { -// let input = serde_json::to_value(EditFileToolInput { -// display_description: "Create main function".into(), -// path: "root/src/main.rs".into(), -// mode: EditFileMode::Overwrite, -// }) -// .unwrap(); -// Arc::new(EditFileTool) -// .run( -// input, -// Arc::default(), -// project.clone(), -// action_log.clone(), -// model.clone(), -// None, -// cx, -// ) -// .output -// }); - -// // Stream the content with trailing whitespace -// cx.executor().run_until_parked(); -// model.send_last_completion_stream_text_chunk( -// CONTENT_WITH_TRAILING_WHITESPACE.to_string(), -// ); -// model.end_last_completion_stream(); - -// edit_task.await -// }; -// assert!(edit_result.is_ok()); - -// // Wait for any async operations (e.g. formatting) to complete -// cx.executor().run_until_parked(); - -// // Read the file to verify trailing whitespace was removed automatically -// assert_eq!( -// // Ignore carriage returns on Windows -// fs.load(path!("/root/src/main.rs").as_ref()) -// .await -// .unwrap() -// .replace("\r\n", "\n"), -// "fn main() {\n println!(\"Hello!\");\n}\n", -// "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled" -// ); - -// // Next, test with remove_trailing_whitespace_on_save disabled -// cx.update(|cx| { -// SettingsStore::update_global(cx, |store, cx| { -// store.update_user_settings::( -// cx, -// |settings| { -// settings.defaults.remove_trailing_whitespace_on_save = Some(false); -// }, -// ); -// }); -// }); - -// // Stream edits again with trailing whitespace -// let edit_result = { -// let edit_task = cx.update(|cx| { -// let input = serde_json::to_value(EditFileToolInput { -// display_description: "Update main function".into(), -// path: "root/src/main.rs".into(), -// mode: EditFileMode::Overwrite, -// }) -// .unwrap(); -// Arc::new(EditFileTool) -// .run( -// input, -// Arc::default(), -// project.clone(), -// action_log.clone(), -// model.clone(), -// None, -// cx, -// ) -// .output -// }); - -// // Stream the content with trailing whitespace -// cx.executor().run_until_parked(); -// model.send_last_completion_stream_text_chunk( -// CONTENT_WITH_TRAILING_WHITESPACE.to_string(), -// ); -// model.end_last_completion_stream(); - -// edit_task.await -// }; -// assert!(edit_result.is_ok()); - -// // Wait for any async operations (e.g. formatting) to complete -// cx.executor().run_until_parked(); - -// // Verify the file still has trailing whitespace -// // Read the file again - it should still have trailing whitespace -// let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); -// assert_eq!( -// // Ignore carriage returns on Windows -// final_content.replace("\r\n", "\n"), -// CONTENT_WITH_TRAILING_WHITESPACE, -// "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" -// ); -// } - -// #[gpui::test] -// async fn test_needs_confirmation(cx: &mut TestAppContext) { -// init_test(cx); -// let tool = Arc::new(EditFileTool); -// let fs = project::FakeFs::new(cx.executor()); -// fs.insert_tree("/root", json!({})).await; - -// // Test 1: Path with .zed component should require confirmation -// let input_with_zed = json!({ -// "display_description": "Edit settings", -// "path": ".zed/settings.json", -// "mode": "edit" -// }); -// let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; -// cx.update(|cx| { -// assert!( -// tool.needs_confirmation(&input_with_zed, &project, cx), -// "Path with .zed component should require confirmation" -// ); -// }); - -// // Test 2: Absolute path should require confirmation -// let input_absolute = json!({ -// "display_description": "Edit file", -// "path": "/etc/hosts", -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert!( -// tool.needs_confirmation(&input_absolute, &project, cx), -// "Absolute path should require confirmation" -// ); -// }); - -// // Test 3: Relative path without .zed should not require confirmation -// let input_relative = json!({ -// "display_description": "Edit file", -// "path": "root/src/main.rs", -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert!( -// !tool.needs_confirmation(&input_relative, &project, cx), -// "Relative path without .zed should not require confirmation" -// ); -// }); - -// // Test 4: Path with .zed in the middle should require confirmation -// let input_zed_middle = json!({ -// "display_description": "Edit settings", -// "path": "root/.zed/tasks.json", -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert!( -// tool.needs_confirmation(&input_zed_middle, &project, cx), -// "Path with .zed in any component should require confirmation" -// ); -// }); - -// // Test 5: When always_allow_tool_actions is enabled, no confirmation needed -// cx.update(|cx| { -// let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); -// settings.always_allow_tool_actions = true; -// agent_settings::AgentSettings::override_global(settings, cx); - -// assert!( -// !tool.needs_confirmation(&input_with_zed, &project, cx), -// "When always_allow_tool_actions is true, no confirmation should be needed" -// ); -// assert!( -// !tool.needs_confirmation(&input_absolute, &project, cx), -// "When always_allow_tool_actions is true, no confirmation should be needed for absolute paths" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) { -// // Set up a custom config directory for testing -// let temp_dir = tempfile::tempdir().unwrap(); -// init_test_with_config(cx, temp_dir.path()); - -// let tool = Arc::new(EditFileTool); - -// // Test ui_text shows context for various paths -// let test_cases = vec![ -// ( -// json!({ -// "display_description": "Update config", -// "path": ".zed/settings.json", -// "mode": "edit" -// }), -// "Update config (local settings)", -// ".zed path should show local settings context", -// ), -// ( -// json!({ -// "display_description": "Fix bug", -// "path": "src/.zed/local.json", -// "mode": "edit" -// }), -// "Fix bug (local settings)", -// "Nested .zed path should show local settings context", -// ), -// ( -// json!({ -// "display_description": "Update readme", -// "path": "README.md", -// "mode": "edit" -// }), -// "Update readme", -// "Normal path should not show additional context", -// ), -// ( -// json!({ -// "display_description": "Edit config", -// "path": "config.zed", -// "mode": "edit" -// }), -// "Edit config", -// ".zed as extension should not show context", -// ), -// ]; - -// for (input, expected_text, description) in test_cases { -// cx.update(|_cx| { -// let ui_text = tool.ui_text(&input); -// assert_eq!(ui_text, expected_text, "Failed for case: {}", description); -// }); -// } -// } - -// #[gpui::test] -// async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) { -// init_test(cx); -// let tool = Arc::new(EditFileTool); -// let fs = project::FakeFs::new(cx.executor()); - -// // Create a project in /project directory -// fs.insert_tree("/project", json!({})).await; -// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - -// // Test file outside project requires confirmation -// let input_outside = json!({ -// "display_description": "Edit file", -// "path": "/outside/file.txt", -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert!( -// tool.needs_confirmation(&input_outside, &project, cx), -// "File outside project should require confirmation" -// ); -// }); - -// // Test file inside project doesn't require confirmation -// let input_inside = json!({ -// "display_description": "Edit file", -// "path": "project/file.txt", -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert!( -// !tool.needs_confirmation(&input_inside, &project, cx), -// "File inside project should not require confirmation" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) { -// // Set up a custom data directory for testing -// let temp_dir = tempfile::tempdir().unwrap(); -// init_test_with_config(cx, temp_dir.path()); - -// let tool = Arc::new(EditFileTool); -// let fs = project::FakeFs::new(cx.executor()); -// fs.insert_tree("/home/user/myproject", json!({})).await; -// let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await; - -// // Get the actual local settings folder name -// let local_settings_folder = paths::local_settings_folder_relative_path(); - -// // Test various config path patterns -// let test_cases = vec![ -// ( -// format!("{}/settings.json", local_settings_folder.display()), -// true, -// "Top-level local settings file".to_string(), -// ), -// ( -// format!( -// "myproject/{}/settings.json", -// local_settings_folder.display() -// ), -// true, -// "Local settings in project path".to_string(), -// ), -// ( -// format!("src/{}/config.toml", local_settings_folder.display()), -// true, -// "Local settings in subdirectory".to_string(), -// ), -// ( -// ".zed.backup/file.txt".to_string(), -// true, -// ".zed.backup is outside project".to_string(), -// ), -// ( -// "my.zed/file.txt".to_string(), -// true, -// "my.zed is outside project".to_string(), -// ), -// ( -// "myproject/src/file.zed".to_string(), -// false, -// ".zed as file extension".to_string(), -// ), -// ( -// "myproject/normal/path/file.rs".to_string(), -// false, -// "Normal file without config paths".to_string(), -// ), -// ]; - -// for (path, should_confirm, description) in test_cases { -// let input = json!({ -// "display_description": "Edit file", -// "path": path, -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert_eq!( -// tool.needs_confirmation(&input, &project, cx), -// should_confirm, -// "Failed for case: {} - path: {}", -// description, -// path -// ); -// }); -// } -// } - -// #[gpui::test] -// async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) { -// // Set up a custom data directory for testing -// let temp_dir = tempfile::tempdir().unwrap(); -// init_test_with_config(cx, temp_dir.path()); - -// let tool = Arc::new(EditFileTool); -// let fs = project::FakeFs::new(cx.executor()); - -// // Create test files in the global config directory -// let global_config_dir = paths::config_dir(); -// fs::create_dir_all(&global_config_dir).unwrap(); -// let global_settings_path = global_config_dir.join("settings.json"); -// fs::write(&global_settings_path, "{}").unwrap(); - -// fs.insert_tree("/project", json!({})).await; -// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - -// // Test global config paths -// let test_cases = vec![ -// ( -// global_settings_path.to_str().unwrap().to_string(), -// true, -// "Global settings file should require confirmation", -// ), -// ( -// global_config_dir -// .join("keymap.json") -// .to_str() -// .unwrap() -// .to_string(), -// true, -// "Global keymap file should require confirmation", -// ), -// ( -// "project/normal_file.rs".to_string(), -// false, -// "Normal project file should not require confirmation", -// ), -// ]; - -// for (path, should_confirm, description) in test_cases { -// let input = json!({ -// "display_description": "Edit file", -// "path": path, -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert_eq!( -// tool.needs_confirmation(&input, &project, cx), -// should_confirm, -// "Failed for case: {}", -// description -// ); -// }); -// } -// } - -// #[gpui::test] -// async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { -// init_test(cx); -// let tool = Arc::new(EditFileTool); -// let fs = project::FakeFs::new(cx.executor()); - -// // Create multiple worktree directories -// fs.insert_tree( -// "/workspace/frontend", -// json!({ -// "src": { -// "main.js": "console.log('frontend');" -// } -// }), -// ) -// .await; -// fs.insert_tree( -// "/workspace/backend", -// json!({ -// "src": { -// "main.rs": "fn main() {}" -// } -// }), -// ) -// .await; -// fs.insert_tree( -// "/workspace/shared", -// json!({ -// ".zed": { -// "settings.json": "{}" -// } -// }), -// ) -// .await; - -// // Create project with multiple worktrees -// let project = Project::test( -// fs.clone(), -// [ -// path!("/workspace/frontend").as_ref(), -// path!("/workspace/backend").as_ref(), -// path!("/workspace/shared").as_ref(), -// ], -// cx, -// ) -// .await; - -// // Test files in different worktrees -// let test_cases = vec![ -// ("frontend/src/main.js", false, "File in first worktree"), -// ("backend/src/main.rs", false, "File in second worktree"), -// ( -// "shared/.zed/settings.json", -// true, -// ".zed file in third worktree", -// ), -// ("/etc/hosts", true, "Absolute path outside all worktrees"), -// ( -// "../outside/file.txt", -// true, -// "Relative path outside worktrees", -// ), -// ]; - -// for (path, should_confirm, description) in test_cases { -// let input = json!({ -// "display_description": "Edit file", -// "path": path, -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert_eq!( -// tool.needs_confirmation(&input, &project, cx), -// should_confirm, -// "Failed for case: {} - path: {}", -// description, -// path -// ); -// }); -// } -// } - -// #[gpui::test] -// async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { -// init_test(cx); -// let tool = Arc::new(EditFileTool); -// let fs = project::FakeFs::new(cx.executor()); -// fs.insert_tree( -// "/project", -// json!({ -// ".zed": { -// "settings.json": "{}" -// }, -// "src": { -// ".zed": { -// "local.json": "{}" -// } -// } -// }), -// ) -// .await; -// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - -// // Test edge cases -// let test_cases = vec![ -// // Empty path - find_project_path returns Some for empty paths -// ("", false, "Empty path is treated as project root"), -// // Root directory -// ("/", true, "Root directory should be outside project"), -// // Parent directory references - find_project_path resolves these -// ( -// "project/../other", -// false, -// "Path with .. is resolved by find_project_path", -// ), -// ( -// "project/./src/file.rs", -// false, -// "Path with . should work normally", -// ), -// // Windows-style paths (if on Windows) -// #[cfg(target_os = "windows")] -// ("C:\\Windows\\System32\\hosts", true, "Windows system path"), -// #[cfg(target_os = "windows")] -// ("project\\src\\main.rs", false, "Windows-style project path"), -// ]; - -// for (path, should_confirm, description) in test_cases { -// let input = json!({ -// "display_description": "Edit file", -// "path": path, -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert_eq!( -// tool.needs_confirmation(&input, &project, cx), -// should_confirm, -// "Failed for case: {} - path: {}", -// description, -// path -// ); -// }); -// } -// } - -// #[gpui::test] -// async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) { -// init_test(cx); -// let tool = Arc::new(EditFileTool); - -// // Test UI text for various scenarios -// let test_cases = vec![ -// ( -// json!({ -// "display_description": "Update config", -// "path": ".zed/settings.json", -// "mode": "edit" -// }), -// "Update config (local settings)", -// ".zed path should show local settings context", -// ), -// ( -// json!({ -// "display_description": "Fix bug", -// "path": "src/.zed/local.json", -// "mode": "edit" -// }), -// "Fix bug (local settings)", -// "Nested .zed path should show local settings context", -// ), -// ( -// json!({ -// "display_description": "Update readme", -// "path": "README.md", -// "mode": "edit" -// }), -// "Update readme", -// "Normal path should not show additional context", -// ), -// ( -// json!({ -// "display_description": "Edit config", -// "path": "config.zed", -// "mode": "edit" -// }), -// "Edit config", -// ".zed as extension should not show context", -// ), -// ]; - -// for (input, expected_text, description) in test_cases { -// cx.update(|_cx| { -// let ui_text = tool.ui_text(&input); -// assert_eq!(ui_text, expected_text, "Failed for case: {}", description); -// }); -// } -// } - -// #[gpui::test] -// async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { -// init_test(cx); -// let tool = Arc::new(EditFileTool); -// let fs = project::FakeFs::new(cx.executor()); -// fs.insert_tree( -// "/project", -// json!({ -// "existing.txt": "content", -// ".zed": { -// "settings.json": "{}" -// } -// }), -// ) -// .await; -// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - -// // Test different EditFileMode values -// let modes = vec![ -// EditFileMode::Edit, -// EditFileMode::Create, -// EditFileMode::Overwrite, -// ]; - -// for mode in modes { -// // Test .zed path with different modes -// let input_zed = json!({ -// "display_description": "Edit settings", -// "path": "project/.zed/settings.json", -// "mode": mode -// }); -// cx.update(|cx| { -// assert!( -// tool.needs_confirmation(&input_zed, &project, cx), -// ".zed path should require confirmation regardless of mode: {:?}", -// mode -// ); -// }); - -// // Test outside path with different modes -// let input_outside = json!({ -// "display_description": "Edit file", -// "path": "/outside/file.txt", -// "mode": mode -// }); -// cx.update(|cx| { -// assert!( -// tool.needs_confirmation(&input_outside, &project, cx), -// "Outside path should require confirmation regardless of mode: {:?}", -// mode -// ); -// }); - -// // Test normal path with different modes -// let input_normal = json!({ -// "display_description": "Edit file", -// "path": "project/normal.txt", -// "mode": mode -// }); -// cx.update(|cx| { -// assert!( -// !tool.needs_confirmation(&input_normal, &project, cx), -// "Normal path should not require confirmation regardless of mode: {:?}", -// mode -// ); -// }); -// } -// } - -// #[gpui::test] -// async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) { -// // Set up with custom directories for deterministic testing -// let temp_dir = tempfile::tempdir().unwrap(); -// init_test_with_config(cx, temp_dir.path()); - -// let tool = Arc::new(EditFileTool); -// let fs = project::FakeFs::new(cx.executor()); -// fs.insert_tree("/project", json!({})).await; -// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - -// // Enable always_allow_tool_actions -// cx.update(|cx| { -// let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); -// settings.always_allow_tool_actions = true; -// agent_settings::AgentSettings::override_global(settings, cx); -// }); - -// // Test that all paths that normally require confirmation are bypassed -// let global_settings_path = paths::config_dir().join("settings.json"); -// fs::create_dir_all(paths::config_dir()).unwrap(); -// fs::write(&global_settings_path, "{}").unwrap(); - -// let test_cases = vec![ -// ".zed/settings.json", -// "project/.zed/config.toml", -// global_settings_path.to_str().unwrap(), -// "/etc/hosts", -// "/absolute/path/file.txt", -// "../outside/project.txt", -// ]; - -// for path in test_cases { -// let input = json!({ -// "display_description": "Edit file", -// "path": path, -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert!( -// !tool.needs_confirmation(&input, &project, cx), -// "Path {} should not require confirmation when always_allow_tool_actions is true", -// path -// ); -// }); -// } - -// // Disable always_allow_tool_actions and verify confirmation is required again -// cx.update(|cx| { -// let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); -// settings.always_allow_tool_actions = false; -// agent_settings::AgentSettings::override_global(settings, cx); -// }); - -// // Verify .zed path requires confirmation again -// let input = json!({ -// "display_description": "Edit file", -// "path": ".zed/settings.json", -// "mode": "edit" -// }); -// cx.update(|cx| { -// assert!( -// tool.needs_confirmation(&input, &project, cx), -// ".zed path should require confirmation when always_allow_tool_actions is false" -// ); -// }); -// } -// } +#[cfg(test)] +mod tests { + use crate::Templates; + + use super::*; + use assistant_tool::ActionLog; + use client::TelemetrySettings; + use gpui::TestAppContext; + use language_model::fake_provider::FakeLanguageModel; + use serde_json::json; + use settings::SettingsStore; + use std::rc::Rc; + use util::path; + + #[gpui::test] + async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { + init_test(cx); + + let fs = project::FakeFs::new(cx.executor()); + fs.insert_tree("/root", json!({})).await; + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = + cx.new(|_| Thread::new(project, Rc::default(), action_log, Templates::new(), model)); + let result = cx + .update(|cx| { + let input = EditFileToolInput { + display_description: "Some edit".into(), + path: "root/nonexistent_file.txt".into(), + mode: EditFileMode::Edit, + }; + Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx) + }) + .await; + assert_eq!( + result.unwrap_err().to_string(), + "Can't edit file: path not found" + ); + } + + // #[gpui::test] + // async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) { + // let mode = &EditFileMode::Create; + + // let result = test_resolve_path(mode, "root/new.txt", cx); + // assert_resolved_path_eq(result.await, "new.txt"); + + // let result = test_resolve_path(mode, "new.txt", cx); + // assert_resolved_path_eq(result.await, "new.txt"); + + // let result = test_resolve_path(mode, "dir/new.txt", cx); + // assert_resolved_path_eq(result.await, "dir/new.txt"); + + // let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx); + // assert_eq!( + // result.await.unwrap_err().to_string(), + // "Can't create file: file already exists" + // ); + + // let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx); + // assert_eq!( + // result.await.unwrap_err().to_string(), + // "Can't create file: parent directory doesn't exist" + // ); + // } + + // #[gpui::test] + // async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) { + // let mode = &EditFileMode::Edit; + + // let path_with_root = "root/dir/subdir/existing.txt"; + // let path_without_root = "dir/subdir/existing.txt"; + // let result = test_resolve_path(mode, path_with_root, cx); + // assert_resolved_path_eq(result.await, path_without_root); + + // let result = test_resolve_path(mode, path_without_root, cx); + // assert_resolved_path_eq(result.await, path_without_root); + + // let result = test_resolve_path(mode, "root/nonexistent.txt", cx); + // assert_eq!( + // result.await.unwrap_err().to_string(), + // "Can't edit file: path not found" + // ); + + // let result = test_resolve_path(mode, "root/dir", cx); + // assert_eq!( + // result.await.unwrap_err().to_string(), + // "Can't edit file: path is a directory" + // ); + // } + + // async fn test_resolve_path( + // mode: &EditFileMode, + // path: &str, + // cx: &mut TestAppContext, + // ) -> anyhow::Result { + // init_test(cx); + + // let fs = project::FakeFs::new(cx.executor()); + // fs.insert_tree( + // "/root", + // json!({ + // "dir": { + // "subdir": { + // "existing.txt": "hello" + // } + // } + // }), + // ) + // .await; + // let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + // let input = EditFileToolInput { + // display_description: "Some edit".into(), + // path: path.into(), + // mode: mode.clone(), + // }; + + // let result = cx.update(|cx| resolve_path(&input, project, cx)); + // result + // } + + // fn assert_resolved_path_eq(path: anyhow::Result, expected: &str) { + // let actual = path + // .expect("Should return valid path") + // .path + // .to_str() + // .unwrap() + // .replace("\\", "/"); // Naive Windows paths normalization + // assert_eq!(actual, expected); + // } + + // #[test] + // fn still_streaming_ui_text_with_path() { + // let input = json!({ + // "path": "src/main.rs", + // "display_description": "", + // "old_string": "old code", + // "new_string": "new code" + // }); + + // assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs"); + // } + + // #[test] + // fn still_streaming_ui_text_with_description() { + // let input = json!({ + // "path": "", + // "display_description": "Fix error handling", + // "old_string": "old code", + // "new_string": "new code" + // }); + + // assert_eq!( + // EditFileTool.still_streaming_ui_text(&input), + // "Fix error handling", + // ); + // } + + // #[test] + // fn still_streaming_ui_text_with_path_and_description() { + // let input = json!({ + // "path": "src/main.rs", + // "display_description": "Fix error handling", + // "old_string": "old code", + // "new_string": "new code" + // }); + + // assert_eq!( + // EditFileTool.still_streaming_ui_text(&input), + // "Fix error handling", + // ); + // } + + // #[test] + // fn still_streaming_ui_text_no_path_or_description() { + // let input = json!({ + // "path": "", + // "display_description": "", + // "old_string": "old code", + // "new_string": "new code" + // }); + + // assert_eq!( + // EditFileTool.still_streaming_ui_text(&input), + // DEFAULT_UI_TEXT, + // ); + // } + + // #[test] + // fn still_streaming_ui_text_with_null() { + // let input = serde_json::Value::Null; + + // assert_eq!( + // EditFileTool.still_streaming_ui_text(&input), + // DEFAULT_UI_TEXT, + // ); + // } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + TelemetrySettings::register(cx); + agent_settings::AgentSettings::register(cx); + Project::init_settings(cx); + }); + } + + // fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) { + // cx.update(|cx| { + // // Set custom data directory (config will be under data_dir/config) + // paths::set_custom_data_dir(data_dir.to_str().unwrap()); + + // let settings_store = SettingsStore::test(cx); + // cx.set_global(settings_store); + // language::init(cx); + // TelemetrySettings::register(cx); + // agent_settings::AgentSettings::register(cx); + // Project::init_settings(cx); + // }); + // } + + // #[gpui::test] + // async fn test_format_on_save(cx: &mut TestAppContext) { + // init_test(cx); + + // let fs = project::FakeFs::new(cx.executor()); + // fs.insert_tree("/root", json!({"src": {}})).await; + + // let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + + // // Set up a Rust language with LSP formatting support + // let rust_language = Arc::new(language::Language::new( + // language::LanguageConfig { + // name: "Rust".into(), + // matcher: language::LanguageMatcher { + // path_suffixes: vec!["rs".to_string()], + // ..Default::default() + // }, + // ..Default::default() + // }, + // None, + // )); + + // // Register the language and fake LSP + // let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + // language_registry.add(rust_language); + + // let mut fake_language_servers = language_registry.register_fake_lsp( + // "Rust", + // language::FakeLspAdapter { + // capabilities: lsp::ServerCapabilities { + // document_formatting_provider: Some(lsp::OneOf::Left(true)), + // ..Default::default() + // }, + // ..Default::default() + // }, + // ); + + // // Create the file + // fs.save( + // path!("/root/src/main.rs").as_ref(), + // &"initial content".into(), + // language::LineEnding::Unix, + // ) + // .await + // .unwrap(); + + // // Open the buffer to trigger LSP initialization + // let buffer = project + // .update(cx, |project, cx| { + // project.open_local_buffer(path!("/root/src/main.rs"), cx) + // }) + // .await + // .unwrap(); + + // // Register the buffer with language servers + // let _handle = project.update(cx, |project, cx| { + // project.register_buffer_with_language_servers(&buffer, cx) + // }); + + // const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n"; + // const FORMATTED_CONTENT: &str = + // "This file was formatted by the fake formatter in the test.\n"; + + // // Get the fake language server and set up formatting handler + // let fake_language_server = fake_language_servers.next().await.unwrap(); + // fake_language_server.set_request_handler::({ + // |_, _| async move { + // Ok(Some(vec![lsp::TextEdit { + // range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)), + // new_text: FORMATTED_CONTENT.to_string(), + // }])) + // } + // }); + + // let action_log = cx.new(|_| ActionLog::new(project.clone())); + // let model = Arc::new(FakeLanguageModel::default()); + + // // First, test with format_on_save enabled + // cx.update(|cx| { + // SettingsStore::update_global(cx, |store, cx| { + // store.update_user_settings::( + // cx, + // |settings| { + // settings.defaults.format_on_save = Some(FormatOnSave::On); + // settings.defaults.formatter = + // Some(language::language_settings::SelectedFormatter::Auto); + // }, + // ); + // }); + // }); + + // // Have the model stream unformatted content + // let edit_result = { + // let edit_task = cx.update(|cx| { + // let input = serde_json::to_value(EditFileToolInput { + // display_description: "Create main function".into(), + // path: "root/src/main.rs".into(), + // mode: EditFileMode::Overwrite, + // }) + // .unwrap(); + // Arc::new(EditFileTool) + // .run( + // input, + // Arc::default(), + // project.clone(), + // action_log.clone(), + // model.clone(), + // None, + // cx, + // ) + // .output + // }); + + // // Stream the unformatted content + // cx.executor().run_until_parked(); + // model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); + // model.end_last_completion_stream(); + + // edit_task.await + // }; + // assert!(edit_result.is_ok()); + + // // Wait for any async operations (e.g. formatting) to complete + // cx.executor().run_until_parked(); + + // // Read the file to verify it was formatted automatically + // let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); + // assert_eq!( + // // Ignore carriage returns on Windows + // new_content.replace("\r\n", "\n"), + // FORMATTED_CONTENT, + // "Code should be formatted when format_on_save is enabled" + // ); + + // let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); + + // assert_eq!( + // stale_buffer_count, 0, + // "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \ + // This causes the agent to think the file was modified externally when it was just formatted.", + // stale_buffer_count + // ); + + // // Next, test with format_on_save disabled + // cx.update(|cx| { + // SettingsStore::update_global(cx, |store, cx| { + // store.update_user_settings::( + // cx, + // |settings| { + // settings.defaults.format_on_save = Some(FormatOnSave::Off); + // }, + // ); + // }); + // }); + + // // Stream unformatted edits again + // let edit_result = { + // let edit_task = cx.update(|cx| { + // let input = serde_json::to_value(EditFileToolInput { + // display_description: "Update main function".into(), + // path: "root/src/main.rs".into(), + // mode: EditFileMode::Overwrite, + // }) + // .unwrap(); + // Arc::new(EditFileTool) + // .run( + // input, + // Arc::default(), + // project.clone(), + // action_log.clone(), + // model.clone(), + // None, + // cx, + // ) + // .output + // }); + + // // Stream the unformatted content + // cx.executor().run_until_parked(); + // model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); + // model.end_last_completion_stream(); + + // edit_task.await + // }; + // assert!(edit_result.is_ok()); + + // // Wait for any async operations (e.g. formatting) to complete + // cx.executor().run_until_parked(); + + // // Verify the file was not formatted + // let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); + // assert_eq!( + // // Ignore carriage returns on Windows + // new_content.replace("\r\n", "\n"), + // UNFORMATTED_CONTENT, + // "Code should not be formatted when format_on_save is disabled" + // ); + // } + + // #[gpui::test] + // async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { + // init_test(cx); + + // let fs = project::FakeFs::new(cx.executor()); + // fs.insert_tree("/root", json!({"src": {}})).await; + + // // Create a simple file with trailing whitespace + // fs.save( + // path!("/root/src/main.rs").as_ref(), + // &"initial content".into(), + // language::LineEnding::Unix, + // ) + // .await + // .unwrap(); + + // let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + // let action_log = cx.new(|_| ActionLog::new(project.clone())); + // let model = Arc::new(FakeLanguageModel::default()); + + // // First, test with remove_trailing_whitespace_on_save enabled + // cx.update(|cx| { + // SettingsStore::update_global(cx, |store, cx| { + // store.update_user_settings::( + // cx, + // |settings| { + // settings.defaults.remove_trailing_whitespace_on_save = Some(true); + // }, + // ); + // }); + // }); + + // const CONTENT_WITH_TRAILING_WHITESPACE: &str = + // "fn main() { \n println!(\"Hello!\"); \n}\n"; + + // // Have the model stream content that contains trailing whitespace + // let edit_result = { + // let edit_task = cx.update(|cx| { + // let input = serde_json::to_value(EditFileToolInput { + // display_description: "Create main function".into(), + // path: "root/src/main.rs".into(), + // mode: EditFileMode::Overwrite, + // }) + // .unwrap(); + // Arc::new(EditFileTool) + // .run( + // input, + // Arc::default(), + // project.clone(), + // action_log.clone(), + // model.clone(), + // None, + // cx, + // ) + // .output + // }); + + // // Stream the content with trailing whitespace + // cx.executor().run_until_parked(); + // model.send_last_completion_stream_text_chunk( + // CONTENT_WITH_TRAILING_WHITESPACE.to_string(), + // ); + // model.end_last_completion_stream(); + + // edit_task.await + // }; + // assert!(edit_result.is_ok()); + + // // Wait for any async operations (e.g. formatting) to complete + // cx.executor().run_until_parked(); + + // // Read the file to verify trailing whitespace was removed automatically + // assert_eq!( + // // Ignore carriage returns on Windows + // fs.load(path!("/root/src/main.rs").as_ref()) + // .await + // .unwrap() + // .replace("\r\n", "\n"), + // "fn main() {\n println!(\"Hello!\");\n}\n", + // "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled" + // ); + + // // Next, test with remove_trailing_whitespace_on_save disabled + // cx.update(|cx| { + // SettingsStore::update_global(cx, |store, cx| { + // store.update_user_settings::( + // cx, + // |settings| { + // settings.defaults.remove_trailing_whitespace_on_save = Some(false); + // }, + // ); + // }); + // }); + + // // Stream edits again with trailing whitespace + // let edit_result = { + // let edit_task = cx.update(|cx| { + // let input = serde_json::to_value(EditFileToolInput { + // display_description: "Update main function".into(), + // path: "root/src/main.rs".into(), + // mode: EditFileMode::Overwrite, + // }) + // .unwrap(); + // Arc::new(EditFileTool) + // .run( + // input, + // Arc::default(), + // project.clone(), + // action_log.clone(), + // model.clone(), + // None, + // cx, + // ) + // .output + // }); + + // // Stream the content with trailing whitespace + // cx.executor().run_until_parked(); + // model.send_last_completion_stream_text_chunk( + // CONTENT_WITH_TRAILING_WHITESPACE.to_string(), + // ); + // model.end_last_completion_stream(); + + // edit_task.await + // }; + // assert!(edit_result.is_ok()); + + // // Wait for any async operations (e.g. formatting) to complete + // cx.executor().run_until_parked(); + + // // Verify the file still has trailing whitespace + // // Read the file again - it should still have trailing whitespace + // let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); + // assert_eq!( + // // Ignore carriage returns on Windows + // final_content.replace("\r\n", "\n"), + // CONTENT_WITH_TRAILING_WHITESPACE, + // "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" + // ); + // } + + // #[gpui::test] + // async fn test_needs_confirmation(cx: &mut TestAppContext) { + // init_test(cx); + // let tool = Arc::new(EditFileTool); + // let fs = project::FakeFs::new(cx.executor()); + // fs.insert_tree("/root", json!({})).await; + + // // Test 1: Path with .zed component should require confirmation + // let input_with_zed = json!({ + // "display_description": "Edit settings", + // "path": ".zed/settings.json", + // "mode": "edit" + // }); + // let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + // cx.update(|cx| { + // assert!( + // tool.needs_confirmation(&input_with_zed, &project, cx), + // "Path with .zed component should require confirmation" + // ); + // }); + + // // Test 2: Absolute path should require confirmation + // let input_absolute = json!({ + // "display_description": "Edit file", + // "path": "/etc/hosts", + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert!( + // tool.needs_confirmation(&input_absolute, &project, cx), + // "Absolute path should require confirmation" + // ); + // }); + + // // Test 3: Relative path without .zed should not require confirmation + // let input_relative = json!({ + // "display_description": "Edit file", + // "path": "root/src/main.rs", + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert!( + // !tool.needs_confirmation(&input_relative, &project, cx), + // "Relative path without .zed should not require confirmation" + // ); + // }); + + // // Test 4: Path with .zed in the middle should require confirmation + // let input_zed_middle = json!({ + // "display_description": "Edit settings", + // "path": "root/.zed/tasks.json", + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert!( + // tool.needs_confirmation(&input_zed_middle, &project, cx), + // "Path with .zed in any component should require confirmation" + // ); + // }); + + // // Test 5: When always_allow_tool_actions is enabled, no confirmation needed + // cx.update(|cx| { + // let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); + // settings.always_allow_tool_actions = true; + // agent_settings::AgentSettings::override_global(settings, cx); + + // assert!( + // !tool.needs_confirmation(&input_with_zed, &project, cx), + // "When always_allow_tool_actions is true, no confirmation should be needed" + // ); + // assert!( + // !tool.needs_confirmation(&input_absolute, &project, cx), + // "When always_allow_tool_actions is true, no confirmation should be needed for absolute paths" + // ); + // }); + // } + + // #[gpui::test] + // async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) { + // // Set up a custom config directory for testing + // let temp_dir = tempfile::tempdir().unwrap(); + // init_test_with_config(cx, temp_dir.path()); + + // let tool = Arc::new(EditFileTool); + + // // Test ui_text shows context for various paths + // let test_cases = vec![ + // ( + // json!({ + // "display_description": "Update config", + // "path": ".zed/settings.json", + // "mode": "edit" + // }), + // "Update config (local settings)", + // ".zed path should show local settings context", + // ), + // ( + // json!({ + // "display_description": "Fix bug", + // "path": "src/.zed/local.json", + // "mode": "edit" + // }), + // "Fix bug (local settings)", + // "Nested .zed path should show local settings context", + // ), + // ( + // json!({ + // "display_description": "Update readme", + // "path": "README.md", + // "mode": "edit" + // }), + // "Update readme", + // "Normal path should not show additional context", + // ), + // ( + // json!({ + // "display_description": "Edit config", + // "path": "config.zed", + // "mode": "edit" + // }), + // "Edit config", + // ".zed as extension should not show context", + // ), + // ]; + + // for (input, expected_text, description) in test_cases { + // cx.update(|_cx| { + // let ui_text = tool.ui_text(&input); + // assert_eq!(ui_text, expected_text, "Failed for case: {}", description); + // }); + // } + // } + + // #[gpui::test] + // async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) { + // init_test(cx); + // let tool = Arc::new(EditFileTool); + // let fs = project::FakeFs::new(cx.executor()); + + // // Create a project in /project directory + // fs.insert_tree("/project", json!({})).await; + // let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // // Test file outside project requires confirmation + // let input_outside = json!({ + // "display_description": "Edit file", + // "path": "/outside/file.txt", + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert!( + // tool.needs_confirmation(&input_outside, &project, cx), + // "File outside project should require confirmation" + // ); + // }); + + // // Test file inside project doesn't require confirmation + // let input_inside = json!({ + // "display_description": "Edit file", + // "path": "project/file.txt", + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert!( + // !tool.needs_confirmation(&input_inside, &project, cx), + // "File inside project should not require confirmation" + // ); + // }); + // } + + // #[gpui::test] + // async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) { + // // Set up a custom data directory for testing + // let temp_dir = tempfile::tempdir().unwrap(); + // init_test_with_config(cx, temp_dir.path()); + + // let tool = Arc::new(EditFileTool); + // let fs = project::FakeFs::new(cx.executor()); + // fs.insert_tree("/home/user/myproject", json!({})).await; + // let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await; + + // // Get the actual local settings folder name + // let local_settings_folder = paths::local_settings_folder_relative_path(); + + // // Test various config path patterns + // let test_cases = vec![ + // ( + // format!("{}/settings.json", local_settings_folder.display()), + // true, + // "Top-level local settings file".to_string(), + // ), + // ( + // format!( + // "myproject/{}/settings.json", + // local_settings_folder.display() + // ), + // true, + // "Local settings in project path".to_string(), + // ), + // ( + // format!("src/{}/config.toml", local_settings_folder.display()), + // true, + // "Local settings in subdirectory".to_string(), + // ), + // ( + // ".zed.backup/file.txt".to_string(), + // true, + // ".zed.backup is outside project".to_string(), + // ), + // ( + // "my.zed/file.txt".to_string(), + // true, + // "my.zed is outside project".to_string(), + // ), + // ( + // "myproject/src/file.zed".to_string(), + // false, + // ".zed as file extension".to_string(), + // ), + // ( + // "myproject/normal/path/file.rs".to_string(), + // false, + // "Normal file without config paths".to_string(), + // ), + // ]; + + // for (path, should_confirm, description) in test_cases { + // let input = json!({ + // "display_description": "Edit file", + // "path": path, + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert_eq!( + // tool.needs_confirmation(&input, &project, cx), + // should_confirm, + // "Failed for case: {} - path: {}", + // description, + // path + // ); + // }); + // } + // } + + // #[gpui::test] + // async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) { + // // Set up a custom data directory for testing + // let temp_dir = tempfile::tempdir().unwrap(); + // init_test_with_config(cx, temp_dir.path()); + + // let tool = Arc::new(EditFileTool); + // let fs = project::FakeFs::new(cx.executor()); + + // // Create test files in the global config directory + // let global_config_dir = paths::config_dir(); + // fs::create_dir_all(&global_config_dir).unwrap(); + // let global_settings_path = global_config_dir.join("settings.json"); + // fs::write(&global_settings_path, "{}").unwrap(); + + // fs.insert_tree("/project", json!({})).await; + // let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // // Test global config paths + // let test_cases = vec![ + // ( + // global_settings_path.to_str().unwrap().to_string(), + // true, + // "Global settings file should require confirmation", + // ), + // ( + // global_config_dir + // .join("keymap.json") + // .to_str() + // .unwrap() + // .to_string(), + // true, + // "Global keymap file should require confirmation", + // ), + // ( + // "project/normal_file.rs".to_string(), + // false, + // "Normal project file should not require confirmation", + // ), + // ]; + + // for (path, should_confirm, description) in test_cases { + // let input = json!({ + // "display_description": "Edit file", + // "path": path, + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert_eq!( + // tool.needs_confirmation(&input, &project, cx), + // should_confirm, + // "Failed for case: {}", + // description + // ); + // }); + // } + // } + + // #[gpui::test] + // async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { + // init_test(cx); + // let tool = Arc::new(EditFileTool); + // let fs = project::FakeFs::new(cx.executor()); + + // // Create multiple worktree directories + // fs.insert_tree( + // "/workspace/frontend", + // json!({ + // "src": { + // "main.js": "console.log('frontend');" + // } + // }), + // ) + // .await; + // fs.insert_tree( + // "/workspace/backend", + // json!({ + // "src": { + // "main.rs": "fn main() {}" + // } + // }), + // ) + // .await; + // fs.insert_tree( + // "/workspace/shared", + // json!({ + // ".zed": { + // "settings.json": "{}" + // } + // }), + // ) + // .await; + + // // Create project with multiple worktrees + // let project = Project::test( + // fs.clone(), + // [ + // path!("/workspace/frontend").as_ref(), + // path!("/workspace/backend").as_ref(), + // path!("/workspace/shared").as_ref(), + // ], + // cx, + // ) + // .await; + + // // Test files in different worktrees + // let test_cases = vec![ + // ("frontend/src/main.js", false, "File in first worktree"), + // ("backend/src/main.rs", false, "File in second worktree"), + // ( + // "shared/.zed/settings.json", + // true, + // ".zed file in third worktree", + // ), + // ("/etc/hosts", true, "Absolute path outside all worktrees"), + // ( + // "../outside/file.txt", + // true, + // "Relative path outside worktrees", + // ), + // ]; + + // for (path, should_confirm, description) in test_cases { + // let input = json!({ + // "display_description": "Edit file", + // "path": path, + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert_eq!( + // tool.needs_confirmation(&input, &project, cx), + // should_confirm, + // "Failed for case: {} - path: {}", + // description, + // path + // ); + // }); + // } + // } + + // #[gpui::test] + // async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { + // init_test(cx); + // let tool = Arc::new(EditFileTool); + // let fs = project::FakeFs::new(cx.executor()); + // fs.insert_tree( + // "/project", + // json!({ + // ".zed": { + // "settings.json": "{}" + // }, + // "src": { + // ".zed": { + // "local.json": "{}" + // } + // } + // }), + // ) + // .await; + // let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // // Test edge cases + // let test_cases = vec![ + // // Empty path - find_project_path returns Some for empty paths + // ("", false, "Empty path is treated as project root"), + // // Root directory + // ("/", true, "Root directory should be outside project"), + // // Parent directory references - find_project_path resolves these + // ( + // "project/../other", + // false, + // "Path with .. is resolved by find_project_path", + // ), + // ( + // "project/./src/file.rs", + // false, + // "Path with . should work normally", + // ), + // // Windows-style paths (if on Windows) + // #[cfg(target_os = "windows")] + // ("C:\\Windows\\System32\\hosts", true, "Windows system path"), + // #[cfg(target_os = "windows")] + // ("project\\src\\main.rs", false, "Windows-style project path"), + // ]; + + // for (path, should_confirm, description) in test_cases { + // let input = json!({ + // "display_description": "Edit file", + // "path": path, + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert_eq!( + // tool.needs_confirmation(&input, &project, cx), + // should_confirm, + // "Failed for case: {} - path: {}", + // description, + // path + // ); + // }); + // } + // } + + // #[gpui::test] + // async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) { + // init_test(cx); + // let tool = Arc::new(EditFileTool); + + // // Test UI text for various scenarios + // let test_cases = vec![ + // ( + // json!({ + // "display_description": "Update config", + // "path": ".zed/settings.json", + // "mode": "edit" + // }), + // "Update config (local settings)", + // ".zed path should show local settings context", + // ), + // ( + // json!({ + // "display_description": "Fix bug", + // "path": "src/.zed/local.json", + // "mode": "edit" + // }), + // "Fix bug (local settings)", + // "Nested .zed path should show local settings context", + // ), + // ( + // json!({ + // "display_description": "Update readme", + // "path": "README.md", + // "mode": "edit" + // }), + // "Update readme", + // "Normal path should not show additional context", + // ), + // ( + // json!({ + // "display_description": "Edit config", + // "path": "config.zed", + // "mode": "edit" + // }), + // "Edit config", + // ".zed as extension should not show context", + // ), + // ]; + + // for (input, expected_text, description) in test_cases { + // cx.update(|_cx| { + // let ui_text = tool.ui_text(&input); + // assert_eq!(ui_text, expected_text, "Failed for case: {}", description); + // }); + // } + // } + + // #[gpui::test] + // async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { + // init_test(cx); + // let tool = Arc::new(EditFileTool); + // let fs = project::FakeFs::new(cx.executor()); + // fs.insert_tree( + // "/project", + // json!({ + // "existing.txt": "content", + // ".zed": { + // "settings.json": "{}" + // } + // }), + // ) + // .await; + // let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // // Test different EditFileMode values + // let modes = vec![ + // EditFileMode::Edit, + // EditFileMode::Create, + // EditFileMode::Overwrite, + // ]; + + // for mode in modes { + // // Test .zed path with different modes + // let input_zed = json!({ + // "display_description": "Edit settings", + // "path": "project/.zed/settings.json", + // "mode": mode + // }); + // cx.update(|cx| { + // assert!( + // tool.needs_confirmation(&input_zed, &project, cx), + // ".zed path should require confirmation regardless of mode: {:?}", + // mode + // ); + // }); + + // // Test outside path with different modes + // let input_outside = json!({ + // "display_description": "Edit file", + // "path": "/outside/file.txt", + // "mode": mode + // }); + // cx.update(|cx| { + // assert!( + // tool.needs_confirmation(&input_outside, &project, cx), + // "Outside path should require confirmation regardless of mode: {:?}", + // mode + // ); + // }); + + // // Test normal path with different modes + // let input_normal = json!({ + // "display_description": "Edit file", + // "path": "project/normal.txt", + // "mode": mode + // }); + // cx.update(|cx| { + // assert!( + // !tool.needs_confirmation(&input_normal, &project, cx), + // "Normal path should not require confirmation regardless of mode: {:?}", + // mode + // ); + // }); + // } + // } + + // #[gpui::test] + // async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) { + // // Set up with custom directories for deterministic testing + // let temp_dir = tempfile::tempdir().unwrap(); + // init_test_with_config(cx, temp_dir.path()); + + // let tool = Arc::new(EditFileTool); + // let fs = project::FakeFs::new(cx.executor()); + // fs.insert_tree("/project", json!({})).await; + // let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // // Enable always_allow_tool_actions + // cx.update(|cx| { + // let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); + // settings.always_allow_tool_actions = true; + // agent_settings::AgentSettings::override_global(settings, cx); + // }); + + // // Test that all paths that normally require confirmation are bypassed + // let global_settings_path = paths::config_dir().join("settings.json"); + // fs::create_dir_all(paths::config_dir()).unwrap(); + // fs::write(&global_settings_path, "{}").unwrap(); + + // let test_cases = vec![ + // ".zed/settings.json", + // "project/.zed/config.toml", + // global_settings_path.to_str().unwrap(), + // "/etc/hosts", + // "/absolute/path/file.txt", + // "../outside/project.txt", + // ]; + + // for path in test_cases { + // let input = json!({ + // "display_description": "Edit file", + // "path": path, + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert!( + // !tool.needs_confirmation(&input, &project, cx), + // "Path {} should not require confirmation when always_allow_tool_actions is true", + // path + // ); + // }); + // } + + // // Disable always_allow_tool_actions and verify confirmation is required again + // cx.update(|cx| { + // let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); + // settings.always_allow_tool_actions = false; + // agent_settings::AgentSettings::override_global(settings, cx); + // }); + + // // Verify .zed path requires confirmation again + // let input = json!({ + // "display_description": "Edit file", + // "path": ".zed/settings.json", + // "mode": "edit" + // }); + // cx.update(|cx| { + // assert!( + // tool.needs_confirmation(&input, &project, cx), + // ".zed path should require confirmation when always_allow_tool_actions is false" + // ); + // }); + // } +} diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent2/src/tools/read_file_tool.rs index 30794ccdad..27419a452e 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent2/src/tools/read_file_tool.rs @@ -285,8 +285,6 @@ impl AgentTool for ReadFileTool { #[cfg(test)] mod test { - use crate::TestToolCallEventStream; - use super::*; use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher}; @@ -304,7 +302,7 @@ mod test { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); + let (event_stream, _) = ToolCallEventStream::test(); let result = cx .update(|cx| { @@ -313,7 +311,7 @@ mod test { start_line: None, end_line: None, }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, event_stream, cx) }) .await; assert_eq!( @@ -321,6 +319,7 @@ mod test { "root/nonexistent_file.txt not found" ); } + #[gpui::test] async fn test_read_small_file(cx: &mut TestAppContext) { init_test(cx); @@ -336,7 +335,6 @@ mod test { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); let result = cx .update(|cx| { let input = ReadFileToolInput { @@ -344,7 +342,7 @@ mod test { start_line: None, end_line: None, }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, ToolCallEventStream::test().0, cx) }) .await; assert_eq!(result.unwrap(), "This is a small file content"); @@ -367,7 +365,6 @@ mod test { language_registry.add(Arc::new(rust_lang())); let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); let content = cx .update(|cx| { let input = ReadFileToolInput { @@ -375,7 +372,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await .unwrap(); @@ -399,7 +396,7 @@ mod test { start_line: None, end_line: None, }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, ToolCallEventStream::test().0, cx) }) .await; let content = result.unwrap(); @@ -438,7 +435,6 @@ mod test { let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); let result = cx .update(|cx| { let input = ReadFileToolInput { @@ -446,7 +442,7 @@ mod test { start_line: Some(2), end_line: Some(4), }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, ToolCallEventStream::test().0, cx) }) .await; assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4"); @@ -467,7 +463,6 @@ mod test { let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); // start_line of 0 should be treated as 1 let result = cx @@ -477,7 +472,7 @@ mod test { start_line: Some(0), end_line: Some(2), }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert_eq!(result.unwrap(), "Line 1\nLine 2"); @@ -490,7 +485,7 @@ mod test { start_line: Some(1), end_line: Some(0), }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert_eq!(result.unwrap(), "Line 1"); @@ -503,7 +498,7 @@ mod test { start_line: Some(3), end_line: Some(2), }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert_eq!(result.unwrap(), "Line 3"); @@ -612,7 +607,6 @@ mod test { let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project, action_log)); - let event_stream = TestToolCallEventStream::new(); // Reading a file outside the project worktree should fail let result = cx @@ -622,7 +616,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -638,7 +632,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -654,7 +648,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -669,7 +663,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -685,7 +679,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -700,7 +694,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -715,7 +709,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -731,7 +725,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; assert!(result.is_ok(), "Should be able to read normal files"); @@ -745,7 +739,7 @@ mod test { start_line: None, end_line: None, }; - tool.run(input, event_stream.stream(), cx) + tool.run(input, ToolCallEventStream::test().0, cx) }) .await; assert!( @@ -826,7 +820,6 @@ mod test { let action_log = cx.new(|_| ActionLog::new(project.clone())); let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone())); - let event_stream = TestToolCallEventStream::new(); // Test reading allowed files in worktree1 let result = cx @@ -836,7 +829,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await .unwrap(); @@ -851,7 +844,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; @@ -872,7 +865,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; @@ -893,7 +886,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await .unwrap(); @@ -911,7 +904,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; @@ -932,7 +925,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await; @@ -954,7 +947,7 @@ mod test { start_line: None, end_line: None, }; - tool.clone().run(input, event_stream.stream(), cx) + tool.clone().run(input, ToolCallEventStream::test().0, cx) }) .await;