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 tests for UX-31 ("Provider/auth doctor tiers"):
//!
//! - dev/01: `supercode doctor` reports per-checkpoint results (config →
//!   key → reachability → optional deep chat/tool probe) and names the
//!   first failing tier with a hint.
//! - dev/02: the deep probe is opt-in (`--deep`) and bounded by a timeout.
//!
//! Spawns the real, built `supercode` binary against real local HTTP stubs
//! (mirroring `login_cli.rs`'s idiom) — the CLI, tier-computation, and
//! reachability/deep-probe code are the genuine, unmodified binary, not a
//! reimplementation. Every scenario below (missing key, catalog reject,
//! deep-probe pass) is an INDUCED failure/success naming the tier it
//! belongs to, per UX-31's proof mode.

use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener};
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::thread::JoinHandle;

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

fn fresh_home(tag: &str) -> PathBuf {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let dir = std::env::temp_dir().join(format!(
        "supercode-ux31-doctor-{tag}-{}-{nanos}",
        std::process::id()
    ));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

/// A one-shot local HTTP stub answering `GET /models` with `models_status`
/// and — only if `chat_body` is `Some` — a second connection's
/// `POST /chat/completions` with an SSE body. No real network access.
fn spawn_stub(
    models_status: &'static str,
    chat_body: Option<&'static str>,
) -> (SocketAddr, JoinHandle<()>) {
    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 /models connection");
        let mut buf = [0u8; 4096];
        let _ = sock.read(&mut buf);
        let body = "{\"data\":[]}";
        let resp = format!(
            "HTTP/1.1 {models_status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
            body.len(),
            body
        );
        sock.write_all(resp.as_bytes())
            .expect("write /models stub response");
        sock.flush().ok();

        if let Some(chat_body) = chat_body {
            let (mut sock2, _) = listener
                .accept()
                .expect("accept /chat/completions connection");
            let mut buf2 = [0u8; 8192];
            let _ = sock2.read(&mut buf2);
            let resp2 = format!(
                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
                chat_body.len(),
                chat_body
            );
            sock2
                .write_all(resp2.as_bytes())
                .expect("write /chat/completions stub response");
            sock2.flush().ok();
        }
    });
    (addr, handle)
}

/// A minimal single-chunk SSE stream that calls the `ping` tool once, then
/// `[DONE]` — the wire shape `crates/harness/src/provider.rs`'s SSE parser
/// expects (mirrors its own unit tests).
const PING_TOOL_CALL_SSE: &str = "data: {\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call_1\",\"function\":{\"name\":\"ping\",\"arguments\":\"{}\"}}]}}]}\n\ndata: [DONE]\n\n";

fn run(home: &Path, args: &[&str]) -> Output {
    Command::new(bin())
        .env("SUPERCODE_HOME", home)
        .env_remove("OPENROUTER_API_KEY")
        .env_remove("OPENAI_API_KEY")
        .env_remove("ANTHROPIC_API_KEY")
        .env_remove("NO_COLOR")
        .args(args)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .expect("failed to spawn the supercode binary")
}

fn json_stdout(out: &Output) -> serde_json::Value {
    assert!(
        out.status.success(),
        "expected success, got {:?}\nstdout: {}\nstderr: {}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    serde_json::from_slice(&out.stdout).unwrap_or_else(|e| {
        panic!(
            "stdout was not valid JSON: {e}\nstdout: {}",
            String::from_utf8_lossy(&out.stdout)
        )
    })
}

/// Pull out the tier object named `name` from a `doctor --json` `tiers` array.
fn tier<'a>(v: &'a serde_json::Value, name: &str) -> &'a serde_json::Value {
    v["tiers"]
        .as_array()
        .expect("tiers must be an array")
        .iter()
        .find(|t| t["name"] == name)
        .unwrap_or_else(|| panic!("no tier named {name} in {v}"))
}

// ---- dev/01: tier 1 (config) fails cleanly, names itself as the culprit --

#[test]
fn doctor_json_names_config_as_the_first_failing_tier_when_no_key_is_configured() {
    let home = fresh_home("no-key");
    let out = run(&home, &["doctor", "--json"]);
    let v = json_stdout(&out);

    assert_eq!(v["first_failing_tier"], "config");
    assert_eq!(v["deep_probe_requested"], false);

    let config = tier(&v, "config");
    assert_eq!(config["status"], "fail");
    assert!(config["hint"].as_str().unwrap().contains("supercode login"));

    // Tiers 2 and 3 didn't (and couldn't) run — skipped, not failed, so
    // the reader isn't misled into chasing three symptoms of one cause.
    assert_eq!(tier(&v, "catalog")["status"], "skip");
    assert_eq!(tier(&v, "deep")["status"], "skip");
}

