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 TR-4/T30 dev/05's C7 clause: `sessions
//! show-reductions` (and, transitively, `inspect`'s `reduce::project`-backed
//! view/full token figures) must attribute savings under the
//! `output-normalized` kind for a bash tool result that was ANSI/redraw
//! collapsed.
//!
//! Follows `reductions_cli.rs`'s exact harness (spawn the built `supercode`
//! binary against an unreachable `--base-url`, `resume --reduced` a
//! hand-crafted Claude Code JSONL fixture to mint an offline reduced
//! session, no network activity ever) rather than duplicating a shared test
//! module — this crate's other integration test files follow the same
//! self-contained-per-file convention.

use std::path::{Path, PathBuf};
use std::process::{Command, Output};

use supercode::reduce::{project, ReductionKind, ReductionLog, ReductionPolicy};
use supercode::session::Session;

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

fn run(home: &Path, extra: &[&str]) -> Output {
    Command::new(bin())
        .env("SUPERCODE_HOME", home)
        .env_remove("OPENROUTER_API_KEY")
        .env_remove("OPENAI_API_KEY")
        .env_remove("ANTHROPIC_API_KEY")
        .args(["--api-key", "x", "--base-url", "http://127.0.0.1:1"])
        .args(extra)
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
        .expect("failed to spawn the supercode binary")
        .wait_with_output()
        .expect("child process failed")
}

fn stdout(out: &Output) -> String {
    String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr(out: &Output) -> String {
    String::from_utf8_lossy(&out.stderr).into_owned()
}

fn sessions_dir(home: &Path) -> PathBuf {
    home.join("sessions")
}

/// The genuine `cargo build --color always` pty capture used by
/// `supercode-core`'s own TR-4 fixture suite
/// (`crates/harness/tests/fixtures/terminal/cargo_build.raw`) — reused here
/// (rather than duplicating the bytes) so both crates' tests exercise the
/// exact same real capture.
fn cargo_build_raw() -> String {
    let path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../harness/tests/fixtures/terminal/cargo_build.raw");
    std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("reading {}: {e}", path.display()))
}

/// A minimal, valid Claude Code session JSONL with ONE `bash` tool result
/// carrying real ANSI/CR-redraw noise (`cargo_build.raw`) — comfortably over
/// TR-4's default 128B savings floor and, since it is the ONLY tool result,
/// also comfortably inside A7's default `protect_last_n_tool_results`
/// window (T30 does not consult `protected` — see `reduce.rs`'s T30 pass —
/// so this is exactly the "most recent tool result" case, not an
/// artificially aged one).
fn write_ansi_fixture(dir: &Path) -> PathBuf {
    let sid = "77777777-8888-9999-aaaa-bbbbbbbbbbbb";
    let noisy = cargo_build_raw();
    let mut lines: Vec<String> = Vec::new();
    lines.push(
        serde_json::json!({
            "type": "user",
            "message": {"role": "user", "content": "please build the project"},
            "uuid": "u0", "parentUuid": null,
            "timestamp": "2026-07-07T18:38:00.000Z", "sessionId": sid,
            "cwd": "/tmp/proj", "userType": "external",
        })
        .to_string(),
    );
    lines.push(
        serde_json::json!({
            "type": "assistant",
            "message": {"role": "assistant", "content": [
                {"type": "tool_use", "id": "toolu_00", "name": "bash", "input": {"command": "cargo build"}}
            ]},
            "uuid": "a0", "parentUuid": "u0",
            "timestamp": "2026-07-07T18:38:01.000Z", "sessionId": sid,
        })
        .to_string(),
    );
    lines.push(
        serde_json::json!({
            "type": "user",
            "message": {"role": "user", "content": [
                {"tool_use_id": "toolu_00", "type": "tool_result", "content": [{"type": "text", "text": noisy}]}
            ]},
            "uuid": "t0", "parentUuid": "a0",
            "timestamp": "2026-07-07T18:38:02.000Z", "sessionId": sid,
            "toolUseResult": {"status": "completed"},
        })
        .to_string(),
    );
    let path = dir.join("ansi_claude_session.jsonl");
    std::fs::write(&path, lines.join("\n") + "\n").unwrap();
    path
}

