lean-ctx 3.9.13

Context Runtime for AI Agents with CCP. 71 MCP tools, 10 read modes, 95+ compression patterns, cross-session memory (CCP), persistent AI knowledge with temporal facts + contradiction detection, multi-agent context sharing, LITM-aware positioning, AAAK compact format, adaptive compression with Thompson Sampling bandits. Supports 24+ AI tools. Reduces LLM token consumption by up to 99%.
Documentation
use super::*;

#[test]
fn wrap_single() {
    let r = wrap_single_command("git status", "lean-ctx");
    assert_eq!(r, expect_wrapped("git status", "lean-ctx"));
}

#[test]
fn wrap_with_quotes() {
    let r = wrap_single_command(r#"curl -H "Auth" https://api.com"#, "lean-ctx");
    assert_eq!(
        r,
        expect_wrapped(r#"curl -H "Auth" https://api.com"#, "lean-ctx")
    );
}

#[test]
fn rewrite_candidate_returns_none_for_existing_lean_ctx_command() {
    assert_eq!(
        rewrite_candidate("lean-ctx -c git status", "lean-ctx"),
        None
    );
}

#[test]
fn rewrite_candidate_leaves_raw_escape_hatch_untouched() {
    // GH #625: the raw escape hatch the SessionStart hint teaches must not be
    // re-wrapped back into a compressing `lean-ctx -c "…"`, or the agent could
    // never actually reach raw bytes. Both spellings already start with
    // `lean-ctx `, so the rewrite hook leaves them as-is (reentrance-safe).
    assert_eq!(
        rewrite_candidate("lean-ctx raw \"git diff\"", "lean-ctx"),
        None
    );
    assert_eq!(
        rewrite_candidate("lean-ctx -c --raw \"git diff\"", "lean-ctx"),
        None
    );
}

#[test]
fn rewrite_candidate_wraps_single_command() {
    assert_eq!(
        rewrite_candidate("git status", "lean-ctx"),
        Some(expect_wrapped("git status", "lean-ctx"))
    );
}

#[test]
fn rewrite_candidate_passes_through_heredoc() {
    assert_eq!(
        rewrite_candidate(
            "git commit -m \"$(cat <<'EOF'\nfix: something\nEOF\n)\"",
            "lean-ctx"
        ),
        None
    );
}

#[test]
fn rewrite_candidate_passes_through_heredoc_compound() {
    assert_eq!(
        rewrite_candidate(
            "git add . && git commit -m \"$(cat <<EOF\nfeat: add\nEOF\n)\"",
            "lean-ctx"
        ),
        None
    );
}

#[test]
fn codex_rewrite_output_uses_native_updated_input_contract() {
    let output = codex_rewrite_output("lean-ctx -c 'git status'");
    let parsed: serde_json::Value = serde_json::from_str(&output).expect("valid hook JSON");

    assert_eq!(parsed["hookSpecificOutput"]["hookEventName"], "PreToolUse");
    assert_eq!(parsed["hookSpecificOutput"]["permissionDecision"], "allow");
    assert_eq!(
        parsed["hookSpecificOutput"]["updatedInput"]["command"],
        "lean-ctx -c 'git status'"
    );
}

/// An unchanged PreToolUse call is allowed by a successful hook with no stdout.
#[test]
fn codex_allow_output_is_empty() {
    let output = codex_allow_output();
    assert!(output.is_empty());
}

/// #809: codex_deny_output with heavily escaped content is valid JSON.
#[test]
fn codex_deny_output_with_nested_quotes_is_valid_json() {
    let complex_cmd = r#"lean-ctx raw 'cat file; printf "
---
"; sed -n "55,145p" file2'"#;
    let output = codex_deny_output(complex_cmd);
    let parsed: serde_json::Value =
        serde_json::from_str(&output).expect("codex_deny_output must be valid JSON");
    assert_eq!(parsed["hookSpecificOutput"]["permissionDecision"], "deny");
    assert!(
        parsed["hookSpecificOutput"].get("reason").is_none(),
        "Codex rejects unknown fields in hookSpecificOutput"
    );
    let reason = parsed["hookSpecificOutput"]["permissionDecisionReason"]
        .as_str()
        .unwrap_or("");
    assert!(
        reason.contains("lean-ctx raw"),
        "deny reason must contain the command: {reason}"
    );
}

#[test]
fn dual_rewrite_output_carries_claude_cursor_and_copilot_fields() {
    // #551: one JSON object must satisfy Claude (hookSpecificOutput.updatedInput),
    // Cursor (updated_input) AND Copilot CLI (top-level permissionDecision +
    // modifiedArgs). Copilot reads modifiedArgs; without it the rewrite no-ops.
    let tool_input = serde_json::json!({ "command": "cat foo.txt", "cwd": "/repo" });
    let out = build_dual_rewrite_output(Some(&tool_input), "lean-ctx read foo.txt");
    let p: serde_json::Value = serde_json::from_str(&out).expect("valid hook JSON");

    // #1264: Claude validates the deprecated top-level `decision` field when
    // present, and `"allow"` is not a valid value there. PreToolUse decisions
    // belong under `hookSpecificOutput`.
    assert!(
        p.get("decision").is_none(),
        "PreToolUse output must not emit the deprecated top-level decision"
    );
    // Copilot CLI contract (top-level).
    assert_eq!(p["permissionDecision"], "allow");
    assert_eq!(p["modifiedArgs"]["command"], "lean-ctx read foo.txt");
    assert_eq!(
        p["modifiedArgs"]["cwd"], "/repo",
        "modifiedArgs must preserve the other original args"
    );
    // Claude / CodeBuddy contract.
    assert_eq!(p["hookSpecificOutput"]["permissionDecision"], "allow");
    assert_eq!(
        p["hookSpecificOutput"]["updatedInput"]["command"],
        "lean-ctx read foo.txt"
    );
    // Cursor contract.
    assert_eq!(p["updated_input"]["command"], "lean-ctx read foo.txt");
}

#[test]
fn claude_allow_output_omits_deprecated_top_level_decision() {
    let parsed: serde_json::Value =
        serde_json::from_str(&build_dual_allow_output()).expect("valid hook JSON");

    assert!(parsed.get("decision").is_none());
    assert_eq!(parsed["hookSpecificOutput"]["permissionDecision"], "allow");
}

#[test]
fn redirect_output_carries_copilot_modified_args() {
    // #551: the read/grep redirect must also surface modifiedArgs so Copilot CLI
    // swaps in the lean-ctx temp-file path instead of reading the original.
    let tool_input = serde_json::json!({ "path": "src/main.rs" });
    let out = build_redirect_output(Some(&tool_input), "path", "/tmp/x.lctx", None);
    let p: serde_json::Value = serde_json::from_str(&out).expect("valid hook JSON");

    assert!(p.get("decision").is_none());
    assert_eq!(p["permissionDecision"], "allow");
    assert_eq!(p["modifiedArgs"]["path"], "/tmp/x.lctx");
    assert_eq!(
        p["hookSpecificOutput"]["updatedInput"]["path"],
        "/tmp/x.lctx"
    );
    assert_eq!(p["updated_input"]["path"], "/tmp/x.lctx");
}

#[test]
fn read_redirect_resolves_and_rewrites_cursor_file_path() {
    // The Cursor/Claude Read fix end-to-end: the path arrives in `file_path`, so
    // the handler must (1) resolve it via READ_PATH_FIELDS and (2) echo the SAME
    // field back in updated_input — otherwise Cursor keeps reading the original
    // file instead of the lean-ctx temp file. Before the fix the handler read
    // `path`, found nothing, and every native Read fell back to the editor.
    let tool_input = serde_json::json!({ "file_path": "/repo/src/main.rs" });

    let (field, path) = payload::resolve_path_field(Some(&tool_input), payload::READ_PATH_FIELDS)
        .expect("Cursor file_path must resolve");
    assert_eq!(field, "file_path");
    assert_eq!(path, "/repo/src/main.rs");

    let out = build_redirect_output(Some(&tool_input), field, "/tmp/x.lctx", None);
    let p: serde_json::Value = serde_json::from_str(&out).expect("valid hook JSON");
    // The redirect rewrites file_path (what Cursor reads), not the absent `path`.
    assert_eq!(p["updated_input"]["file_path"], "/tmp/x.lctx");
    assert_eq!(
        p["hookSpecificOutput"]["updatedInput"]["file_path"],
        "/tmp/x.lctx"
    );
    assert_eq!(p["modifiedArgs"]["file_path"], "/tmp/x.lctx");
    assert!(
        p["updated_input"].get("path").is_none(),
        "must not invent a `path` field Cursor never sent"
    );
}

// --- build_rewrite_compound: wrap-whole for gate-clean compounds (#589) ---
// A gate-clean compound is wrapped ENTIRELY in one `lean-ctx -c "…"`: the
// pipe/chain runs inside lean-ctx's profile-free shell (fixes the Windows
// `_lc: command not found`) and only the FINAL output is compressed (fixes the
// left-of-pipe corruption). Tricky sinks (non-allowlisted / interpreter-eval)
// are declined and left raw for the agent's shell (compat-first, no new block).

#[test]
fn compound_rewrite_and_chain() {
    let cmd = "cd src && git status && echo done";
    let result = with_test_allowlist(|| build_rewrite_compound(cmd, "lean-ctx"));
    assert_eq!(result, Some(expect_wrapped(cmd, "lean-ctx")));
}

#[test]
fn compound_rewrite_pipe() {
    let cmd = "git log --oneline | head -5";
    let result = with_test_allowlist(|| build_rewrite_compound(cmd, "lean-ctx"));
    assert_eq!(result, Some(expect_wrapped(cmd, "lean-ctx")));
}

#[test]
fn compound_rewrite_multi_pipe() {
    let cmd = "git log | grep fix | wc -l";
    let result = with_test_allowlist(|| build_rewrite_compound(cmd, "lean-ctx"));
    assert_eq!(result, Some(expect_wrapped(cmd, "lean-ctx")));
}

#[test]
fn compound_rewrite_right_only_rewritable() {
    // `cat` is a FileRead (not `-c`-wrappable alone) but `rg` makes the compound
    // rewritable; the whole thing is gate-clean, so it wraps as one unit.
    let cmd = "cat notes.txt | rg TODO";
    let result = with_test_allowlist(|| build_rewrite_compound(cmd, "lean-ctx"));
    assert_eq!(result, Some(expect_wrapped(cmd, "lean-ctx")));
}

#[test]
fn compound_rewrite_no_rewritable_segment() {
    // Neither `cd` nor `echo` is rewritable → nothing to compress → left as-is.
    let result = with_test_allowlist(|| build_rewrite_compound("cd src && echo done", "lean-ctx"));
    assert_eq!(result, None);
}

#[test]
fn compound_rewrite_multiple_rewritable() {
    let cmd = "git add . && cargo test && npm run lint";
    let result = with_test_allowlist(|| build_rewrite_compound(cmd, "lean-ctx"));
    assert_eq!(result, Some(expect_wrapped(cmd, "lean-ctx")));
}

#[test]
fn compound_rewrite_semicolons() {
    let cmd = "git add .; git commit -m 'fix'";
    let result = with_test_allowlist(|| build_rewrite_compound(cmd, "lean-ctx"));
    assert_eq!(result, Some(expect_wrapped(cmd, "lean-ctx")));
}

#[test]
fn compound_rewrite_or_chain() {
    let cmd = "git pull || echo failed";
    let result = with_test_allowlist(|| build_rewrite_compound(cmd, "lean-ctx"));
    assert_eq!(result, Some(expect_wrapped(cmd, "lean-ctx")));
}

#[test]
fn compound_skips_already_rewritten() {
    // A segment that is already a lean-ctx call must not be nested inside another
    // `lean-ctx -c "…"`; the whole compound is left untouched.
    let result = with_test_allowlist(|| {
        build_rewrite_compound("lean-ctx -c git status && git diff", "lean-ctx")
    });
    assert_eq!(result, None);
}

#[test]
fn compound_tricky_interpreter_sink_left_raw() {
    // Piping into `python3 -c` (not allowlisted) must NOT be wrapped — wrapping
    // would newly subject the interpreter to the gate and block a command the
    // agent's shell ran fine before (#589, compat-first).
    let result = with_test_allowlist(|| {
        build_rewrite_compound("git log | python3 -c 'print(1)'", "lean-ctx")
    });
    assert_eq!(result, None);
}

#[test]
fn compound_tricky_non_allowlisted_sink_left_raw() {
    // `kubectl` is rewritable but deliberately excluded from the defaults; the
    // compound therefore fails the gate and stays raw rather than being blocked.
    let result =
        with_test_allowlist(|| build_rewrite_compound("git log | kubectl apply -f -", "lean-ctx"));
    assert_eq!(result, None);
}

#[test]
fn compound_tricky_chain_sink_left_raw() {
    let result = with_test_allowlist(|| {
        build_rewrite_compound("cargo test && python3 -c 'print(1)'", "lean-ctx")
    });
    assert_eq!(result, None);
}

#[test]
fn single_command_not_compound() {
    let result = with_test_allowlist(|| build_rewrite_compound("git status", "lean-ctx"));
    assert_eq!(result, None);
}

#[test]
fn rewrite_candidate_wraps_clean_compound() {
    // End-to-end: a gate-clean pipeline routes through the compound handler and
    // is wrapped whole (never split, never falling to the single-command path).
    let cmd = "git log | head -5";
    let result = with_test_allowlist(|| rewrite_candidate(cmd, "lean-ctx"));
    assert_eq!(result, Some(expect_wrapped(cmd, "lean-ctx")));
}

#[test]
fn rewrite_candidate_leaves_tricky_compound_untouched() {
    // End-to-end: a tricky compound must not be re-wrapped by the single-command
    // `is_rewritable` fallback after the compound handler declines it (#589).
    let result =
        with_test_allowlist(|| rewrite_candidate("git log | python3 -c 'print(1)'", "lean-ctx"));
    assert_eq!(result, None);
}