#[test]
fn doctor_human_shows_diagnostic_tiers_and_the_first_failing_hint() {
    let home = fresh_home("no-key-human");
    let out = run(&home, &["doctor"]);
    assert!(out.status.success());
    let stdout = String::from_utf8_lossy(&out.stdout);

    assert!(stdout.contains("diagnostic tiers"), "got: {stdout}");
    assert!(stdout.contains("tier · config"), "got: {stdout}");
    assert!(stdout.contains("tier · catalog"), "got: {stdout}");
    assert!(stdout.contains("tier · deep"), "got: {stdout}");
    assert!(
        stdout.contains("first failing tier: config"),
        "expected a first-failing-tier hint line naming `config`, got: {stdout}"
    );
    assert!(serde_json::from_str::<serde_json::Value>(&stdout).is_err());
}

// ---- dev/01: tier 2 (catalog) passes with a reachable key, tier 3 stays --
// ---- skipped unless --deep is passed -------------------------------------

#[test]
fn doctor_json_tier2_passes_and_tier3_is_skipped_without_the_deep_flag() {
    let (addr, server) = spawn_stub("200 OK", None);
    let home = fresh_home("catalog-pass");

    let out = run(
        &home,
        &[
            "--api-key",
            "sk-test-key",
            "--base-url",
            &format!("http://{addr}"),
            "doctor",
            "--json",
        ],
    );
    let v = json_stdout(&out);

    assert_eq!(v["first_failing_tier"], serde_json::Value::Null);
    assert_eq!(v["deep_probe_requested"], false);
    assert_eq!(tier(&v, "config")["status"], "pass");
    let catalog = tier(&v, "catalog");
    assert_eq!(catalog["status"], "pass");
    assert!(catalog["detail"].as_str().unwrap().contains("reachable"));
    let deep = tier(&v, "deep");
    assert_eq!(deep["status"], "skip");
    assert!(deep["hint"].as_str().unwrap().contains("--deep"));

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

// ---- dev/02: --deep runs a real chat + tool round-trip and reports pass --

#[test]
fn doctor_json_deep_probe_runs_and_passes_with_the_deep_flag() {
    let (addr, server) = spawn_stub("200 OK", Some(PING_TOOL_CALL_SSE));
    let home = fresh_home("deep-pass");

    let out = run(
        &home,
        &[
            "--api-key",
            "sk-test-key",
            "--base-url",
            &format!("http://{addr}"),
            "doctor",
            "--json",
            "--deep",
        ],
    );
    let v = json_stdout(&out);

    assert_eq!(v["deep_probe_requested"], true);
    assert_eq!(v["first_failing_tier"], serde_json::Value::Null);
    let deep = tier(&v, "deep");
    assert_eq!(deep["status"], "pass");
    assert!(
        deep["detail"].as_str().unwrap().contains("tool-call"),
        "expected the round-trip's tool call to be observed, got: {v}"
    );

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

// ---- dev/01: induced failure at tier 2 (catalog) — the AC's required ----
// ---- "induced failure naming the failing tier" proof mode ---------------

#[test]
fn doctor_json_names_catalog_as_the_first_failing_tier_when_the_key_is_rejected() {
    let (addr, server) = spawn_stub("401 Unauthorized", None);
    let home = fresh_home("catalog-fail");

    // --deep is passed too: tier 3 must stay SKIPPED (not attempted, not a
    // second failure) once tier 2 has already failed.
    let out = run(
        &home,
        &[
            "--api-key",
            "sk-bad-key",
            "--base-url",
            &format!("http://{addr}"),
            "doctor",
            "--json",
            "--deep",
        ],
    );
    let v = json_stdout(&out);

    assert_eq!(v["first_failing_tier"], "catalog");
    assert_eq!(tier(&v, "config")["status"], "pass");
    let catalog = tier(&v, "catalog");
    assert_eq!(catalog["status"], "fail");
    assert!(catalog["detail"].as_str().unwrap().contains("401"));
    assert!(catalog["hint"].as_str().unwrap().contains("key/base-url"));
    let deep = tier(&v, "deep");
    assert_eq!(
        deep["status"], "skip",
        "tier 3 must not run once tier 2 has already failed"
    );

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