bctx 0.1.35

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.
/// Driven through `git` (a routed compressor) because only commands with a real compressor
/// are recorded — recording bare passthrough would pollute `gain` with 0%-saved rows.
#[test]
fn records_command_that_writes_only_to_stderr() {
    let home = unique_dir("stderr");

    // `git status` in a non-repo dir writes "fatal: not a git repository" to STDERR only.
    let run = Command::new(bctx())
        .current_dir(&home)
        .env("HOME", &home)
        .env_remove("BCTX_BYPASS")
        .args(["git", "status"])
        .output()
        .expect("spawn bctx git status");
    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");

    // `git --version` writes a routed, deterministic line to STDOUT.
    let _ = Command::new(bctx())
        .current_dir(&home)
        .env("HOME", &home)
        .env_remove("BCTX_BYPASS")
        .args(["git", "--version"])
        .output()
        .expect("spawn bctx git --version");

    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);
}

/// A command with no domain compressor (bare `python -c …`) must NOT be recorded — this is
/// the honest-accounting fix that stops `python app.py` polluting `bctx gain` with 0% rows.
#[test]
fn passthrough_command_is_not_recorded() {
    let home = unique_dir("passthrough");

    let _ = Command::new(bctx())
        .current_dir(&home)
        .env("HOME", &home)
        .env_remove("BCTX_BYPASS")
        .args(["python3", "-c", "print('hello from app')"])
        .output()
        .expect("spawn bctx python3 -c");

    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"),
        "a no-compressor passthrough command was recorded — 0%-row pollution is back.\n\
         gain output:\n{out}"
    );

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