-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathrpc_shell_user_requested.rs
More file actions
173 lines (156 loc) · 6.6 KB
/
Copy pathrpc_shell_user_requested.rs
File metadata and controls
173 lines (156 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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;
// Await the spawned task by mutable reference so a timeout can abort it instead of
// dropping the handle. A dropped JoinHandle detaches the task, leaving the shell
// command running in the background where it can keep file handles open and
// destabilize later tests.
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('\'', "'\\''"))
}