fn mint_reduced_session(home: &Path, fixture: &Path) -> String {
    let out = run(home, &["resume", fixture.to_str().unwrap(), "--reduced"]);
    let err = stderr(&out);
    let line = err
        .lines()
        .find(|l| l.contains("full copy:"))
        .unwrap_or_else(|| panic!("no `full copy:` line in:\n{err}"));
    let sidecar_path = line.split("full copy:").nth(1).unwrap().trim();
    Path::new(sidecar_path)
        .file_name()
        .unwrap()
        .to_str()
        .unwrap()
        .strip_suffix(".sidecar.jsonl")
        .unwrap()
        .to_string()
}

#[test]
fn show_reductions_attributes_savings_under_output_normalized() {
    let home = fresh_home("output-normalized");
    let fixture = write_ansi_fixture(&home);

    // Ground truth, independent of the CLI: `project()` must mint exactly
    // one `OutputNormalized` reduction for this fixture.
    let session = Session::load(&fixture).unwrap();
    let (_, log) = project(
        &session,
        &ReductionPolicy::default(),
        &ReductionLog::default(),
    );
    assert_eq!(log.reductions.len(), 1, "{:?}", log.reductions);
    let (expected_id, expected_original_bytes) = match &log.reductions[0].kind {
        ReductionKind::OutputNormalized { original_bytes, .. } => {
            (log.reductions[0].id.clone(), *original_bytes)
        }
        other => panic!("expected OutputNormalized, got {other:?}"),
    };

    let name = mint_reduced_session(&home, &fixture);

    // The persisted reduction-log family file must record the same kind.
    let log_path = sessions_dir(&home).join(format!("{name}.reduction.json"));
    let log_json: serde_json::Value =
        serde_json::from_str(&std::fs::read_to_string(&log_path).unwrap()).unwrap();
    let records = log_json["reductions"].as_array().unwrap();
    assert_eq!(records.len(), 1);
    assert!(
        records[0]["kind"]
            .as_object()
            .map(|o| o.contains_key("OutputNormalized"))
            .unwrap_or(false),
        "persisted log must record an OutputNormalized kind: {records:?}"
    );

    // `sessions show-reductions` (C4/C7): the row must name the kind
    // `output-normalized`, the id, and attribute the RAW (pre-normalization)
    // byte count as savings — not some other figure.
    let out = run(&home, &["sessions", "show-reductions", &name]);
    assert!(out.status.success(), "must exit 0: {}", stderr(&out));
    let table = stdout(&out);
    assert!(
        table.contains(&expected_id),
        "table must list id {expected_id}:\n{table}"
    );
    assert!(
        table.contains("output-normalized"),
        "table must show the output-normalized kind:\n{table}"
    );
    // The byte column is comma-grouped (`format_commas`); compare against
    // that same rendering rather than the raw digit string.
    let comma_grouped = {
        let digits = expected_original_bytes.to_string();
        let bytes = digits.as_bytes();
        let mut s = String::new();
        for (i, b) in bytes.iter().enumerate() {
            if i > 0 && (bytes.len() - i) % 3 == 0 {
                s.push(',');
            }
            s.push(*b as char);
        }
        s
    };
    assert!(
        table.contains(&comma_grouped),
        "table must attribute {comma_grouped} B (the raw original size) as savings:\n{table}"
    );

    // `--json` must round-trip the same single record.
    let json_out = run(&home, &["sessions", "show-reductions", &name, "--json"]);
    assert!(json_out.status.success());
    let log: serde_json::Value = serde_json::from_str(&stdout(&json_out)).unwrap();
    assert_eq!(log["reductions"].as_array().unwrap().len(), 1);

    // `inspect`'s aggregate view/full token figures must also reflect the
    // reduction (a non-reduced view would be identical to the full one): the
    // metadata panel's `reduced` row names the stub count and the same
    // view/full token figures `show-reductions` reported above (C9
    // cross-surface consistency).
    let inspect_out = run(&home, &["inspect", &name]);
    assert!(inspect_out.status.success(), "{}", stderr(&inspect_out));
    let inspect_stdout = stdout(&inspect_out);
    assert!(
        inspect_stdout.contains("reduced") && inspect_stdout.contains("1 stubs"),
        "inspect must show the reduced-session stub count: {inspect_stdout}"
    );

    std::fs::remove_dir_all(&home).ok();
}