use std::time::Duration; use github_copilot_sdk::MessageOptions; use super::support::assistant_message_content; /// Built-in tool tests spawn a real CLI subprocess and execute actual shell / /// file tools. Under concurrent Windows CI load (e2e runs 4-wide on a 4-vCPU /// runner) this agent loop can briefly exceed the 60s `send_and_wait` default, /// so give it extra headroom while still failing fast on a genuine hang. const SEND_TIMEOUT: Duration = Duration::from_secs(120); fn message(prompt: &str) -> MessageOptions { MessageOptions::from(prompt).with_wait_timeout(SEND_TIMEOUT) } #[tokio::test] async fn should_capture_exit_code_in_output() { super::support::with_shared_e2e_context( &E2E, "builtin_tools", "should_capture_exit_code_in_output", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); let msg = session .send_and_wait(message( "Run 'echo hello && echo world'. Tell me the exact output.", )) .await .expect("send") .expect("assistant message"); let content = assistant_message_content(&msg); assert!(content.contains("hello")); assert!(content.contains("world")); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); }) }, ) .await; } #[tokio::test] async fn should_capture_stderr_output() { super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_capture_stderr_output", |ctx| { Box::pin(async move { if cfg!(windows) { return; } ctx.set_default_copilot_user(); let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); let msg = session .send_and_wait(message("Run 'echo error_msg >&2; sleep 0.5; echo ok' and tell me what stderr said. Reply with just the stderr content.")) .await .expect("send") .expect("assistant message"); assert!(assistant_message_content(&msg).contains("error_msg")); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); }) }) .await; } #[tokio::test] async fn should_read_file_with_line_range() { super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_read_file_with_line_range", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); std::fs::write(ctx.work_dir().join("lines.txt"), "line1\nline2\nline3\nline4\nline5\n") .expect("write lines file"); let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); let msg = session .send_and_wait(message("Read lines 2 through 4 of the file 'lines.txt' in this directory. Tell me what those lines contain.")) .await .expect("send") .expect("assistant message"); let content = assistant_message_content(&msg); assert!(content.contains("line2")); assert!(content.contains("line4")); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); }) }) .await; } #[tokio::test] async fn should_handle_nonexistent_file_gracefully() { super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_handle_nonexistent_file_gracefully", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); let msg = session .send_and_wait(message("Try to read the file 'does_not_exist.txt'. If it doesn't exist, say 'FILE_NOT_FOUND'.")) .await .expect("send") .expect("assistant message"); let content = assistant_message_content(&msg).to_uppercase(); assert!( content.contains("NOT FOUND") || content.contains("NOT EXIST") || content.contains("NO SUCH") || content.contains("FILE_NOT_FOUND") || content.contains("DOES NOT EXIST") || content.contains("ERROR"), "expected missing-file response, got: {content}" ); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); }) }, ) .await; } #[tokio::test] async fn should_edit_a_file_successfully() { super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_edit_a_file_successfully", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); std::fs::write(ctx.work_dir().join("edit_me.txt"), "Hello World\nGoodbye World\n") .expect("write edit file"); let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); let msg = session .send_and_wait(message("Edit the file 'edit_me.txt': replace 'Hello World' with 'Hi Universe'. Then read it back and tell me its contents.")) .await .expect("send") .expect("assistant message"); assert!(assistant_message_content(&msg).contains("Hi Universe")); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); }) }) .await; } #[tokio::test] async fn should_create_a_new_file() { super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_create_a_new_file", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); let msg = session .send_and_wait(message("Create a file called 'new_file.txt' with the content 'Created by test'. Then read it back to confirm.")) .await .expect("send") .expect("assistant message"); assert!(assistant_message_content(&msg).contains("Created by test")); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); }) }) .await; } #[tokio::test] async fn should_search_for_patterns_in_files() { super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_search_for_patterns_in_files", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); std::fs::write(ctx.work_dir().join("data.txt"), "apple\nbanana\napricot\ncherry\n") .expect("write data file"); let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); let msg = session .send_and_wait(message("Search for lines starting with 'ap' in the file 'data.txt'. Tell me which lines matched.")) .await .expect("send") .expect("assistant message"); let content = assistant_message_content(&msg); assert!(content.contains("apple")); assert!(content.contains("apricot")); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); }) }, ) .await; } #[tokio::test] async fn should_find_files_by_pattern() { super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_find_files_by_pattern", |ctx| { Box::pin(async move { ctx.set_default_copilot_user(); let src = ctx.work_dir().join("src"); std::fs::create_dir(&src).expect("create src directory"); std::fs::write(src.join("index.ts"), "export const index = 1;") .expect("write index.ts"); std::fs::write(ctx.work_dir().join("README.md"), "# Readme").expect("write readme"); let client = ctx.start_client().await; let session = client .create_session(ctx.approve_all_session_config()) .await .expect("create session"); let msg = session .send_and_wait(message("Find all .ts files in this directory (recursively). List the filenames you found.")) .await .expect("send") .expect("assistant message"); assert!(assistant_message_content(&msg).contains("index.ts")); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); }) }) .await; } static E2E: super::support::SharedE2eGroup = super::support::SharedE2eGroup::standard("builtin_tools", 8);