cc-switch-tui 0.2.2

All-in-One Assistant for Claude Code, Codex, Gemini, OpenCode, OpenClaw & Hermes
Documentation
#![cfg(not(windows))]

use serial_test::serial;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use tempfile::TempDir;

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("repo root should exist")
        .to_path_buf()
}

fn install_script_path() -> PathBuf {
    repo_root().join("install.sh")
}

fn write_executable(path: &Path, contents: &str) {
    fs::write(path, contents).expect("script should be written");
    let mut perms = fs::metadata(path)
        .expect("metadata should exist")
        .permissions();
    perms.set_mode(0o755);
    fs::set_permissions(path, perms).expect("permissions should be updated");
}

struct Harness {
    _temp: TempDir,
    home: PathBuf,
    fakebin: PathBuf,
    install_dir: PathBuf,
    logs_dir: PathBuf,
    archive_path: PathBuf,
}

impl Harness {
    fn new() -> Self {
        let temp = tempfile::tempdir().expect("temp dir should exist");
        let home = temp.path().join("home");
        let fakebin = temp.path().join("fakebin");
        let install_dir = temp.path().join("install");
        let logs_dir = temp.path().join("logs");
        let payload_dir = temp.path().join("payload");
        let archive_path = temp.path().join("cc-switch.tar.gz");

        fs::create_dir_all(&home).expect("home should exist");
        fs::create_dir_all(&fakebin).expect("fakebin should exist");
        fs::create_dir_all(&install_dir).expect("install dir should exist");
        fs::create_dir_all(&logs_dir).expect("logs dir should exist");
        fs::create_dir_all(&payload_dir).expect("payload dir should exist");

        write_executable(
            &payload_dir.join("cc-switch-tui"),
            "#!/usr/bin/env bash\necho new build\n",
        );

        let status = Command::new("tar")
            .arg("-czf")
            .arg(&archive_path)
            .arg("-C")
            .arg(&payload_dir)
            .arg("cc-switch-tui")
            .status()
            .expect("tar should run");
        assert!(status.success(), "tar should create archive");

        write_executable(
            &fakebin.join("uname"),
            r#"#!/usr/bin/env bash
set -eu
case "${1:-}" in
  -s) printf 'Linux\n' ;;
  -m) printf 'x86_64\n' ;;
  *) /usr/bin/uname "$@" ;;
esac
"#,
        );

        write_executable(
            &fakebin.join("curl"),
            r#"#!/usr/bin/env bash
set -eu
output=''
url=''
while [ "$#" -gt 0 ]; do
  case "$1" in
    --output)
      output="$2"
      shift 2
      ;;
    --fail|--location|--silent|--show-error)
      shift
      ;;
    *)
      url="$1"
      shift
      ;;
  esac
done

printf '%s' "$url" > "${CC_SWITCH_TEST_LOG_DIR}/last-url"
if [ "${url##*/}" = "latest.json" ]; then
  printf '{\n  "version": "v0.0.0"\n}\n' > "$output"
  exit 0
fi
if [ "${CC_SWITCH_TEST_FAIL_MUSL:-0}" = "1" ] && [[ "${url##*/}" == *linux-x64-musl.tar.gz ]]; then
  exit 22
fi
cp "${CC_SWITCH_TEST_ARCHIVE_PATH}" "$output"
"#,
        );

        Self {
            _temp: temp,
            home,
            fakebin,
            install_dir,
            logs_dir,
            archive_path,
        }
    }

    fn run(&self, extra_envs: &[(&str, &str)], extra_path: Option<&Path>) -> Output {
        let mut path_parts = vec![self.fakebin.display().to_string()];
        if let Some(extra) = extra_path {
            path_parts.push(extra.display().to_string());
        }
        path_parts.push(std::env::var("PATH").unwrap_or_default());

        let mut command = Command::new("bash");
        command
            .arg(install_script_path())
            .env("HOME", &self.home)
            .env("CC_SWITCH_INSTALL_DIR", &self.install_dir)
            .env("CC_SWITCH_TEST_ARCHIVE_PATH", &self.archive_path)
            .env("CC_SWITCH_TEST_LOG_DIR", &self.logs_dir)
            .env("PATH", path_parts.join(":"));

        for (key, value) in extra_envs {
            command.env(key, value);
        }

        command.output().expect("install script should run")
    }
}

#[test]
#[serial]
fn install_script_requires_force_for_non_tty_overwrite() {
    let harness = Harness::new();
    write_executable(
        &harness.install_dir.join("cc-switch-tui"),
        "#!/usr/bin/env bash\necho old build\n",
    );

    let output = harness.run(&[], None);
    assert!(
        !output.status.success(),
        "overwrite should fail without force"
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("CC_SWITCH_FORCE=1"), "stderr was: {stderr}");
}

#[test]
#[serial]
fn install_script_force_overwrites_and_warns_about_shadowed_path() {
    let harness = Harness::new();
    let shadow_dir = harness.home.join("shadow-bin");
    fs::create_dir_all(&shadow_dir).expect("shadow dir should exist");
    write_executable(
        &shadow_dir.join("cc-switch-tui"),
        "#!/usr/bin/env bash\necho shadow build\n",
    );
    write_executable(
        &harness.install_dir.join("cc-switch-tui"),
        "#!/usr/bin/env bash\necho old build\n",
    );

    let output = harness.run(&[("CC_SWITCH_FORCE", "1")], Some(&shadow_dir));
    assert!(
        output.status.success(),
        "force overwrite should succeed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("shadow"), "stderr was: {stderr}");

    let installed = fs::read_to_string(harness.install_dir.join("cc-switch-tui"))
        .expect("installed file should exist");
    assert!(installed.contains("new build"));
}

#[test]
#[serial]
fn install_script_supports_linux_glibc_override() {
    let harness = Harness::new();

    let output = harness.run(&[("CC_SWITCH_LINUX_LIBC", "glibc")], None);
    assert!(
        output.status.success(),
        "glibc override install should succeed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    let requested_url = fs::read_to_string(harness.logs_dir.join("last-url"))
        .expect("download url should be logged");
    assert!(
        requested_url.ends_with("/cc-switch-tui-v0.0.0-linux-x64.tar.gz"),
        "expected glibc asset request, got {requested_url}"
    );
}

#[test]
#[serial]
fn install_script_falls_back_to_glibc_when_musl_download_fails() {
    let harness = Harness::new();

    let output = harness.run(&[("CC_SWITCH_TEST_FAIL_MUSL", "1")], None);
    assert!(
        output.status.success(),
        "glibc fallback install should succeed\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    let requested_url = fs::read_to_string(harness.logs_dir.join("last-url"))
        .expect("download url should be logged");
    assert!(
        requested_url.ends_with("/cc-switch-tui-v0.0.0-linux-x64.tar.gz"),
        "expected fallback glibc asset request, got {requested_url}"
    );
}