github-copilot-sdk 1.0.29-preview.9

Rust SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC.
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *--------------------------------------------------------------------------------------------*/

#![cfg(test)]

use std::sync::Mutex;
use std::sync::atomic::AtomicUsize;
use std::time::{Duration, Instant};

use super::*;

static FFI_LIFECYCLE_TEST_LOCK: Mutex<()> = Mutex::new(());
static TEST_ALLOW_CLOSE: AtomicBool = AtomicBool::new(false);
static TEST_CLOSE_CALLS: AtomicUsize = AtomicUsize::new(0);
static TEST_SHUTDOWN_CALLS: AtomicUsize = AtomicUsize::new(0);

unsafe extern "C" fn test_host_shutdown(_server_id: u32) -> bool {
    TEST_SHUTDOWN_CALLS.fetch_add(1, Ordering::SeqCst);
    true
}

unsafe extern "C" fn test_connection_write(
    _connection_id: u32,
    _bytes: *const u8,
    _length: usize,
) -> bool {
    true
}

unsafe extern "C" fn test_connection_close(_connection_id: u32) -> bool {
    TEST_CLOSE_CALLS.fetch_add(1, Ordering::SeqCst);
    TEST_ALLOW_CLOSE.load(Ordering::SeqCst)
}

unsafe extern "C" fn test_set_ui_thread_handler(connection_id: u32, enabled: bool) -> bool {
    connection_id == 21 && enabled
}

unsafe extern "C" fn test_run_ui_action(connection_id: u32, action_id: u32) -> bool {
    connection_id == 21 && action_id == 42
}

#[test]
fn argv_without_entrypoint_contains_only_client_options() {
    let argv: Vec<String> = serde_json::from_slice(&build_argv_json(
        None,
        None,
        &["--log-level".into(), "debug".into()],
    ))
    .unwrap();

    assert_eq!(argv, ["--log-level", "debug"]);
}

#[test]
fn explicit_javascript_entrypoint_uses_node() {
    let argv: Vec<String> = serde_json::from_slice(&build_argv_json(
        Some(Path::new("index.js")),
        Some("microsoft/vscode"),
        &[],
    ))
    .unwrap();

    assert_eq!(
        argv,
        [
            "node",
            "index.js",
            "--embedded-host",
            "--no-auto-update",
            "--cli-login-sync-integration-id=microsoft/vscode",
        ]
    );
}

#[cfg(windows)]
#[test]
fn child_process_path_removes_windows_verbatim_prefix() {
    assert_eq!(
        path_for_child_process(PathBuf::from(r"\\?\D:\a\copilot-sdk\index.js")),
        PathBuf::from(r"D:\a\copilot-sdk\index.js")
    );
    assert_eq!(
        path_for_child_process(PathBuf::from(r"\\?\UNC\server\share\copilot-sdk\index.js")),
        PathBuf::from(r"\\server\share\copilot-sdk\index.js")
    );
}

#[test]
fn environment_is_omitted_when_empty() {
    assert_eq!(build_env_json(&[]), None);
}

#[test]
fn environment_key_casing_preserves_later_overrides_on_windows() {
    let env: serde_json::Value = serde_json::from_slice(
        &build_env_json(&[
            ("COPILOT_HOME".into(), "original-home".into()),
            ("copilot_home".into(), "alias-home".into()),
            ("COPILOT_HOME".into(), "typed-home".into()),
            ("HTTP_PROXY".into(), "http://proxy".into()),
            ("http_proxy".into(), "http://proxy".into()),
        ])
        .unwrap(),
    )
    .unwrap();

    assert_eq!(env["COPILOT_HOME"], "typed-home");
    assert_eq!(env["HTTP_PROXY"], "http://proxy");
    if cfg!(windows) {
        assert!(env.get("copilot_home").is_none());
        assert!(env.get("http_proxy").is_none());
    } else {
        assert_eq!(env["copilot_home"], "alias-home");
        assert_eq!(env["http_proxy"], "http://proxy");
    }
}

#[test]
fn environment_serializes_worker_overrides() {
    let env: serde_json::Value = serde_json::from_slice(
        &build_env_json(&[
            ("COPILOT_HOME".into(), "state".into()),
            ("COPILOT_DISABLE_KEYTAR".into(), "1".into()),
        ])
        .unwrap(),
    )
    .unwrap();

    assert_eq!(env["COPILOT_HOME"], "state");
    assert_eq!(env["COPILOT_DISABLE_KEYTAR"], "1");
    for (name, value) in std::env::vars_os() {
        let (Ok(name), Ok(value)) = (name.into_string(), value.into_string()) else {
            continue;
        };
        let name = if cfg!(windows) {
            name.to_uppercase()
        } else {
            name
        };
        if matches!(name.as_str(), "COPILOT_HOME" | "COPILOT_DISABLE_KEYTAR") {
            continue;
        }
        assert!(
            env.get(&name) == Some(&serde_json::Value::String(value)),
            "startup environment must preserve inherited variable {name}"
        );
    }
}

