supercode-cli 0.4.6

supercode — a lightweight, fully-customizable AI coding agent CLI in Rust. Any model via OpenRouter; natively continues Claude Code and Codex sessions.
//! CLI-level acceptance test for UX-20 (`update` checks the latest GitHub
//! release and tells the user if a newer version exists):
//!
//! - dev/01: `supercode update` reports whether a newer release exists
//!   (comparing to the latest tag) and prints the upgrade command for the
//!   detected install method.
//! - dev/02: on check failure (non-2xx / unreachable), it degrades
//!   gracefully — a clear "couldn't check" notice, the static upgrade
//!   commands still print, and the process exits zero.
//!
//! Spawns the real, built `supercode` binary (mirroring `quiet_cli.rs`'s
//! idiom) against a real local HTTP stub standing in for the GitHub
//! releases API — the CLI, HTTP client, version-compare, and message
//! formatting are all the genuine, unmodified release/debug binary, not a
//! reimplementation. `SUPERCODE_UPDATE_CHECK_URL` (read by `update_cmd` in
//! `main.rs`) redirects the check at the stub instead of the real network.

use std::io::{Read, Write};
use std::net::TcpListener;
use std::process::{Command, Output, Stdio};

fn bin() -> std::path::PathBuf {
    std::path::PathBuf::from(env!("CARGO_BIN_EXE_supercode"))
}

/// One-shot local HTTP server that drains a single request and replies with
/// a hand-framed HTTP response built from `status_line` (e.g. `"200 OK"`)
/// and `body`.
fn spawn_http_stub(
    status_line: &'static str,
    body: impl Into<String>,
) -> (std::net::SocketAddr, std::thread::JoinHandle<()>) {
    let body = body.into();
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind stub listener");
    let addr = listener.local_addr().unwrap();
    let handle = std::thread::spawn(move || {
        let (mut sock, _) = listener.accept().expect("accept one connection");
        let mut buf = [0u8; 4096];
        let _ = sock.read(&mut buf); // drain the request; contents unused.

        let resp = format!(
            "HTTP/1.1 {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
            status_line,
            body.len(),
            body
        );
        sock.write_all(resp.as_bytes())
            .expect("write stub response");
        sock.flush().ok();
    });
    (addr, handle)
}

fn run_update(check_url: &str) -> Output {
    let home = std::env::temp_dir().join(format!(
        "supercode-ux20-update-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ));
    std::fs::create_dir_all(&home).unwrap();

    Command::new(bin())
        .env("SUPERCODE_HOME", &home)
        .env("SUPERCODE_UPDATE_CHECK_URL", check_url)
        .env_remove("NO_COLOR")
        .args(["update"])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .expect("failed to spawn the supercode binary")
}

/// The running binary's own version — same source `update_cmd` reads
/// (`CARGO_PKG_VERSION` of the `supercode-cli` package), so this is exactly
/// what the compiled-in "current version" will be.
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");

#[test]
fn newer_release_available_reports_update_and_upgrade_command() {
    let body = r#"{"tag_name":"v999.0.0","name":"v999.0.0","draft":false,"prerelease":false}"#;
    let (addr, server) = spawn_http_stub("200 OK", body);

    let out = run_update(&format!("http://{addr}/repos/x/x/releases/latest"));

    assert!(
        out.status.success(),
        "update should exit 0 even when reporting an available update: status={:?} stdout={} stderr={}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains("v999.0.0"),
        "expected the newer tag in the output, got: {stdout}"
    );
    assert!(
        stdout.to_lowercase().contains("update"),
        "expected an update-available notice, got: {stdout}"
    );
    // Always prints the static per-channel commands too (AC dev/01: "shows
    // the right upgrade command for the detected install method" — the
    // full list stays as a fallback/context either way).
    assert!(
        stdout.contains("brew upgrade supercode") && stdout.contains("cargo install supercode-cli"),
        "expected the static upgrade command rows to remain present, got: {stdout}"
    );

    server.join().expect("stub server thread panicked");
}

#[test]
fn up_to_date_release_reports_current() {
    let tag = format!("v{CURRENT_VERSION}");
    let body = format!(r#"{{"tag_name":"{tag}","name":"{tag}","draft":false}}"#);
    let (addr, server) = spawn_http_stub("200 OK", body);

    let out = run_update(&format!("http://{addr}/repos/x/x/releases/latest"));

    assert!(
        out.status.success(),
        "update failed: status={:?} stdout={} stderr={}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.to_lowercase().contains("up to date"),
        "expected an up-to-date notice when the latest tag matches the running version, got: {stdout}"
    );

    server.join().expect("stub server thread panicked");
}

#[test]
fn check_failure_404_degrades_gracefully_and_still_exits_zero() {
    // Mirrors the real (currently-private) repo's actual response shape,
    // proven live: `GET api.github.com/repos/volter-ai/supercode/releases/latest`
    // returns exactly this 404 body today.
    let body = r#"{"message":"Not Found","documentation_url":"https://docs.github.com/rest/releases/releases#get-the-latest-release","status":"404"}"#;
    let (addr, server) = spawn_http_stub("404 Not Found", body);

    let out = run_update(&format!("http://{addr}/repos/x/x/releases/latest"));

    assert!(
        out.status.success(),
        "a failed update check must never fail the command: status={:?} stdout={} stderr={}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.to_lowercase().contains("couldn't check"),
        "expected a graceful 'couldn't check' notice on 404, got: {stdout}"
    );
    // AC dev/02: "prints upgrade commands" even when the check fails.
    assert!(
        stdout.contains("curl -fsSL https://supercode.volter.ai/install.sh"),
        "expected the static upgrade commands to still print on check failure, got: {stdout}"
    );

    server.join().expect("stub server thread panicked");
}

#[test]
fn check_failure_unreachable_endpoint_degrades_gracefully() {
    // Bind a listener to reserve a free port, then drop it before the CLI
    // connects — guarantees a real, immediate connection-refused rather
    // than relying on network access to an external unreachable host.
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind to find a free port");
    let addr = listener.local_addr().unwrap();
    drop(listener);

    let out = run_update(&format!("http://{addr}/repos/x/x/releases/latest"));

    assert!(
        out.status.success(),
        "an unreachable update-check endpoint must never fail the command: status={:?} stdout={} stderr={}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.to_lowercase().contains("couldn't check"),
        "expected a graceful 'couldn't check' notice when unreachable, got: {stdout}"
    );
}