bctx 0.1.29

bctx CLI — intercept CLI commands and compress output for LLM coding agents
//! End-to-end CLI tests that drive the real `bctx` binary the way a user/agent does.
//!
//! Each test runs the compiled binary in an isolated `HOME` (so it reads/writes a
//! throwaway `~/.bctx/executions.db`) and from a temp CWD (the repo root carries a
//! `.bctx-bypass` file that would otherwise disable recording).

use std::path::PathBuf;
use std::process::Command;

fn unique_dir(tag: &str) -> PathBuf {
    // Test binaries run in parallel; combine pid + the per-test tag for a unique path
    // without needing Date/rand (a thread id collision across tags is not possible).
    let dir = std::env::temp_dir().join(format!("bctx-cli-{}-{tag}", std::process::id()));
    std::fs::create_dir_all(&dir).expect("create temp dir");
    dir
}

fn bctx() -> &'static str {
    env!("CARGO_BIN_EXE_bctx")
}

/// Regression for the stderr-recording fix: a command whose entire output goes to STDERR
/// (cargo, rustc, tsc, … all do this) used to record NOTHING, because the token count read
/// only stdout and recording was gated on `tokens_before > 0`. `bctx gain` stayed empty.
#[test]
fn records_command_that_writes_only_to_stderr() {
    let home = unique_dir("stderr");

    let run = Command::new(bctx())
        .current_dir(&home)
        .env("HOME", &home)
        .env_remove("BCTX_BYPASS")
        .args([
            "sh",
            "-c",
            "i=1; while [ $i -le 80 ]; do echo \"compiling crate-$i v1.0.0\" 1>&2; i=$((i+1)); done",
        ])
        .output()
        .expect("spawn bctx <stderr-only cmd>");
    assert!(
        run.status.code().is_some(),
        "wrapped command did not run to completion"
    );

    let gain = Command::new(bctx())
        .current_dir(&home)
        .env("HOME", &home)
        .arg("gain")
        .output()
        .expect("spawn bctx gain");
    let out = String::from_utf8_lossy(&gain.stdout);

    assert!(
        !out.contains("No executions recorded yet"),
        "stderr-only command was not recorded — the stderr-accounting regression is back.\n\
         gain output:\n{out}"
    );
    assert!(
        out.contains("Commands run"),
        "gain summary missing.\ngain output:\n{out}"
    );

    let _ = std::fs::remove_dir_all(&home);
}

/// A command with real stdout is still recorded, and `gain` reports a non-empty summary.
#[test]
fn records_command_with_stdout() {
    let home = unique_dir("stdout");

    let _ = Command::new(bctx())
        .current_dir(&home)
        .env("HOME", &home)
        .env_remove("BCTX_BYPASS")
        .args([
            "sh",
            "-c",
            "i=1; while [ $i -le 80 ]; do echo \"row $i\"; i=$((i+1)); done",
        ])
        .output()
        .expect("spawn bctx <stdout cmd>");

    let gain = Command::new(bctx())
        .current_dir(&home)
        .env("HOME", &home)
        .arg("gain")
        .output()
        .expect("spawn bctx gain");
    let out = String::from_utf8_lossy(&gain.stdout);

    assert!(out.contains("TOKEN SAVINGS"), "gain output:\n{out}");
    assert!(
        !out.contains("No executions recorded yet"),
        "gain output:\n{out}"
    );

    let _ = std::fs::remove_dir_all(&home);
}