use std::process::Command; fn lean_ctx_bin() -> Command { let mut cmd = Command::new(env!("CARGO_BIN_EXE_lean-ctx")); cmd.current_dir(env!("CARGO_MANIFEST_DIR")); cmd.env("LEAN_CTX_ACTIVE", "1"); cmd.env("__LEAN_CTX_SKIP_EVENTS", "1"); // These tests exercise CLI/compression behavior, not the shell allowlist // (which has its own unit tests in shell::exec). On CI stderr is not a TTY, // so `allowlist_must_enforce()` would block test scripts (`for`/`while` // loops are not allowlisted) with exit 126 — hermetic warn-only keeps the // tests pinned to what they actually assert. cmd.env("LEAN_CTX_ALLOWLIST_WARN_ONLY", "1"); cmd } #[test] fn binary_prints_version() { let output = lean_ctx_bin() .arg("--version") .output() .expect("failed to run lean-ctx"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("lean-ctx"), "version output should contain 'lean-ctx', got: {stdout}" ); } #[test] fn binary_prints_help() { let output = lean_ctx_bin() .arg("--help") .output() .expect("failed to run lean-ctx"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("Context Runtime"), "help should contain tagline" ); assert!(stdout.contains("lean-ctx"), "help should mention lean-ctx"); } #[test] fn binary_read_file() { let output = lean_ctx_bin() .args(["read", "Cargo.toml", "-m", "signatures"]) .output() .expect("failed to run lean-ctx"); assert!(output.status.success(), "read should succeed"); } #[test] fn binary_config_shows_defaults() { let output = lean_ctx_bin() .arg("config") .output() .expect("failed to run lean-ctx"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("checkpoint_interval"), "config should show checkpoint_interval" ); } #[test] fn shell_hook_compresses_echo() { let output = lean_ctx_bin() .args(["-c", "echo", "hello", "world"]) .output() .expect("failed to run lean-ctx -c"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("hello"), "shell hook should pass through echo output" ); } #[test] fn disabled_env_bypasses_compression() { let output = Command::new(env!("CARGO_BIN_EXE_lean-ctx")) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("LEAN_CTX_DISABLED", "1") .env("LEAN_CTX_COMPRESS", "1") .env("__LEAN_CTX_SKIP_EVENTS", "1") .args(["-c", "echo", "passthrough test"]) .output() .expect("failed to run lean-ctx with LEAN_CTX_DISABLED"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("passthrough"), "LEAN_CTX_DISABLED should pass output through unmodified" ); assert!( !stdout.contains("[lean-ctx:"), "LEAN_CTX_DISABLED should not add compression markers" ); } #[test] fn help_shows_environment_section() { let output = lean_ctx_bin() .arg("--help") .output() .expect("failed to run lean-ctx"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("LEAN_CTX_DISABLED"), "help should document LEAN_CTX_DISABLED" ); assert!( stdout.contains("LEAN_CTX_RAW"), "help should document LEAN_CTX_RAW" ); } // ── Pipe Guard Tests ──────────────────────────────────────── #[test] fn pipe_guard_no_compression_when_stdout_is_piped() { if cfg!(windows) { return; } let output = Command::new(env!("CARGO_BIN_EXE_lean-ctx")) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("__LEAN_CTX_SKIP_EVENTS", "1") .args(["-c", "echo hello world"]) .output() .expect("failed to run lean-ctx -c with piped stdout"); let stdout = String::from_utf8_lossy(&output.stdout); assert_eq!( stdout.trim(), "hello world", "piped stdout must pass through raw output, got: {stdout}" ); } #[test] fn pipe_guard_force_compress_overrides_pipe_guard() { if cfg!(windows) { return; } let output = Command::new(env!("CARGO_BIN_EXE_lean-ctx")) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("LEAN_CTX_COMPRESS", "1") .env("__LEAN_CTX_SKIP_EVENTS", "1") .args(["-c", "echo hello world"]) .output() .expect("failed to run lean-ctx -c with LEAN_CTX_COMPRESS"); assert!( output.status.success(), "LEAN_CTX_COMPRESS should not crash even with piped stdout" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("hello"), "output should contain the echoed text" ); } #[test] fn pipe_guard_multiline_output_unchanged_when_piped() { if cfg!(windows) { return; } let script = "echo line1; echo line2; echo line3; echo 'result: 42'"; let output = Command::new(env!("CARGO_BIN_EXE_lean-ctx")) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("__LEAN_CTX_SKIP_EVENTS", "1") .args(["-c", script]) .output() .expect("failed to run lean-ctx -c with multiline output"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("line1"), "must contain line1"); assert!(stdout.contains("line2"), "must contain line2"); assert!(stdout.contains("line3"), "must contain line3"); assert!( stdout.contains("result: 42"), "must preserve exact output content" ); } #[test] fn pipe_guard_bash_hook_script_test() { if cfg!(windows) { return; } let binary = env!("CARGO_BIN_EXE_lean-ctx"); let script = format!( r#" _lc() {{ if [ -n "${{LEAN_CTX_DISABLED:-}}" ] || [ ! -t 1 ]; then command "$@" return fi '{binary}' -c "$@" }} # Pipe test: _lc echo should bypass lean-ctx when piped RESULT=$(_lc echo "pipe-guard-test-value") echo "CAPTURED:$RESULT" "# ); let output = Command::new("bash") .args(["-c", &script]) .output() .expect("failed to run bash pipe guard test"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("CAPTURED:pipe-guard-test-value"), "pipe guard must bypass lean-ctx in command substitution, got: {stdout}" ); } #[test] fn pipe_guard_bash_hook_pipe_to_sh() { if cfg!(windows) { return; } let binary = env!("CARGO_BIN_EXE_lean-ctx"); let script = format!( r#" _lc() {{ if [ -n "${{LEAN_CTX_DISABLED:-}}" ] || [ ! -t 1 ]; then command "$@" return fi '{binary}' -c "$@" }} # Simulate curl | sh: echo a script, pipe to sh _lc echo 'echo INSTALL_SUCCESS' | sh "# ); let output = Command::new("bash") .args(["-c", &script]) .output() .expect("failed to run bash pipe-to-sh test"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("INSTALL_SUCCESS"), "piped script must execute correctly through sh, got: {stdout}" ); } #[test] fn pipe_guard_bash_hook_redirect_to_file() { if cfg!(windows) { return; } let binary = env!("CARGO_BIN_EXE_lean-ctx"); let tmp = std::env::temp_dir().join("lean-ctx-pipe-guard-test.txt"); let tmp_path = tmp.to_str().unwrap(); let script = format!( r#" _lc() {{ if [ -n "${{LEAN_CTX_DISABLED:-}}" ] || [ ! -t 1 ]; then command "$@" return fi '{binary}' -c "$@" }} _lc echo "redirect-test-value" > {tmp_path} cat {tmp_path} rm -f {tmp_path} "# ); let output = Command::new("bash") .args(["-c", &script]) .output() .expect("failed to run bash redirect test"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("redirect-test-value"), "redirected output must be raw, got: {stdout}" ); } // ── Redirect-to-file fidelity under FORCED compression ────── // Regression: the agent shell hook deliberately bypasses the `[ ! -t 1 ]` pipe // guard (an agent IS the consumer), so an agent running // `git status --short > files.txt` reached `lean-ctx -c` with stdout pointing at a // regular FILE and forced compression on. The compressed digest was written INTO // the file, dropping/abbreviating lines and producing contradictory diffs. Output // redirected to a regular file must ALWAYS stay byte-faithful, even when // compression is forced; pipes (agent capture) must keep compressing. // >30 distinct lines so the compression engine definitely rewrites the output // (the truncate-with-safety-scan fallback collapses the middle). This makes the // assertions below meaningful — pre-fix, the file held the compressed digest. // The payload must NOT contain any passthrough/verbatim keyword (notably the // literal "lean-ctx", which classifies the whole command as Passthrough), or the // output would never be compressed and the test would be vacuous. fn redirect_fidelity_command() -> &'static str { "i=0; while [ $i -lt 60 ]; do echo \"row $i alpha beta gamma delta payload\"; i=$((i+1)); done" } /// `lean-ctx -c` with a *hermetic* control environment for the compression /// fidelity tests. A developer's shell hook exports `LEAN_CTX_RAW` / /// `LEAN_CTX_DISABLED` (and CI may set `LEAN_CTX_ACTIVE`) — each forces raw /// passthrough, which would make these tests assert nothing (the redirect test /// would pass vacuously). Stripping them pins the exact forced-compress path. /// `LEAN_CTX_WRAPPED` must go too: running `cargo test` *through* the lean-ctx /// wrapper stamps it on every child, and the nested `lean-ctx -c` under test /// would then (correctly) defer to the parent instead of compressing. fn forced_compress_bin() -> Command { let mut cmd = Command::new(env!("CARGO_BIN_EXE_lean-ctx")); cmd.current_dir(env!("CARGO_MANIFEST_DIR")) .env_remove("LEAN_CTX_RAW") .env_remove("LEAN_CTX_DISABLED") .env_remove("LEAN_CTX_ACTIVE") .env_remove("LEAN_CTX_WRAPPED") .env("LEAN_CTX_COMPRESS", "1") .env("__LEAN_CTX_SKIP_EVENTS", "1") // Allowlist enforcement (non-TTY stderr ⇒ block) is out of scope here; // the fidelity scripts use `while` loops that are not allowlisted. .env("LEAN_CTX_ALLOWLIST_WARN_ONLY", "1"); cmd } #[test] fn force_compress_redirect_to_file_is_byte_faithful() { if cfg!(windows) { return; } let cmd = redirect_fidelity_command(); // Ground truth: the raw command output (no lean-ctx in the loop). let raw = Command::new("sh") .args(["-c", cmd]) .output() .expect("failed to run raw command"); let raw_stdout = String::from_utf8_lossy(&raw.stdout).to_string(); // Run lean-ctx with forced compression, stdout redirected to a real file — // exactly the agent `cmd > file` scenario. let tmp = std::env::temp_dir().join(format!( "lean-ctx-redirect-fidelity-{}.txt", std::process::id() )); let file = std::fs::File::create(&tmp).expect("create temp file"); let status = forced_compress_bin() .args(["-c", cmd]) .stdout(std::process::Stdio::from(file)) .status() .expect("failed to run lean-ctx -c with file redirect"); assert!(status.success(), "lean-ctx -c should exit cleanly"); let file_content = std::fs::read_to_string(&tmp).expect("read temp file"); let _ = std::fs::remove_file(&tmp); assert_eq!( file_content, raw_stdout, "redirect to a regular file under LEAN_CTX_COMPRESS must be byte-faithful (never compressed)" ); } #[test] fn force_compress_pipe_still_compresses() { // Companion to the redirect test: proves compression is genuinely active for // the SAME command + env when stdout is a PIPE (an agent's capture). If this // ever stops compressing, the redirect test above would be vacuous. if cfg!(windows) { return; } let cmd = redirect_fidelity_command(); let raw = Command::new("sh") .args(["-c", cmd]) .output() .expect("failed to run raw command"); let raw_stdout = String::from_utf8_lossy(&raw.stdout).to_string(); let piped = forced_compress_bin() .args(["-c", cmd]) .output() .expect("failed to run lean-ctx -c with piped stdout"); let piped_stdout = String::from_utf8_lossy(&piped.stdout).to_string(); assert_ne!( piped_stdout, raw_stdout, "piped stdout under LEAN_CTX_COMPRESS should be compressed (changed) — proves the redirect guard is meaningful" ); } // ── Extended CLI Smoke Tests ──────────────────────────────── #[test] fn binary_gain_json_returns_valid_json() { let output = lean_ctx_bin() .args(["gain", "--json"]) .output() .expect("failed to run lean-ctx gain --json"); assert!(output.status.success(), "gain --json should succeed"); let stdout = String::from_utf8_lossy(&output.stdout); let parsed: Result = serde_json::from_str(stdout.trim()); assert!( parsed.is_ok(), "gain --json should return valid JSON, got: {stdout}" ); } #[test] fn binary_grep_finds_matches() { let output = lean_ctx_bin() .args(["grep", "fn main", "src/"]) .output() .expect("failed to run lean-ctx grep"); assert!( output.status.success(), "grep should succeed (exit={})", output.status.code().unwrap_or(-1) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("main") || stdout.contains("fn"), "grep should find matches: {stdout}" ); } #[test] fn binary_ls_shows_tree() { let output = lean_ctx_bin() .args(["ls", "--depth", "2"]) .output() .expect("failed to run lean-ctx ls"); assert!( output.status.success(), "ls should succeed (exit={})", output.status.code().unwrap_or(-1) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("src") || stdout.contains("Cargo"), "ls should show directory tree: {stdout}" ); } #[test] fn binary_doctor_runs_without_crash() { let output = lean_ctx_bin() .arg("doctor") .output() .expect("failed to run lean-ctx doctor"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("lean-ctx") || stdout.contains("doctor") || stdout.contains("✓") || stdout.contains("check"), "doctor should produce diagnostic output: {stdout}" ); } #[test] fn pipe_guard_rust_side_defense_in_depth() { if cfg!(windows) { return; } let script = "for i in 1 2 3 4 5; do echo \"item_$i: $(date +%s)\"; done"; let output = Command::new(env!("CARGO_BIN_EXE_lean-ctx")) .current_dir(env!("CARGO_MANIFEST_DIR")) .env("__LEAN_CTX_SKIP_EVENTS", "1") // `for` + command substitution are not allowlisted; this test pins the // pipe guard, not enforcement (covered by shell::exec unit tests). .env("LEAN_CTX_ALLOWLIST_WARN_ONLY", "1") .args(["-c", script]) .output() .expect("failed to run lean-ctx -c"); let stdout = String::from_utf8_lossy(&output.stdout); for i in 1..=5 { assert!( stdout.contains(&format!("item_{i}:")), "Rust-side pipe guard must pass through all lines unchanged (missing item_{i})" ); } }