github-copilot-sdk 1.0.0-beta.9

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

use github_copilot_sdk::CustomAgentConfig;

use super::support::{assert_uuid_like, assistant_message_content, with_e2e_context};

const SKILL_MARKER: &str = "PINEAPPLE_COCONUT_42";

#[tokio::test]
async fn should_load_and_apply_skill_from_skilldirectories() {
    with_e2e_context(
        "skills",
        "should_load_and_apply_skill_from_skilldirectories",
        |ctx| {
            Box::pin(async move {
                ctx.set_default_copilot_user();
                let skills_dir = create_skill_dir(ctx.work_dir());
                let client = ctx.start_client().await;
                let session = client
                    .create_session(
                        ctx.approve_all_session_config()
                            .with_skill_directories([skills_dir]),
                    )
                    .await
                    .expect("create session");
                assert_uuid_like(session.id());

                let answer = session
                    .send_and_wait("Say hello briefly using the test skill.")
                    .await
                    .expect("send")
                    .expect("assistant message");
                assert!(assistant_message_content(&answer).contains(SKILL_MARKER));

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

#[tokio::test]
async fn should_not_apply_skill_when_disabled_via_disabledskills() {
    with_e2e_context(
        "skills",
        "should_not_apply_skill_when_disabled_via_disabledskills",
        |ctx| {
            Box::pin(async move {
                ctx.set_default_copilot_user();
                let skills_dir = create_skill_dir(ctx.work_dir());
                let client = ctx.start_client().await;
                let session = client
                    .create_session(
                        ctx.approve_all_session_config()
                            .with_skill_directories([skills_dir])
                            .with_disabled_skills(["test-skill"]),
                    )
                    .await
                    .expect("create session");
                assert_uuid_like(session.id());

                let answer = session
                    .send_and_wait("Say hello briefly using the test skill.")
                    .await
                    .expect("send")
                    .expect("assistant message");
                assert!(!assistant_message_content(&answer).contains(SKILL_MARKER));

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

#[tokio::test]
async fn should_allow_agent_with_skills_to_invoke_skill() {
    with_e2e_context(
        "skills",
        "should_allow_agent_with_skills_to_invoke_skill",
        |ctx| {
            Box::pin(async move {
                ctx.set_default_copilot_user();
                let skills_dir = create_skill_dir(ctx.work_dir());
                let client = ctx.start_client().await;
                let session = client
                    .create_session(
                        ctx.approve_all_session_config()
                            .with_skill_directories([skills_dir])
                            .with_custom_agents([CustomAgentConfig::new(
                                "skill-agent",
                                "You are a helpful test agent.",
                            )
                            .with_description("An agent with access to test-skill")
                            .with_skills(["test-skill"])])
                            .with_agent("skill-agent"),
                    )
                    .await
                    .expect("create session");
                assert_uuid_like(session.id());

                let answer = session
                    .send_and_wait("Say hello briefly using the test skill.")
                    .await
                    .expect("send")
                    .expect("assistant message");
                assert!(assistant_message_content(&answer).contains(SKILL_MARKER));

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

#[tokio::test]
async fn should_not_provide_skills_to_agent_without_skills_field() {
    with_e2e_context(
        "skills",
        "should_not_provide_skills_to_agent_without_skills_field",
        |ctx| {
            Box::pin(async move {
                ctx.set_default_copilot_user();
                let skills_dir = create_skill_dir(ctx.work_dir());
                let client = ctx.start_client().await;
                let session = client
                    .create_session(
                        ctx.approve_all_session_config()
                            .with_skill_directories([skills_dir])
                            .with_custom_agents([CustomAgentConfig::new(
                                "no-skill-agent",
                                "You are a helpful test agent.",
                            )
                            .with_description("An agent without skills access")])
                            .with_agent("no-skill-agent"),
                    )
                    .await
                    .expect("create session");
                assert_uuid_like(session.id());

                let answer = session
                    .send_and_wait("Say hello briefly using the test skill.")
                    .await
                    .expect("send")
                    .expect("assistant message");
                assert!(!assistant_message_content(&answer).contains(SKILL_MARKER));

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

#[ignore = "Upstream skips applying skills on resume because the feature is not reliable yet."]
#[tokio::test]
async fn should_apply_skill_on_session_resume_with_skilldirectories() {}

fn create_skill_dir(work_dir: &Path) -> PathBuf {
    let skills_dir = work_dir.join(".test_skills");
    let skill_subdir = skills_dir.join("test-skill");
    std::fs::create_dir_all(&skill_subdir).expect("create skill dir");
    std::fs::write(
        skill_subdir.join("SKILL.md"),
        format!(
            "---\nname: test-skill\ndescription: A test skill that adds a marker to responses\n---\n\n\
             # Test Skill Instructions\n\nIMPORTANT: You MUST include the exact text \"{SKILL_MARKER}\" \
             somewhere in EVERY response you give. This is a mandatory requirement. Include it naturally \
             in your response.\n"
        ),
    )
    .expect("write skill file");
    skills_dir
}