github-copilot-sdk 1.0.0-beta.8

Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC. Technical preview, pre-1.0.
use std::path::Path;
use std::process::Command;

use github_copilot_sdk::generated::api_types::{
    WorkspaceDiffFileChangeType, WorkspaceDiffMode, WorkspacesDiffRequest,
    WorkspacesReadCheckpointRequest, WorkspacesReadFileRequest, WorkspacesSaveLargePasteRequest,
};

use super::support::with_e2e_context;

#[tokio::test]
async fn should_list_no_checkpoints_for_fresh_session() {
    with_e2e_context(
        "rpc_workspace_checkpoints",
        "should_list_no_checkpoints_for_fresh_session",
        |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 checkpoints = session
                    .rpc()
                    .workspaces()
                    .list_checkpoints()
                    .await
                    .expect("list checkpoints");
                assert!(checkpoints.checkpoints.is_empty());

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

#[tokio::test]
async fn should_return_null_or_empty_content_for_unknown_checkpoint() {
    with_e2e_context(
        "rpc_workspace_checkpoints",
        "should_return_null_or_empty_content_for_unknown_checkpoint",
        |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 checkpoint = session
                    .rpc()
                    .workspaces()
                    .read_checkpoint(WorkspacesReadCheckpointRequest { number: i64::MAX })
                    .await
                    .expect("read missing checkpoint");
                assert!(checkpoint.content.as_deref().unwrap_or_default().is_empty());

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

#[tokio::test]
async fn should_return_typed_workspace_diff_result() {
    with_e2e_context(
        "rpc_workspace_checkpoints",
        "should_return_typed_workspace_diff_result",
        |ctx| {
            Box::pin(async move {
                ctx.set_default_copilot_user();
                init_git_repository(ctx.work_dir());
                let changed_path = ctx.work_dir().join("rust-workspace-diff.txt");
                std::fs::write(&changed_path, "diff content\n").expect("write diff file");
                let client = ctx.start_client().await;
                let session = client
                    .create_session(ctx.approve_all_session_config())
                    .await
                    .expect("create session");

                let diff = session
                    .rpc()
                    .workspaces()
                    .diff(WorkspacesDiffRequest {
                        mode: WorkspaceDiffMode::Unstaged,
                    })
                    .await
                    .expect("workspace diff");
                assert_eq!(diff.requested_mode, WorkspaceDiffMode::Unstaged);
                assert!(matches!(
                    diff.mode,
                    WorkspaceDiffMode::Unstaged | WorkspaceDiffMode::Branch
                ));
                if let Some(change) = diff.changes.iter().find(|change| {
                    normalize_path(&change.path).ends_with("rust-workspace-diff.txt")
                }) {
                    assert_eq!(change.change_type, WorkspaceDiffFileChangeType::Added);
                    assert!(change.diff.contains("diff content") || change.diff.is_empty());
                } else {
                    assert!(
                        diff.changes.is_empty(),
                        "unexpected diff changes: {:?}",
                        diff.changes
                    );
                }

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

#[tokio::test]
async fn should_save_large_paste_and_expose_readable_content() {
    with_e2e_context(
        "rpc_workspace_checkpoints",
        "should_save_large_paste_and_expose_readable_content",
        |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 content = "large paste rust content\n".repeat(512);

                let saved = session
                    .rpc()
                    .workspaces()
                    .save_large_paste(WorkspacesSaveLargePasteRequest {
                        content: content.clone(),
                    })
                    .await
                    .expect("save large paste")
                    .saved
                    .expect("saved paste descriptor");
                assert!(saved.filename.ends_with(".txt"));
                assert_eq!(saved.size_bytes, content.len() as i64);
                assert_eq!(
                    std::fs::read_to_string(&saved.file_path).expect("read saved paste"),
                    content
                );
                let read = session
                    .rpc()
                    .workspaces()
                    .read_file(WorkspacesReadFileRequest {
                        path: saved.filename,
                    })
                    .await
                    .expect("read saved paste through workspace");
                assert_eq!(read.content, content);

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

fn normalize_path(path: &str) -> String {
    path.replace('\\', "/")
}

fn init_git_repository(path: &Path) {
    let status = Command::new("git")
        .arg("init")
        .arg("--quiet")
        .current_dir(path)
        .status()
        .expect("run git init");
    assert!(status.success(), "git init should succeed");
}