From 970b5fe06e6dc6c6bcc8b67d713b6cc291def409 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 8 Aug 2025 13:59:05 +0200 Subject: [PATCH] Get tests working Co-authored-by: Antonio Scandurra --- crates/agent2/src/thread.rs | 40 +- crates/agent2/src/tools/edit_file_tool.rs | 1732 +++++++++------------ crates/assistant_tools/src/edit_agent.rs | 6 - 3 files changed, 776 insertions(+), 1002 deletions(-) diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index e7df7b3b22..e60387bd44 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -920,10 +920,7 @@ pub struct ToolCallEventStream { impl ToolCallEventStream { #[cfg(test)] - pub fn test() -> ( - Self, - mpsc::UnboundedReceiver>, - ) { + pub fn test() -> (Self, ToolCallEventStreamReceiver) { let (events_tx, events_rx) = mpsc::unbounded::>(); @@ -939,7 +936,7 @@ impl ToolCallEventStream { AgentResponseEventStream(events_tx), ); - (stream, events_rx) + (stream, ToolCallEventStreamReceiver(events_rx)) } fn new( @@ -975,3 +972,36 @@ impl ToolCallEventStream { ) } } + +#[cfg(test)] +pub struct ToolCallEventStreamReceiver( + mpsc::UnboundedReceiver>, +); + +#[cfg(test)] +impl ToolCallEventStreamReceiver { + pub async fn expect_tool_authorization(&mut self) -> ToolCallAuthorization { + let event = self.0.next().await; + if let Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth))) = event { + auth + } else { + panic!("Expected ToolCallAuthorization but got: {:?}", event); + } + } +} + +#[cfg(test)] +impl std::ops::Deref for ToolCallEventStreamReceiver { + type Target = mpsc::UnboundedReceiver>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +impl std::ops::DerefMut for ToolCallEventStreamReceiver { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index 8c96c248d5..2cd4cd5a96 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -439,7 +439,8 @@ mod tests { use super::*; use assistant_tool::ActionLog; use client::TelemetrySettings; - use gpui::TestAppContext; + use fs::Fs; + use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use serde_json::json; use settings::SettingsStore; @@ -565,982 +566,745 @@ mod tests { assert_eq!(actual, expected); } - // #[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" - // ); - // }); - // } + #[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()); + let thread = cx.new(|_| { + Thread::new( + project, + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + + // 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 = EditFileToolInput { + display_description: "Create main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }; + Arc::new(EditFileTool { + thread: thread.clone(), + }) + .run(input, ToolCallEventStream::test().0, cx) + }); + + // 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 = EditFileToolInput { + display_description: "Update main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }; + Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx) + }); + + // 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()); + let thread = cx.new(|_| { + Thread::new( + project, + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + + // 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 = EditFileToolInput { + display_description: "Create main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }; + Arc::new(EditFileTool { + thread: thread.clone(), + }) + .run(input, ToolCallEventStream::test().0, cx) + }); + + // 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 = EditFileToolInput { + display_description: "Update main function".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Overwrite, + }; + Arc::new(EditFileTool { + thread: thread.clone(), + }) + .run(input, ToolCallEventStream::test().0, cx) + }); + + // 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 fs = project::FakeFs::new(cx.executor()); + 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.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + fs.insert_tree("/root", json!({})).await; + + // Test 1: Path with .zed component should require confirmation + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 1".into(), + path: ".zed/settings.json".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + + let event = stream_rx.expect_tool_authorization().await; + assert_eq!(event.tool_call.title, "test 1 (local settings)"); + + // Test 2: Path outside project should require confirmation + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 2".into(), + path: "/etc/hosts".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + + let event = stream_rx.expect_tool_authorization().await; + assert_eq!(event.tool_call.title, "test 2"); + + // Test 3: Relative path without .zed should not require confirmation + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 3".into(), + path: "root/src/main.rs".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }) + .await + .unwrap(); + assert!(stream_rx.try_next().is_err()); + + // Test 4: Path with .zed in the middle should require confirmation + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 4".into(), + path: "root/.zed/tasks.json".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + let event = stream_rx.expect_tool_authorization().await; + assert_eq!(event.tool_call.title, "test 4 (local settings)"); + + // Test 5: Path outside of the project should require confirmation. + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 5".into(), + path: paths::config_dir().join("tasks.json"), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + let event = stream_rx.expect_tool_authorization().await; + assert_eq!(event.tool_call.title, "test 5 (global settings)"); + + // Test 6: 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); + }); + + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 6.1".into(), + path: ".zed/settings.json".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }) + .await + .unwrap(); + assert!(stream_rx.try_next().is_err()); + + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "test 6.2".into(), + path: "/etc/hosts".into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }) + .await + .unwrap(); + assert!(stream_rx.try_next().is_err()); + } + + #[gpui::test] + async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { + init_test(cx); + 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; + + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project.clone(), + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + + // 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 (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit file".into(), + path: path.into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + + if should_confirm { + stream_rx.expect_tool_authorization().await; + } else { + auth.await.unwrap(); + assert!( + stream_rx.try_next().is_err(), + "Failed for case: {} - path: {} - expected no confirmation but got one", + description, + path + ); + } + } + } + + #[gpui::test] + async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { + init_test(cx); + 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; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project.clone(), + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + + // 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 (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit file".into(), + path: path.into(), + mode: EditFileMode::Edit, + }, + &stream_tx, + cx, + ) + }); + + if should_confirm { + stream_rx.expect_tool_authorization().await; + } else { + auth.await.unwrap(); + assert!( + stream_rx.try_next().is_err(), + "Failed for case: {} - path: {} - expected no confirmation but got one", + description, + path + ); + } + } + } + + #[gpui::test] + async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { + init_test(cx); + 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; + let action_log = cx.new(|_| ActionLog::new(project.clone())); + let model = Arc::new(FakeLanguageModel::default()); + let thread = cx.new(|_| { + Thread::new( + project.clone(), + Rc::default(), + action_log.clone(), + Templates::new(), + model.clone(), + ) + }); + let tool = Arc::new(EditFileTool { thread }); + + // Test different EditFileMode values + let modes = vec![ + EditFileMode::Edit, + EditFileMode::Create, + EditFileMode::Overwrite, + ]; + + for mode in modes { + // Test .zed path with different modes + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit settings".into(), + path: "project/.zed/settings.json".into(), + mode: mode.clone(), + }, + &stream_tx, + cx, + ) + }); + + stream_rx.expect_tool_authorization().await; + + // Test outside path with different modes + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + let _auth = cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit file".into(), + path: "/outside/file.txt".into(), + mode: mode.clone(), + }, + &stream_tx, + cx, + ) + }); + + stream_rx.expect_tool_authorization().await; + + // Test normal path with different modes + let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); + cx.update(|cx| { + tool.authorize( + &EditFileToolInput { + display_description: "Edit file".into(), + path: "project/normal.txt".into(), + mode: mode.clone(), + }, + &stream_tx, + cx, + ) + }) + .await + .unwrap(); + assert!(stream_rx.try_next().is_err()); + } + } fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { @@ -1552,18 +1316,4 @@ mod tests { 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); - }); - } } diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index 715d106a26..dcb14a48f3 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -29,7 +29,6 @@ use serde::{Deserialize, Serialize}; use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::Poll}; use streaming_diff::{CharOperation, StreamingDiff}; use streaming_fuzzy_matcher::StreamingFuzzyMatcher; -use util::debug_panic; #[derive(Serialize)] struct CreateFilePromptTemplate { @@ -682,11 +681,6 @@ impl EditAgent { if last_message.content.is_empty() { conversation.messages.pop(); } - } else { - debug_panic!( - "Last message must be an Assistant tool calling! Got {:?}", - last_message.content - ); } }