github-copilot-sdk 1.0.0-beta.4

Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC. Technical preview, pre-1.0.
Documentation
use github_copilot_sdk::generated::session_events::SessionEventType;

use super::support::{
    assistant_message_content, collect_until_idle, event_types, wait_for_condition,
    with_e2e_context,
};

#[tokio::test]
async fn should_list_created_sessions_after_sending_a_message() {
    with_e2e_context(
        "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_delete_session_permanently() {
    with_e2e_context(
        "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() {
    with_e2e_context(
        "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_messages().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() {
    with_e2e_context(
        "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() {
    with_e2e_context(
        "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;
}