use std::path::PathBuf;
use github_copilot_sdk::{
CliProgram, Client, ClientOptions, ErrorKind, HAS_BUNDLED_CLI, install_bundled_cli,
};
use serial_test::serial;
fn unset_env(key: &str) {
unsafe { std::env::remove_var(key) };
}
fn set_env(key: &str, value: &str) {
unsafe { std::env::set_var(key, value) };
}
#[tokio::test(flavor = "current_thread")]
#[serial(copilot_cli_path)]
async fn env_override_resolves_to_pointed_file() {
let tmp = tempfile::NamedTempFile::new().expect("create tempfile");
let path = tmp.path().to_path_buf();
set_env(
"COPILOT_CLI_PATH",
path.to_str().expect("utf-8 tempfile path"),
);
let opts = ClientOptions::default().with_program(CliProgram::Resolve);
let result = Client::start(opts).await;
unset_env("COPILOT_CLI_PATH");
match result {
Ok(_) => {}
Err(e) => {
let msg = format!("{e}");
assert!(
!msg.contains("not found"),
"expected COPILOT_CLI_PATH to win; got {msg}"
);
}
}
drop(tmp);
let _ = path;
}
#[tokio::test(flavor = "current_thread")]
#[serial(copilot_cli_path)]
async fn stale_env_override_falls_through() {
set_env("COPILOT_CLI_PATH", "/definitely/does/not/exist/copilot");
let opts = ClientOptions::default().with_program(CliProgram::Resolve);
let result = Client::start(opts).await;
unset_env("COPILOT_CLI_PATH");
if let Err(e) = &result {
assert!(
!matches!(e.kind(), ErrorKind::BinaryNotFound { .. }),
"stale COPILOT_CLI_PATH should fall through; got BinaryNotFound: {e}"
);
}
}
#[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))]
#[test]
fn extracted_binary_present_at_conventional_path() {
let version = env!("COPILOT_SDK_CLI_VERSION");
let binary = if cfg!(windows) {
"copilot.exe"
} else {
"copilot"
};
let sanitized = sanitize_version_for_test(version);
let path = dirs::cache_dir()
.expect("platform cache dir")
.join("github-copilot-sdk")
.join("cli")
.join(sanitized)
.join(binary);
assert!(
path.is_file(),
"expected build.rs to extract the CLI to {} (`bundled-cli` off)",
path.display()
);
}
#[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))]
fn sanitize_version_for_test(version: &str) -> String {
version
.chars()
.map(|c| match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' => c,
_ => '_',
})
.collect()
}
#[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))]
#[tokio::test(flavor = "current_thread")]
#[serial(copilot_cli_path)]
async fn unbundled_resolver_finds_extracted_binary() {
unset_env("COPILOT_CLI_PATH");
unset_env("COPILOT_CLI_EXTRACT_DIR");
let opts = ClientOptions::default().with_program(CliProgram::Resolve);
let result = Client::start(opts).await;
if let Err(e) = result {
assert!(
!matches!(e.kind(), ErrorKind::BinaryNotFound { .. }),
"resolver returned BinaryNotFound with `bundled-cli` off: {e}"
);
}
}
#[cfg(all(not(feature = "bundled-cli"), has_extracted_cli))]
#[tokio::test(flavor = "current_thread")]
#[serial(copilot_cli_path)]
async fn extract_dir_runtime_override_is_honored() {
let tmp = tempfile::tempdir().expect("create tempdir");
let binary = if cfg!(windows) {
"copilot.exe"
} else {
"copilot"
};
let fake = tmp.path().join(binary);
std::fs::write(&fake, b"").expect("write fake binary");
unset_env("COPILOT_CLI_PATH");
set_env(
"COPILOT_CLI_EXTRACT_DIR",
tmp.path().to_str().expect("utf-8 tempdir path"),
);
let opts = ClientOptions::default().with_program(CliProgram::Resolve);
let result = Client::start(opts).await;
unset_env("COPILOT_CLI_EXTRACT_DIR");
if let Err(e) = result {
assert!(
!matches!(e.kind(), ErrorKind::BinaryNotFound { .. }),
"EXTRACT_DIR-redirected resolver returned BinaryNotFound: {e}"
);
}
drop(tmp);
let _ = fake;
}
#[test]
fn pin_file_when_present_is_well_formed() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let pin = PathBuf::from(manifest_dir).join("cli-version.txt");
if !pin.is_file() {
return;
}
let contents = std::fs::read_to_string(&pin).expect("read cli-version.txt");
let mut saw_version = false;
for raw in contents.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (key, value) = line
.split_once('=')
.unwrap_or_else(|| panic!("malformed line: {raw:?}"));
assert!(!value.trim().is_empty(), "empty value for key {key:?}");
if key.trim() == "version" {
saw_version = true;
}
}
assert!(saw_version, "cli-version.txt missing `version=` line");
}
#[cfg(all(feature = "bundled-cli", has_bundled_cli))]
#[test]
fn install_bundled_cli_returns_extracted_path() {
const { assert!(HAS_BUNDLED_CLI) };
let first = install_bundled_cli().expect("bundled CLI should install");
assert!(
first.is_file(),
"install_bundled_cli returned a path that is not a file: {}",
first.display()
);
let second = install_bundled_cli().expect("second call should also succeed");
assert_eq!(
first, second,
"install_bundled_cli must be idempotent across calls"
);
}
#[cfg(all(feature = "bundled-cli", has_bundled_cli))]
#[tokio::test(flavor = "current_thread")]
#[serial(copilot_cli_path)]
async fn install_bundled_cli_matches_resolver() {
unset_env("COPILOT_CLI_PATH");
unset_env("COPILOT_CLI_EXTRACT_DIR");
let direct = install_bundled_cli().expect("bundled CLI should install");
assert!(direct.is_file());
let opts = ClientOptions::default().with_program(CliProgram::Resolve);
if let Err(e) = Client::start(opts).await {
assert!(
!matches!(e.kind(), ErrorKind::BinaryNotFound { .. }),
"resolver returned BinaryNotFound while install_bundled_cli succeeded: {e}"
);
}
}
#[cfg(not(all(feature = "bundled-cli", has_bundled_cli)))]
#[test]
fn install_bundled_cli_is_none_without_embed() {
const { assert!(!HAS_BUNDLED_CLI) };
assert!(
install_bundled_cli().is_none(),
"install_bundled_cli must not fall back to the dev-cache path"
);
}