rag-rat 0.9.0

CLI and MCP entrypoint for indexing repositories into local source, graph, history, and memory evidence.
//! Regression (#219): an inherited `GIT_DIR` / `GIT_WORK_TREE` (e.g. a tool operating in a linked
//! worktree — Claude Code, an IDE) must NOT hijack the index's git resolution.
//!
//! With the env honored unconditionally, `index --worktree` resolved BOTH the base repo and the
//! linked repo to the single env-specified worktree (ignoring their path arguments), so the
//! base↔linked overlay delta collapsed to empty and the real overlay rows were pruned/tombstoned —
//! the file-reported worktree flip-flop. `discover_repo` now resolves from the configured path
//! first, falling back to env overrides only when no repo is found upward. This test runs the CLI
//! in a subprocess (env-isolated) with the hijacking env set and asserts the overlay survives.

use std::fs;
use std::path::Path;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};

use rag_rat_core::language::Language;
use rag_rat_core::{Config, IndexDatabase};

static TEMP: AtomicU64 = AtomicU64::new(0);

fn git(dir: &Path, args: &[&str]) {
    let out = Command::new("git").arg("-C").arg(dir).args(args).output().unwrap();
    assert!(out.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&out.stderr));
}

#[test]
fn worktree_overlay_ignores_inherited_git_dir_env() {
    let base = std::env::temp_dir().join(format!(
        "rag-rat-wtenv-{}-{}",
        std::process::id(),
        TEMP.fetch_add(1, Ordering::Relaxed)
    ));
    let _ = fs::remove_dir_all(&base);
    let main = base.join("main");
    fs::create_dir_all(main.join("src")).unwrap();
    fs::write(main.join("src/keep.rs"), "pub fn keep_fn() {}\n").unwrap();
    fs::write(main.join("src/reinf.rs"), "pub fn classify_seg() {}\n").unwrap();
    // Nest the worktree under config.root and gitignore it (the perverse-but-real held layout).
    fs::write(main.join(".gitignore"), "/wt/\n").unwrap();
    fs::write(
        main.join("rag-rat.toml"),
        "[index]\nroot = \".\"\ndatabase = \".rag-rat/index.sqlite\"\n\n[target_bindings]\nrust = \
         [\"src\"]\n",
    )
    .unwrap();
    git(&main, &["init", "-q"]);
    git(&main, &["config", "user.email", "t@e.com"]);
    git(&main, &["config", "user.name", "t"]);
    git(&main, &["add", "."]);
    git(&main, &["commit", "-q", "-m", "C1 has reinf"]);

    // Linked worktree forked at C1 (HAS reinf.rs), nested + gitignored.
    let wt = main.join("wt");
    git(&main, &["worktree", "add", "-q", "-b", "feat", wt.to_str().unwrap()]);

    // Main REMOVES reinf.rs at C2; the branch keeps it — so reinf.rs lives ONLY in the overlay.
    fs::remove_file(main.join("src/reinf.rs")).unwrap();
    git(&main, &["add", "."]);
    git(&main, &["commit", "-q", "-m", "C2 removed reinf"]);

    let config_path = main.join("rag-rat.toml");
    let config = Config::load(&config_path).unwrap();
    IndexDatabase::rebuild(&config).unwrap();

    // Run `index --worktree` with the WORKTREE's GIT_DIR/GIT_WORK_TREE in the env — the exact shape
    // a worktree shell exports, which used to hijack resolution and prune the overlay.
    let git_dir = main.join(".git/worktrees/feat");
    let binary = env!("CARGO_BIN_EXE_rag-rat");
    let out = Command::new(binary)
        .arg("--config")
        .arg(&config_path)
        .args(["index", "--worktree"])
        .arg(&wt)
        .env("RAG_RAT_NO_WATCH", "1")
        .env("GIT_DIR", &git_dir)
        .env("GIT_WORK_TREE", &wt)
        .output()
        .unwrap();
    assert!(
        out.status.success(),
        "index --worktree failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );

    // The overlay must serve reinf.rs READABLE in the worktree scope — the inherited GIT_DIR must
    // not have collapsed the delta and pruned/tombstoned the row.
    let mut db = IndexDatabase::open(&config.database).unwrap();
    db.use_worktree_scope(&main, Some(&wt)).unwrap();
    let hits = db.symbols("classify_seg", Some(Language::Rust), 10).unwrap();
    assert!(
        !hits.is_empty(),
        "an inherited worktree GIT_DIR hijacked resolution and pruned the overlay (reinf.rs \
         missing)"
    );

    let _ = fs::remove_dir_all(&base);
}