Skip to content

Commit 458dd30

Browse files
stephentoubCopilot
andcommitted
Address Rust E2E review feedback
Move struct-only E2E placeholders into unit tests, exercise invalid external-auth client options, avoid logging session IDs from test assertions, and harden failing Rust E2Es on CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dc1e93d commit 458dd30

10 files changed

Lines changed: 114 additions & 82 deletions

File tree

rust/src/lib.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,24 @@ impl Client {
927927
if let Some(cfg) = &options.session_fs {
928928
validate_session_fs_config(cfg)?;
929929
}
930+
// Auth options only make sense when the SDK spawns the CLI; with an
931+
// external server, the server manages its own auth.
932+
if matches!(options.transport, Transport::External { .. }) {
933+
if options.github_token.is_some() {
934+
return Err(Error::InvalidConfig(
935+
"github_token cannot be used with Transport::External \
936+
(external server manages its own auth)"
937+
.to_string(),
938+
));
939+
}
940+
if options.use_logged_in_user.is_some() {
941+
return Err(Error::InvalidConfig(
942+
"use_logged_in_user cannot be used with Transport::External \
943+
(external server manages its own auth)"
944+
.to_string(),
945+
));
946+
}
947+
}
930948
// Validate token + transport combination. Stdio cannot use a
931949
// connection token; auto-generate a UUID when the SDK spawns
932950
// its own CLI in TCP mode and no explicit token was set.
Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,89 @@
1+
// Unit tests for generated API types -- struct construction and field
2+
// access. These do not require a client, session, or replay proxy.
3+
4+
#![allow(clippy::unwrap_used)]
5+
16
use github_copilot_sdk::generated::api_types::{
27
Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest,
3-
ExtensionsEnableRequest,
8+
ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest,
49
};
510

6-
#[tokio::test]
7-
async fn discovers_loads_and_reports_running_extension() {
11+
#[test]
12+
fn extension_running_has_expected_status_and_source() {
813
let extension = running_extension("project:demo", "demo");
9-
1014
assert_eq!(extension.status, ExtensionStatus::Running);
1115
assert_eq!(extension.source, ExtensionSource::Project);
1216
}
1317

14-
#[tokio::test]
15-
async fn disable_then_enable_cycles_extension_status() {
18+
#[test]
19+
fn disable_and_enable_requests_share_the_same_id() {
1620
let disable = ExtensionsDisableRequest {
1721
id: "project:demo".to_string(),
1822
};
1923
let enable = ExtensionsEnableRequest {
2024
id: disable.id.clone(),
2125
};
22-
2326
assert_eq!(disable.id, enable.id);
2427
}
2528

26-
#[tokio::test]
27-
async fn reload_picks_up_extension_added_after_session_create() {
29+
#[test]
30+
fn extension_list_contains_newly_added_extension_by_name() {
2831
let list = ExtensionList {
2932
extensions: vec![running_extension("project:late", "late")],
3033
};
31-
32-
assert!(
33-
list.extensions
34-
.iter()
35-
.any(|extension| extension.name == "late")
36-
);
34+
assert!(list.extensions.iter().any(|e| e.name == "late"));
3735
}
3836

39-
#[tokio::test]
40-
async fn failed_extension_reports_failed_status() {
37+
#[test]
38+
fn failed_extension_reports_failed_status() {
4139
let mut extension = running_extension("project:broken", "broken");
4240
extension.status = ExtensionStatus::Failed;
43-
4441
assert_eq!(extension.status, ExtensionStatus::Failed);
4542
}
4643

47-
#[tokio::test]
48-
async fn multiple_extensions_are_discovered_independently() {
44+
#[test]
45+
fn multiple_extensions_have_distinct_ids() {
4946
let list = ExtensionList {
5047
extensions: vec![
5148
running_extension("project:first", "first"),
5249
running_extension("user:second", "second"),
5350
],
5451
};
55-
5652
assert_eq!(list.extensions.len(), 2);
5753
assert_ne!(list.extensions[0].id, list.extensions[1].id);
5854
}
5955

60-
#[tokio::test]
61-
async fn reload_preserves_disabled_state_across_calls() {
56+
#[test]
57+
fn disabled_extension_preserves_disabled_status() {
6258
let mut extension = running_extension("project:disabled", "disabled");
6359
extension.status = ExtensionStatus::Disabled;
64-
6560
assert_eq!(extension.status, ExtensionStatus::Disabled);
6661
}
6762

63+
#[test]
64+
fn fleet_start_request_and_result_fields_are_accessible() {
65+
let request = FleetStartRequest {
66+
prompt: Some("Use the custom tool".to_string()),
67+
};
68+
let result = FleetStartResult { started: true };
69+
assert_eq!(request.prompt.as_deref(), Some("Use the custom tool"));
70+
assert!(result.started);
71+
}
72+
73+
#[test]
74+
fn tasks_start_agent_request_fields_are_accessible() {
75+
let request = TasksStartAgentRequest {
76+
agent_type: "general-purpose".to_string(),
77+
prompt: "Say hi".to_string(),
78+
name: "sdk-test-task".to_string(),
79+
description: Some("SDK task agent".to_string()),
80+
model: None,
81+
};
82+
assert_eq!(request.agent_type, "general-purpose");
83+
assert_eq!(request.name, "sdk-test-task");
84+
assert_eq!(request.description.as_deref(), Some("SDK task agent"));
85+
}
86+
6887
fn running_extension(id: &str, name: &str) -> Extension {
6988
Extension {
7089
id: id.to_string(),

rust/tests/e2e.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ mod rpc_additional_edge_cases;
5151
mod rpc_agent;
5252
#[path = "e2e/rpc_event_side_effects.rs"]
5353
mod rpc_event_side_effects;
54-
#[path = "e2e/rpc_extensions_loaded.rs"]
55-
mod rpc_extensions_loaded;
5654
#[path = "e2e/rpc_mcp_and_skills.rs"]
5755
mod rpc_mcp_and_skills;
5856
#[path = "e2e/rpc_mcp_config.rs"]

rust/tests/e2e/abort.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ async fn should_abort_during_active_tool_execution() {
9393
)
9494
.await
9595
.expect("create session");
96+
let events = session.subscribe();
9697

9798
session
9899
.send("Use slow_analysis with value 'test_abort'. Wait for the result.")
@@ -106,6 +107,10 @@ async fn should_abort_during_active_tool_execution() {
106107
release_tx
107108
.send("RELEASED_AFTER_ABORT".to_string())
108109
.expect("release slow tool");
110+
wait_for_event(events, "session.idle after abort", |event| {
111+
event.parsed_type() == SessionEventType::SessionIdle
112+
})
113+
.await;
109114

110115
let recovery = session
111116
.send_and_wait("Say 'tool_abort_recovery_ok'.")

rust/tests/e2e/client_options.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
22

33
use github_copilot_sdk::{
4-
Client, ClientOptions, LogLevel, MessageOptions, OtelExporterType, SessionConfig,
4+
Client, ClientOptions, Error, LogLevel, MessageOptions, OtelExporterType, SessionConfig,
55
TelemetryConfig, Transport,
66
};
77
use serde_json::json;
@@ -242,8 +242,18 @@ async fn should_throw_when_githubtoken_used_with_cliurl() {
242242
})
243243
.with_github_token("token");
244244

245-
assert!(matches!(options.transport, Transport::External { .. }));
246-
assert_eq!(options.github_token.as_deref(), Some("token"));
245+
let err = Client::start(options).await.unwrap_err();
246+
assert!(
247+
matches!(err, Error::InvalidConfig(_)),
248+
"expected InvalidConfig, got {err:?}"
249+
);
250+
let Error::InvalidConfig(msg) = err else {
251+
unreachable!()
252+
};
253+
assert!(
254+
msg.contains("github_token"),
255+
"error message should mention github_token, got: {msg}"
256+
);
247257
}
248258

249259
#[tokio::test]
@@ -255,8 +265,18 @@ async fn should_throw_when_useloggedinuser_used_with_cliurl() {
255265
})
256266
.with_use_logged_in_user(true);
257267

258-
assert!(matches!(options.transport, Transport::External { .. }));
259-
assert_eq!(options.use_logged_in_user, Some(true));
268+
let err = Client::start(options).await.unwrap_err();
269+
assert!(
270+
matches!(err, Error::InvalidConfig(_)),
271+
"expected InvalidConfig, got {err:?}"
272+
);
273+
let Error::InvalidConfig(msg) = err else {
274+
unreachable!()
275+
};
276+
assert!(
277+
msg.contains("use_logged_in_user"),
278+
"error message should mention use_logged_in_user, got: {msg}"
279+
);
260280
}
261281

262282
fn get_available_tcp_port() -> u16 {

rust/tests/e2e/rpc_shell_and_fleet.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use github_copilot_sdk::generated::api_types::{
2-
FleetStartRequest, FleetStartResult, ShellExecRequest, ShellKillRequest,
3-
};
1+
use github_copilot_sdk::generated::api_types::{ShellExecRequest, ShellKillRequest};
42

53
use super::support::{wait_for_condition, with_e2e_context};
64

@@ -82,17 +80,6 @@ async fn should_kill_shell_process() {
8280
.await;
8381
}
8482

85-
#[tokio::test]
86-
async fn should_start_fleet_and_complete_custom_tool_task() {
87-
let request = FleetStartRequest {
88-
prompt: Some("Use the custom tool".to_string()),
89-
};
90-
let result = FleetStartResult { started: true };
91-
92-
assert_eq!(request.prompt.as_deref(), Some("Use the custom tool"));
93-
assert!(result.started);
94-
}
95-
9683
async fn wait_for_file_text(path: &std::path::Path, expected: &'static str) {
9784
wait_for_condition("shell command output file", || async {
9885
match std::fs::read_to_string(path) {

rust/tests/e2e/rpc_tasks_and_handlers.rs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -159,21 +159,6 @@ async fn should_report_implemented_error_for_invalid_task_agent_model() {
159159
.await;
160160
}
161161

162-
#[tokio::test]
163-
async fn should_start_background_agent_and_report_task_details() {
164-
let request = TasksStartAgentRequest {
165-
agent_type: "general-purpose".to_string(),
166-
prompt: "Say hi".to_string(),
167-
name: "sdk-test-task".to_string(),
168-
description: Some("SDK task agent".to_string()),
169-
model: None,
170-
};
171-
172-
assert_eq!(request.agent_type, "general-purpose");
173-
assert_eq!(request.name, "sdk-test-task");
174-
assert_eq!(request.description.as_deref(), Some("SDK task agent"));
175-
}
176-
177162
#[tokio::test]
178163
async fn should_return_expected_results_for_missing_pending_handler_requestids() {
179164
with_e2e_context(

rust/tests/e2e/session_fs.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async fn should_route_file_operations_through_the_session_fs_provider() {
3838
assert!(assistant_message_content(&answer).contains("300"));
3939
let events_path = provider_root
4040
.join(session.id().as_ref())
41-
.join("session-state")
41+
.join(provider_relative_path(&session_state_path()))
4242
.join("events.jsonl");
4343
wait_for_file_containing(&events_path, "300").await;
4444
let content = std::fs::read_to_string(events_path).expect("read events");
@@ -214,7 +214,7 @@ async fn should_reject_setprovider_when_sessions_already_exist() {
214214
let config = session_fs_config();
215215

216216
assert_eq!(config.initial_cwd, "/");
217-
assert_eq!(config.session_state_path, "/session-state");
217+
assert_eq!(config.session_state_path, session_state_path());
218218
}
219219

220220
#[tokio::test]
@@ -277,7 +277,7 @@ async fn should_persist_plan_md_via_sessionfs() {
277277
.expect("update plan");
278278
let plan_path = provider_root
279279
.join(session.id().as_ref())
280-
.join("session-state")
280+
.join(provider_relative_path(&session_state_path()))
281281
.join("plan.md");
282282
wait_for_file_containing(&plan_path, "This is a test.").await;
283283
assert!(
@@ -383,7 +383,7 @@ async fn should_write_workspace_metadata_via_sessionfs() {
383383
assert!(assistant_message_content(&answer).contains("56"));
384384
let workspace_path = provider_root
385385
.join(session.id().as_ref())
386-
.join("session-state")
386+
.join(provider_relative_path(&session_state_path()))
387387
.join("workspace.yaml");
388388
wait_for_file_containing(&workspace_path, session.id().as_ref()).await;
389389

@@ -413,7 +413,23 @@ fn session_config(
413413
}
414414

415415
fn session_fs_config() -> SessionFsConfig {
416-
SessionFsConfig::new("/", "/session-state", SessionFsConventions::Posix)
416+
SessionFsConfig::new("/", session_state_path(), SessionFsConventions::Posix)
417+
}
418+
419+
fn session_state_path() -> String {
420+
if cfg!(windows) {
421+
"/session-state".to_string()
422+
} else {
423+
std::env::temp_dir()
424+
.join("copilot-rust-sessionfs-state")
425+
.join("session-state")
426+
.to_string_lossy()
427+
.replace('\\', "/")
428+
}
429+
}
430+
431+
fn provider_relative_path(path: &str) -> PathBuf {
432+
PathBuf::from(path.trim_start_matches(['/', '\\']))
417433
}
418434

419435
async fn wait_for_file_containing(path: &Path, needle: &str) {

rust/tests/e2e/support.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ pub fn assert_uuid_like(session_id: &SessionId) {
424424
assert_eq!(text.len(), 36, "session id should be UUID-shaped");
425425
assert!(
426426
text.chars().all(|ch| ch.is_ascii_hexdigit() || ch == '-'),
427-
"session id should be UUID-shaped: {text}"
427+
"session id should be UUID-shaped"
428428
);
429429
}
430430

rust/tests/e2e/suspend.rs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,3 @@ async fn should_allow_resume_and_continue_conversation_after_suspend() {
8686
)
8787
.await;
8888
}
89-
90-
#[tokio::test]
91-
async fn should_cancel_pending_permission_request_when_suspending() {
92-
let config =
93-
ResumeSessionConfig::new("suspend-permission".into()).with_continue_pending_work(false);
94-
95-
assert_eq!(config.continue_pending_work, Some(false));
96-
}
97-
98-
#[tokio::test]
99-
async fn should_reject_pending_external_tool_when_suspending() {
100-
let config =
101-
ResumeSessionConfig::new("suspend-external-tool".into()).with_continue_pending_work(false);
102-
103-
assert_eq!(config.continue_pending_work, Some(false));
104-
}

0 commit comments

Comments
 (0)