#![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}");
}