weavatrix-git 0.3.1

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
#![cfg(feature = "mcp")]

use std::{
    fmt::Write as _,
    fs,
    io::{Read, Write},
    path::{Path, PathBuf},
    process::{Command, Stdio},
    time::{SystemTime, UNIX_EPOCH},
};

fn server(repository: &Path) -> std::process::Child {
    Command::new(env!("CARGO_BIN_EXE_weavatrix-git-mcp"))
        .arg("--repository")
        .arg(repository)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("start MCP server")
}

fn git(repository: &Path, args: &[&str]) -> std::process::Output {
    let output = Command::new("git")
        .args(args)
        .current_dir(repository)
        .output()
        .expect("run Git fixture command");
    assert!(output.status.success(), "{output:?}");
    output
}

fn fixture() -> (PathBuf, String) {
    let nonce = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system clock")
        .as_nanos();
    let repository =
        std::env::temp_dir().join(format!("weavatrix-git-mcp-{}-{nonce}", std::process::id()));
    fs::create_dir_all(&repository).expect("create fixture directory");
    git(&repository, &["init", "--initial-branch=main"]);
    fs::write(repository.join("alpha.txt"), b"alpha\n").expect("write first fixture");
    git(&repository, &["add", "alpha.txt"]);
    git(
        &repository,
        &[
            "-c",
            "user.name=Weavatrix",
            "-c",
            "user.email=test@example.com",
            "commit",
            "-m",
            "first",
        ],
    );
    let old = String::from_utf8(git(&repository, &["rev-parse", "HEAD"]).stdout)
        .expect("UTF-8 object ID")
        .trim()
        .to_owned();
    fs::write(repository.join("beta.txt"), b"beta\n").expect("write second fixture");
    fs::write(repository.join("gamma.txt"), b"gamma\n").expect("write third fixture");
    git(&repository, &["add", "beta.txt", "gamma.txt"]);
    git(
        &repository,
        &[
            "-c",
            "user.name=Weavatrix",
            "-c",
            "user.email=test@example.com",
            "commit",
            "-m",
            "second",
        ],
    );
    (repository, old)
}

#[test]
fn subprocess_serves_a_fragmented_read_only_session() {
    let (repository, old) = fixture();
    let mut child = server(&repository);
    let mut input = concat!(
        "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":",
        "{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{},",
        "\"clientInfo\":{\"name\":\"test\",\"version\":\"1\"}}}\n",
        "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\",\"params\":{}}\n",
        "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}\n",
        "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":",
        "{\"name\":\"git_head\",\"arguments\":{}}}\n"
    )
    .to_owned();
    input.push_str(
        "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":\
         {\"name\":\"git_history\",\"arguments\":{\"limit\":2}}}\n",
    );
    input.push_str(
        "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":\
         {\"name\":\"git_status\",\"arguments\":{\"limit\":2}}}\n",
    );
    input.push_str(
        "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"tools/call\",\"params\":\
         {\"name\":\"git_snapshot\",\"arguments\":{\"limit\":2}}}\n",
    );
    writeln!(
        input,
        "{{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/call\",\"params\":\
         {{\"name\":\"git_diff\",\"arguments\":{{\"old\":\"{old}\",\"limit\":2}}}}}}"
    )
    .expect("format diff request");
    input.push_str(
        "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"tools/call\",\"params\":\
         {\"name\":\"git_history\",\"arguments\":{\"limit\":0}}}\n",
    );
    let mut stdin = child.stdin.take().expect("piped stdin");
    for chunk in input.as_bytes().chunks(7) {
        stdin.write_all(chunk).expect("write fragmented request");
    }
    drop(stdin);

    let output = child.wait_with_output().expect("wait for MCP server");
    fs::remove_dir_all(&repository).expect("remove fixture");
    assert!(output.status.success(), "{output:?}");
    assert!(output.stderr.is_empty(), "{output:?}");
    let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
    let lines = stdout.lines().collect::<Vec<_>>();
    assert_eq!(lines.len(), 8, "{stdout}");
    for id in 1..=8 {
        assert!(
            lines
                .iter()
                .any(|line| line.contains(&format!("\"id\":{id}"))),
            "{stdout}"
        );
    }
    assert!(stdout.contains("\"name\":\"git_head\""), "{stdout}");
    assert!(stdout.contains("\"name\":\"git_history\""), "{stdout}");
    assert!(stdout.contains("\"name\":\"git_diff\""), "{stdout}");
    assert!(stdout.contains("\"name\":\"git_status\""), "{stdout}");
    assert!(stdout.contains("\"name\":\"git_snapshot\""), "{stdout}");
    assert!(stdout.contains("\"structuredContent\""), "{stdout}");
    assert!(stdout.contains("\"commits\""), "{stdout}");
    assert!(stdout.contains("\"changes\""), "{stdout}");
    assert!(stdout.contains("\"entries\""), "{stdout}");
    assert!(stdout.contains("\"nextCursor\":2"), "{stdout}");
    assert!(stdout.contains("\"isError\":true"), "{stdout}");
}

#[test]
fn subprocess_rejects_an_oversized_frame_and_recovers() {
    let mut child = server(Path::new(env!("CARGO_MANIFEST_DIR")));
    let mut stdin = child.stdin.take().expect("piped stdin");
    stdin
        .write_all(&vec![b'x'; weavatrix_git::mcp::MAX_REQUEST_BYTES + 1])
        .expect("write oversized frame");
    stdin.write_all(b"\n").expect("finish oversized frame");
    stdin
        .write_all(
            b"{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"initialize\",\"params\":\
              {\"protocolVersion\":\"2025-11-25\",\"capabilities\":{},\
              \"clientInfo\":{\"name\":\"test\",\"version\":\"1\"}}}\n",
        )
        .expect("write valid request");
    drop(stdin);

    let mut stdout = String::new();
    child
        .stdout
        .take()
        .expect("piped stdout")
        .read_to_string(&mut stdout)
        .expect("read stdout");
    let status = child.wait().expect("wait for MCP server");
    assert!(status.success(), "{stdout}");
    assert_eq!(stdout.lines().count(), 2, "{stdout}");
    assert!(
        stdout.contains("request exceeds max_request_bytes"),
        "{stdout}"
    );
    assert!(stdout.contains("\"id\":7"), "{stdout}");
}