use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use github_copilot_sdk::RequestId;
use github_copilot_sdk::rpc::{ShellCancelUserRequestedRequest, ShellExecuteUserRequestedRequest};
use super::support::{wait_for_condition, with_e2e_context};
#[tokio::test]
async fn should_execute_user_requested_shell_command() {
with_e2e_context(
"rpc_shell_user_requested",
"should_execute_user_requested_shell_command",
|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 marker = format!("copilotusershell{}", uuid::Uuid::new_v4().simple());
let request_id = RequestId::new(format!("req-{}", uuid::Uuid::new_v4().simple()));
let result = session
.rpc()
.shell()
.execute_user_requested(ShellExecuteUserRequestedRequest {
request_id,
command: format!("echo {marker}"),
})
.await
.expect("execute user-requested shell command");
assert!(
result.success,
"expected shell command to succeed: {:?}",
result.error
);
assert_eq!(result.exit_code, Some(0));
assert!(result.output.contains(&marker));
assert!(!result.tool_call_id.trim().is_empty());
session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
})
},
)
.await;
}
#[tokio::test]
async fn should_cancel_user_requested_shell_command() {
with_e2e_context(
"rpc_shell_user_requested",
"should_cancel_user_requested_shell_command",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
let client = ctx.start_client().await;
let session = Arc::new(
client
.create_session(ctx.approve_all_session_config())
.await
.expect("create session"),
);
let missing = session
.rpc()
.shell()
.cancel_user_requested(ShellCancelUserRequestedRequest {
request_id: RequestId::new(format!(
"missing-{}",
uuid::Uuid::new_v4().simple()
)),
})
.await
.expect("cancel missing request");
assert!(!missing.cancelled);
let request_id = RequestId::new(format!("req-{}", uuid::Uuid::new_v4().simple()));
let marker_dir = tempfile::Builder::new()
.prefix("shell-cancel-")
.tempdir()
.expect("create shell cancel marker directory");
let marker_path = marker_dir.path().join("marker.txt");
let command = create_marker_then_sleep_command(&marker_path, 60);
let execute_session = Arc::clone(&session);
let execute_request_id = request_id.clone();
let mut execute_task = tokio::spawn(async move {
execute_session
.rpc()
.shell()
.execute_user_requested(ShellExecuteUserRequestedRequest {
request_id: execute_request_id,
command,
})
.await
});
wait_for_file_text(&marker_path, "running").await;
wait_for_condition("user-requested shell command cancellation", || {
let session = Arc::clone(&session);
let request_id = request_id.clone();
async move {
session
.rpc()
.shell()
.cancel_user_requested(ShellCancelUserRequestedRequest { request_id })
.await
.expect("cancel running request")
.cancelled
}
})
.await;
let result =
match tokio::time::timeout(Duration::from_secs(30), &mut execute_task).await {
Ok(joined) => joined
.expect("shell execution task should not panic")
.expect("execute user-requested shell command"),
Err(_elapsed) => {
execute_task.abort();
panic!("cancelled shell command did not finish within 30s");
}
};
assert!(!result.success);
session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
})
},
)
.await;
}
async fn wait_for_file_text(path: &Path, expected: &'static str) {
wait_for_condition("shell marker text", || async {
std::fs::read_to_string(path).is_ok_and(|content| content.contains(expected))
})
.await;
}
#[cfg(windows)]
fn create_marker_then_sleep_command(marker_path: &Path, seconds: u64) -> String {
format!(
"Set-Content -LiteralPath {} -Value 'running'; Start-Sleep -Seconds {seconds}",
powershell_quote(marker_path)
)
}
#[cfg(not(windows))]
fn create_marker_then_sleep_command(marker_path: &Path, seconds: u64) -> String {
format!(
"echo running > {}; sleep {seconds}",
posix_shell_quote(marker_path)
)
}
#[cfg(windows)]
fn powershell_quote(path: &Path) -> String {
format!("'{}'", path.display().to_string().replace('\'', "''"))
}
#[cfg(not(windows))]
fn posix_shell_quote(path: &Path) -> String {
format!("'{}'", path.display().to_string().replace('\'', "'\\''"))
}