github-copilot-sdk 1.0.1

Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC.
use github_copilot_sdk::Client;
use github_copilot_sdk::rpc::{
    AgentRegistrySpawnRequest, SendAttachmentsToMessageParams, SessionsOpenStatus,
};

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

#[tokio::test]
async fn should_reload_user_settings() {
    with_e2e_context("rpc_server_misc", "should_reload_user_settings", |ctx| {
        Box::pin(async move {
            let client = ctx.start_client().await;

            client
                .rpc()
                .user()
                .settings()
                .reload()
                .await
                .expect("reload user settings");

            client.stop().await.expect("stop client");
        })
    })
    .await;
}

#[tokio::test]
async fn should_report_agent_registry_spawn_gate_closed() {
    with_e2e_context(
        "rpc_server_misc",
        "should_report_agent_registry_spawn_gate_closed",
        |ctx| {
            Box::pin(async move {
                let client = ctx.start_client().await;

                let err = client
                    .rpc()
                    .agent_registry()
                    .spawn(AgentRegistrySpawnRequest {
                        agent_name: None,
                        cwd: ctx.work_dir().to_string_lossy().to_string(),
                        initial_prompt: None,
                        model: None,
                        name: None,
                        permission_mode: None,
                    })
                    .await
                    .expect_err("agent registry spawn should be gated");

                let message = err.to_string();
                assert_not_unhandled(&message);
                let lower = message.to_ascii_lowercase();
                assert!(lower.contains("agentregistry.spawn"), "{message}");
                assert!(
                    lower.contains("not enabled") || lower.contains("no delegate"),
                    "{message}"
                );

                client.stop().await.expect("stop client");
            })
        },
    )
    .await;
}

#[tokio::test]
async fn should_shut_down_owned_runtime() {
    with_e2e_context("rpc_server_misc", "should_shut_down_owned_runtime", |ctx| {
        Box::pin(async move {
            let client = Client::start(ctx.client_options())
                .await
                .expect("start dedicated client");

            client
                .rpc()
                .user()
                .settings()
                .reload()
                .await
                .expect("runtime should start live");

            client
                .rpc()
                .runtime()
                .shutdown()
                .await
                .expect("shut down runtime");

            wait_for_condition("runtime to stop serving RPCs", || async {
                client.rpc().user().settings().reload().await.is_err()
            })
            .await;

            let _ = client.stop().await;
        })
    })
    .await;
}

#[tokio::test]
async fn should_report_not_found_when_opening_session_without_context() {
    with_e2e_context(
        "rpc_server_misc",
        "should_report_not_found_when_opening_session_without_context",
        |ctx| {
            Box::pin(async move {
                let client = ctx.start_client().await;

                let result = client
                    .rpc()
                    .sessions()
                    .open()
                    .await
                    .expect("open session without context");

                assert_eq!(result.status, SessionsOpenStatus::NotFound);
                assert!(result.session_id.is_none());

                client.stop().await.expect("stop client");
            })
        },
    )
    .await;
}

#[tokio::test]
async fn should_reject_send_attachments_from_non_extension_connection() {
    with_e2e_context(
        "rpc_server_misc",
        "should_reject_send_attachments_from_non_extension_connection",
        |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 err = session
                    .rpc()
                    .extensions()
                    .send_attachments_to_message(SendAttachmentsToMessageParams {
                        attachments: Vec::new(),
                        instance_id: None,
                    })
                    .await
                    .expect_err("normal session connection should be rejected");
                let message = err.to_string();
                assert_not_unhandled(&message);
                assert!(
                    message.to_ascii_lowercase().contains("extension"),
                    "{message}"
                );

                session.disconnect().await.expect("disconnect session");
                client.stop().await.expect("stop client");
            })
        },
    )
    .await;
}

fn assert_not_unhandled(message: &str) {
    assert!(
        !message.to_ascii_lowercase().contains("unhandled method"),
        "{message}"
    );
}