#[cfg(any(unix, windows))]
#[test]
fn environment_preserves_non_unicode_explicit_settings() {
    const CHILD: &str = "COPILOT_FFI_ENVIRONMENT_TEST_KEY";
    if let Ok(key) = std::env::var(CHILD) {
        let environment: serde_json::Value = serde_json::from_slice(
            &build_env_json(&[("COPILOT_HOME".into(), "typed-home".into())]).unwrap(),
        )
        .unwrap();
        let normalized = if cfg!(windows) {
            key.to_uppercase()
        } else {
            key.clone()
        };
        assert_eq!(environment[&normalized], "\u{FFFD}");
        assert_eq!(environment["COPILOT_HOME"], "typed-home");
        assert!(environment.get("UNRELATED_NON_UNICODE").is_none());
        if !cfg!(windows) && key == "https_proxy" {
            assert_eq!(environment["HTTPS_PROXY"], "http://fallback.invalid:8080");
        }
        let overridden: serde_json::Value =
            serde_json::from_slice(&build_env_json(&[(key, "typed-setting".into())]).unwrap())
                .unwrap();
        assert_eq!(overridden[&normalized], "typed-setting");
        return;
    }

    #[cfg(unix)]
    let non_unicode = {
        use std::os::unix::ffi::OsStringExt;
        std::ffi::OsString::from_vec(vec![0xFF])
    };
    #[cfg(windows)]
    let non_unicode = {
        use std::os::windows::ffi::OsStringExt;
        std::ffi::OsString::from_wide(&[0xD800])
    };
    let current = std::thread::current();
    for key in [
        "http_proxy",
        "HTTP_PROXY",
        "https_proxy",
        "HTTPS_PROXY",
        "COPILOT_PROXY_KERBEROS_SPN",
        "TMPDIR",
    ] {
        let output = std::process::Command::new(
            std::env::current_exe().expect("resolve current test executable"),
        )
        .args(["--exact", current.name().expect("test name"), "--nocapture"])
        .env_clear()
        .env(CHILD, key)
        .env("HTTPS_PROXY", "http://fallback.invalid:8080")
        .env("UNRELATED_NON_UNICODE", &non_unicode)
        .env(key, &non_unicode)
        .output()
        .expect("run isolated environment snapshot");
        assert!(
            output.status.success() && String::from_utf8_lossy(&output.stdout).contains("1 passed"),
            "{key}: {}{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }
}

#[test]
fn callback_state_is_retained_until_connection_close_succeeds() {
    let _guard = FFI_LIFECYCLE_TEST_LOCK.lock().unwrap();
    TEST_ALLOW_CLOSE.store(false, Ordering::SeqCst);
    TEST_CLOSE_CALLS.store(0, Ordering::SeqCst);
    TEST_SHUTDOWN_CALLS.store(0, Ordering::SeqCst);

    let (tx, mut rx) = mpsc::unbounded_channel();
    let state_ptr = Box::into_raw(Box::new(CallbackState {
        tx,
        closing: AtomicBool::new(false),
    }));
    let shared = FfiShared {
        host_shutdown: test_host_shutdown,
        owns_host: true,
        connection_write: test_connection_write,
        connection_close: test_connection_close,
        connection_set_ui_thread_handler: test_set_ui_thread_handler,
        connection_run_ui_action: test_run_ui_action,
        server_id: AtomicU32::new(11),
        connection_id: AtomicU32::new(21),
        callback_state: AtomicPtr::new(state_ptr),
        closed: AtomicBool::new(false),
        operation_lock: parking_lot::Mutex::new(()),
        library_path: PathBuf::from("test-runtime"),
    };

    shared.close();

    assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 1);
    assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 0);
    assert_eq!(shared.connection_id.load(Ordering::SeqCst), 0);
    assert!(shared.callback_state.load(Ordering::SeqCst).is_null());

    assert!(matches!(
        rx.try_recv(),
        Err(mpsc::error::TryRecvError::Empty)
    ));

    TEST_ALLOW_CLOSE.store(true, Ordering::SeqCst);
    let deadline = Instant::now() + Duration::from_secs(5);
    while Instant::now() < deadline && TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst) == 0 {
        std::thread::sleep(Duration::from_millis(10));
    }

    assert!(TEST_CLOSE_CALLS.load(Ordering::SeqCst) >= 2);
    assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1);
    assert!(matches!(
        rx.try_recv(),
        Err(mpsc::error::TryRecvError::Disconnected)
    ));

    let close_calls_after_cleanup = TEST_CLOSE_CALLS.load(Ordering::SeqCst);
    shared.close();
    assert_eq!(
        TEST_CLOSE_CALLS.load(Ordering::SeqCst),
        close_calls_after_cleanup
    );
    assert_eq!(TEST_SHUTDOWN_CALLS.load(Ordering::SeqCst), 1);
}