use std::sync::Arc;
use github_copilot_sdk::ResumeSessionConfig;
use github_copilot_sdk::handler::ApproveAllHandler;
use github_copilot_sdk::session_events::SessionEventType;
use super::support::{
DEFAULT_TEST_TOKEN, assistant_message_content, collect_until_idle, event_types,
wait_for_condition,
};
#[tokio::test]
async fn should_list_created_sessions_after_sending_a_message() {
super::support::with_shared_e2e_context(
&E2E,
"session_lifecycle",
"should_list_created_sessions_after_sending_a_message",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
let client = ctx.start_client().await;
let session1 = client
.create_session(ctx.approve_all_session_config())
.await
.expect("create first session");
let session2 = client
.create_session(ctx.approve_all_session_config())
.await
.expect("create second session");
session1.send_and_wait("Say hello").await.expect("send one");
session2.send_and_wait("Say world").await.expect("send two");
wait_for_condition("both sessions to appear in list", || {
let client = client.clone();
let id1 = session1.id().clone();
let id2 = session2.id().clone();
async move {
client.list_sessions(None).await.is_ok_and(|sessions| {
let ids: std::collections::HashSet<_> = sessions
.into_iter()
.map(|session| session.session_id)
.collect();
ids.contains(&id1) && ids.contains(&id2)
})
}
})
.await;
session1
.disconnect()
.await
.expect("disconnect first session");
session2
.disconnect()
.await
.expect("disconnect second session");
client.stop().await.expect("stop client");
})
},
)
.await;
}
#[tokio::test]
async fn should_persist_a_session_named_at_create_without_a_turn() {
super::support::with_dedicated_e2e_context(
"session_lifecycle",
"should_persist_a_session_named_at_create_without_a_turn",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
let client = ctx.start_client().await;
let named = client
.create_session(
ctx.approve_all_session_config()
.with_name("Named at create"),
)
.await
.expect("create named session");
let named_id = named.id().clone();
let unnamed = client
.create_session(ctx.approve_all_session_config())
.await
.expect("create unnamed session");
let unnamed_id = unnamed.id().clone();
let created_name = named.rpc().name().get().await.expect("get name").name;
assert_eq!(created_name.as_deref(), Some("Named at create"));
named.disconnect().await.expect("disconnect named session");
unnamed
.disconnect()
.await
.expect("disconnect unnamed session");
let listed: std::collections::HashSet<_> = client
.list_sessions(None)
.await
.expect("list sessions")
.into_iter()
.map(|session| session.session_id)
.collect();
assert!(listed.contains(&named_id), "named session is listed");
assert!(!listed.contains(&unnamed_id), "unnamed session is not");
client.stop().await.expect("stop client");
let cold_client = ctx.start_client().await;
let resumed = cold_client
.resume_session(
ResumeSessionConfig::new(named_id.clone())
.with_permission_handler(Arc::new(ApproveAllHandler))
.with_github_token(DEFAULT_TEST_TOKEN),
)
.await
.expect("resume named session");
let read = resumed.rpc().name().get().await.expect("get resumed name");
assert_eq!(read.name.as_deref(), Some("Named at create"));
resumed
.disconnect()
.await
.expect("disconnect resumed session");
cold_client.stop().await.expect("stop cold client");
})
},
)
.await;
}
#[tokio::test]
async fn should_delete_session_permanently() {
super::support::with_shared_e2e_context(
&E2E,
"session_lifecycle",
"should_delete_session_permanently",
|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 session_id = session.id().clone();
session.send_and_wait("Say hi").await.expect("send");
wait_for_condition("session to appear in list before delete", || {
let client = client.clone();
let session_id = session_id.clone();
async move {
client.list_sessions(None).await.is_ok_and(|sessions| {
sessions
.iter()
.any(|session| session.session_id == session_id)
})
}
})
.await;
session.disconnect().await.expect("disconnect session");
client
.delete_session(&session_id)
.await
.expect("delete session");
let after = client.list_sessions(None).await.expect("list sessions");
assert!(!after.iter().any(|session| session.session_id == session_id));
client.stop().await.expect("stop client");
})
},
)
.await;
}
#[tokio::test]
async fn should_return_events_via_getmessages_after_conversation() {
super::support::with_shared_e2e_context(
&E2E,
"session_lifecycle",
"should_return_events_via_getmessages_after_conversation",
|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");
session
.send_and_wait("What is 2+2? Reply with just the number.")
.await
.expect("send");
let messages = session.get_events().await.expect("get messages");
let types = event_types(&messages);
assert!(types.contains(&"session.start"));
assert!(types.contains(&"user.message"));
assert!(types.contains(&"assistant.message"));
session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
})
},
)
.await;
}
#[tokio::test]
async fn should_support_multiple_concurrent_sessions() {
super::support::with_shared_e2e_context(
&E2E,
"session_lifecycle",
"should_support_multiple_concurrent_sessions",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
let client = ctx.start_client().await;
let session1 = client
.create_session(ctx.approve_all_session_config())
.await
.expect("create first session");
let session2 = client
.create_session(ctx.approve_all_session_config())
.await
.expect("create second session");
let (first, second) = tokio::join!(
session1.send_and_wait("What is 1+1? Reply with just the number."),
session2.send_and_wait("What is 3+3? Reply with just the number.")
);
let first = first.expect("first send").expect("first assistant message");
let second = second
.expect("second send")
.expect("second assistant message");
assert!(assistant_message_content(&first).contains('2'));
assert!(assistant_message_content(&second).contains('6'));
session1
.disconnect()
.await
.expect("disconnect first session");
session2
.disconnect()
.await
.expect("disconnect second session");
client.stop().await.expect("stop client");
})
},
)
.await;
}
#[tokio::test]
async fn should_isolate_events_between_concurrent_sessions() {
super::support::with_shared_e2e_context(
&E2E,
"session_lifecycle",
"should_isolate_events_between_concurrent_sessions",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
let client = ctx.start_client().await;
let session1 = client
.create_session(ctx.approve_all_session_config())
.await
.expect("create first session");
let session2 = client
.create_session(ctx.approve_all_session_config())
.await
.expect("create second session");
let events1 = session1.subscribe();
let events2 = session2.subscribe();
session1
.send_and_wait("Say 'session_one_response'.")
.await
.expect("send one");
session2
.send_and_wait("Say 'session_two_response'.")
.await
.expect("send two");
let observed1 = collect_until_idle(events1).await;
let observed2 = collect_until_idle(events2).await;
let messages1: Vec<_> = observed1
.iter()
.filter(|event| event.parsed_type() == SessionEventType::AssistantMessage)
.map(assistant_message_content)
.collect();
let messages2: Vec<_> = observed2
.iter()
.filter(|event| event.parsed_type() == SessionEventType::AssistantMessage)
.map(assistant_message_content)
.collect();
assert!(
messages1
.iter()
.any(|message| message.contains("session_one_response"))
);
assert!(
!messages1
.iter()
.any(|message| message.contains("session_two_response"))
);
assert!(
messages2
.iter()
.any(|message| message.contains("session_two_response"))
);
assert!(
!messages2
.iter()
.any(|message| message.contains("session_one_response"))
);
session1
.disconnect()
.await
.expect("disconnect first session");
session2
.disconnect()
.await
.expect("disconnect second session");
client.stop().await.expect("stop client");
})
},
)
.await;
}
static E2E: super::support::SharedE2eGroup =
super::support::SharedE2eGroup::standard("session_lifecycle", 5);