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-38 (`--quiet` scripting mode):
//!
//! - dev/01: `supercode --quiet run ...` emits no spinner/status/banner
//!   chrome to stderr; only the answer (stdout) and genuine errors remain.
//! - dev/02: `SUPERCODE_QUIET=1` has the same effect as the flag.
//! - dev/03: non-quiet runs are unchanged (still carry the chrome).
//!
//! Spawns the real, built `supercode` binary (mirroring `spinner_cli.rs`'s
//! idiom) against a real local HTTP/SSE stub — the CLI, streaming sink,
//! banner/status code, and `--quiet` gating are all the genuine, unmodified
//! release/debug binary, not a reimplementation.

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

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-ux38-quiet-{tag}-{}-{nanos}",
        std::process::id()
    ));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

/// A one-shot local HTTP server that answers immediately with a hand-framed
/// `text/event-stream` reply — same framing `supercode-core`'s
/// `OpenAiProvider` parses, mirrored from `spinner_cli.rs`.
fn spawn_sse_stub() -> (std::net::SocketAddr, std::thread::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 one connection");
        let mut buf = [0u8; 4096];
        let _ = sock.read(&mut buf); // drain the request; contents unused.

        let sse = "data: {\"choices\":[{\"delta\":{\"content\":\"hel\"}}]}\n\n\
                   data: {\"choices\":[{\"delta\":{\"content\":\"lo there\"}}]}\n\n\
                   data: [DONE]\n\n";
        let resp = format!(
            "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
            sse.len(),
            sse
        );
        sock.write_all(resp.as_bytes())
            .expect("write stub response");
        sock.flush().ok();
    });
    (addr, handle)
}

fn run_piped(home: &Path, base_url: &str, extra: &[&str], quiet_env: bool) -> Output {
    let mut args = vec!["--api-key", "x", "--base-url", base_url];
    args.extend_from_slice(extra);
    let mut cmd = Command::new(bin());
    cmd.env("SUPERCODE_HOME", home)
        .env_remove("OPENROUTER_API_KEY")
        .env_remove("OPENAI_API_KEY")
        .env_remove("ANTHROPIC_API_KEY")
        .env_remove("NO_COLOR");
    if quiet_env {
        cmd.env("SUPERCODE_QUIET", "1");
    } else {
        cmd.env_remove("SUPERCODE_QUIET");
    }
    cmd.args(&args)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("failed to spawn the supercode binary")
        .wait_with_output()
        .expect("child process failed")
}

/// The exact substring `build_config`'s safety banner emits
/// (`"{sandbox_label} / {approval_label}"`, UX-38's canonical "safety
/// banner"). Non-interactive piped stdin/stderr resolves to the
/// `workspace-write` / `never` defaults, so this string appears in EVERY
/// non-quiet run of this test and in NO quiet run — a precise, unambiguous
/// chrome marker (unlike raw "supercode", which could theoretically appear
/// elsewhere).
const BANNER_MARKER: &str = "workspace-write / never";

#[test]
fn non_quiet_run_shows_the_safety_banner_and_the_reply() {
    let (addr, server) = spawn_sse_stub();
    let home = fresh_home("chrome-on");

    let out = run_piped(&home, &format!("http://{addr}"), &["run", "say hi"], false);

    assert!(
        out.status.success(),
        "run failed: status={:?} stdout={} stderr={}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains(BANNER_MARKER),
        "expected the safety banner on stderr without --quiet, got: {stderr}"
    );
    assert!(
        String::from_utf8_lossy(&out.stdout).contains("hello there"),
        "expected the streamed reply on stdout, got: {}",
        String::from_utf8_lossy(&out.stdout)
    );

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

#[test]
fn quiet_flag_suppresses_the_banner_but_keeps_the_reply() {
    let (addr, server) = spawn_sse_stub();
    let home = fresh_home("chrome-off-flag");

    let out = run_piped(
        &home,
        &format!("http://{addr}"),
        &["--quiet", "run", "say hi"],
        false,
    );

    assert!(
        out.status.success(),
        "run --quiet failed: status={:?} stdout={} stderr={}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        !stderr.contains(BANNER_MARKER),
        "--quiet must suppress the safety banner, got stderr: {stderr}"
    );
    // dev/01: "only the answer (stdout) ... remain" — stdout is byte-for-byte
    // the same reply as the non-quiet run above.
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(
        stdout.contains("hello there"),
        "the actual result must never be suppressed by --quiet, got stdout: {stdout}"
    );

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

#[test]
fn supercode_quiet_env_var_has_the_same_effect_as_the_flag() {
    // dev/02: `SUPERCODE_QUIET=1` alone (no --quiet flag) suppresses chrome
    // identically.
    let (addr, server) = spawn_sse_stub();
    let home = fresh_home("chrome-off-env");

    let out = run_piped(&home, &format!("http://{addr}"), &["run", "say hi"], true);

    assert!(
        out.status.success(),
        "SUPERCODE_QUIET=1 run failed: status={:?} stdout={} stderr={}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        !stderr.contains(BANNER_MARKER),
        "SUPERCODE_QUIET=1 must suppress the safety banner exactly like --quiet, got stderr: {stderr}"
    );
    assert!(
        String::from_utf8_lossy(&out.stdout).contains("hello there"),
        "the actual result must never be suppressed by SUPERCODE_QUIET, got stdout: {}",
        String::from_utf8_lossy(&out.stdout)
    );

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

#[test]
fn quiet_does_not_swallow_errors() {
    // A `resume` against a session file that doesn't exist is a clean,
    // deterministic, offline way to force a genuine error (no API key/
    // network involved) — `Session::load` fails before `require_api_key`
    // even matters here since resume_cmd calls `require_api_key` first;
    // use an obviously-missing path either way triggers a real `Err` that
    // propagates to `main`'s top-level handler and a non-zero exit.
    let home = fresh_home("quiet-errors");
    let missing = home.join("does-not-exist.jsonl");

    let out = Command::new(bin())
        .env("SUPERCODE_HOME", &home)
        .env("OPENROUTER_API_KEY", "x")
        .env_remove("SUPERCODE_QUIET")
        .env_remove("NO_COLOR")
        .args(["--quiet", "resume", missing.to_str().unwrap()])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .expect("failed to spawn the supercode binary");

    assert!(
        !out.status.success(),
        "resuming a missing session must fail even under --quiet"
    );
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        !stderr.trim().is_empty(),
        "the genuine error must still be printed to stderr under --quiet"
    );
    assert!(
        !stderr.contains(BANNER_MARKER),
        "an error run under --quiet must still suppress the (irrelevant, pre-error) banner if it \
         were ever reached, got stderr: {stderr}"
    );
}