#![cfg(feature = "mem-repo")]
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use tempfile::TempDir;
const WORKSPACE_TOML_BODY: &str = "format = \"memstead-git-branch-2\"\n\n\
[persistence_adapter]\nname = \"file-two-layer\"\n";
const MOUNTS_JSON_BODY: &str = r#"{ "format": "memstead-mounts-3", "mounts": [] }"#;
fn memstead_mcp_bin() -> &'static str {
env!("CARGO_BIN_EXE_memstead-mcp")
}
fn seed_workspace(root: &std::path::Path) {
let memstead = root.join(".memstead");
std::fs::create_dir_all(memstead.join("state")).unwrap();
std::fs::write(memstead.join("workspace.toml"), WORKSPACE_TOML_BODY).unwrap();
std::fs::write(memstead.join("state").join("mounts.json"), MOUNTS_JSON_BODY).unwrap();
}
fn initialize_request() -> String {
serde_json::to_string(&serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "boot-smoke-test", "version": "0" }
}
}))
.unwrap()
}
fn read_response_with_timeout(
stdout: std::process::ChildStdout,
want_id: i64,
timeout: Duration,
) -> Option<serde_json::Value> {
let mut reader = BufReader::new(stdout);
let deadline = Instant::now() + timeout;
let mut line = String::new();
loop {
if Instant::now() >= deadline {
return None;
}
line.clear();
match reader.read_line(&mut line) {
Ok(0) => return None,
Ok(_) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let value: serde_json::Value = match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(_) => continue,
};
if value.get("id").and_then(|v| v.as_i64()) == Some(want_id) {
return Some(value);
}
}
Err(_) => return None,
}
}
}
fn assert_initialize_envelope(response: &serde_json::Value) {
let result = response
.get("result")
.expect("initialize response must carry a `result` field");
assert!(
result.get("protocolVersion").is_some(),
"initialize result missing `protocolVersion`: {response}"
);
assert!(
result.get("capabilities").is_some(),
"initialize result missing `capabilities`: {response}"
);
let server_info = result
.get("serverInfo")
.expect("initialize result missing `serverInfo`");
assert!(
server_info.get("name").is_some(),
"serverInfo missing `name`: {response}"
);
}
#[test]
fn full_binary_boots_against_new_layout_workspace() {
let tmp = TempDir::new().unwrap();
seed_workspace(tmp.path());
memstead_git_branch::test_support::init_real_mem_repo(tmp.path(), &[]);
let mut child = Command::new(memstead_mcp_bin())
.current_dir(tmp.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn memstead-mcp (full) — confirm the binary built before running tests");
let mut stdin = child.stdin.take().expect("child stdin");
writeln!(stdin, "{}", initialize_request()).expect("write initialize");
stdin.flush().expect("flush initialize");
drop(stdin);
let stdout = child.stdout.take().expect("child stdout");
let response = read_response_with_timeout(stdout, 1, Duration::from_secs(15))
.expect("initialize response within 15s — binary did not boot or did not reply");
assert_initialize_envelope(&response);
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn full_binary_boot_failure_prints_typed_code_and_shared_message() {
let tmp = TempDir::new().unwrap();
seed_workspace(tmp.path());
memstead_git_branch::test_support::init_real_mem_repo(tmp.path(), &[]);
std::fs::write(
tmp.path()
.join(".memstead")
.join("state")
.join("mounts.json"),
"this is not json {",
)
.unwrap();
let ws = tmp.path().canonicalize().unwrap();
let boot_err = memstead_git_branch::workspace_store::engine_from_workspace_root(&ws)
.expect_err("fixture must fail the in-process boot");
assert_eq!(boot_err.code(), "WORKSPACE_STORE_PARSE");
let expected = format!(
"memstead-mcp: ERROR [{}]: {}",
boot_err.code(),
boot_err.surface_message(&ws)
);
let output = Command::new(memstead_mcp_bin())
.current_dir(tmp.path())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("spawn memstead-mcp (full) — confirm the binary built before running tests");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(&expected),
"stderr must carry the typed boot diagnostic\nexpected line: {expected}\n--- stderr ---\n{stderr}"
);
}
fn boot_stderr(root: &std::path::Path) -> String {
let output = Command::new(memstead_mcp_bin())
.current_dir(root)
.env("RUST_LOG", "info")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("spawn memstead-mcp (full) — confirm the binary built before running tests");
String::from_utf8_lossy(&output.stderr).into_owned()
}
#[test]
fn full_binary_boot_line_names_the_shape_it_opened() {
let repo_ws = TempDir::new().unwrap();
seed_workspace(repo_ws.path());
memstead_git_branch::test_support::init_real_mem_repo(repo_ws.path(), &[]);
let stderr = boot_stderr(repo_ws.path());
assert!(
stderr.contains("boot: mem-repo workspace at"),
"mem-repo workspace must boot as mem-repo\n--- stderr ---\n{stderr}"
);
let fs_ws = TempDir::new().unwrap();
seed_workspace(fs_ws.path());
let stderr = boot_stderr(fs_ws.path());
assert!(
stderr.contains("boot: filesystem-mem workspace at"),
"filesystem-mem workspace must boot as filesystem-mem\n--- stderr ---\n{stderr}"
);
assert!(
!stderr.contains("mem-repo workspace at"),
"the mem-repo spelling must never name a filesystem-mem workspace\n--- stderr ---\n{stderr}"
);
}
fn tools_call_request(id: i64, name: &str) -> String {
serde_json::to_string(&serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"method": "tools/call",
"params": { "name": name, "arguments": {} }
}))
.unwrap()
}
#[test]
fn full_binary_serves_partially_broken_workspace() {
let tmp = TempDir::new().unwrap();
seed_workspace(tmp.path());
memstead_git_branch::test_support::init_real_mem_repo(tmp.path(), &[]);
std::fs::create_dir_all(tmp.path().join("plenum")).unwrap();
std::fs::write(
tmp.path().join(".memstead").join("state").join("mounts.json"),
r#"{ "format": "memstead-mounts-3", "mounts": [
{ "mem": "plenum", "schema": "ghost@1.0.0", "storage": { "type": "folder", "path": "plenum" }, "capability": "write", "lifecycle": "eager", "cross_linkable": true }
] }"#,
)
.unwrap();
let mut child = Command::new(memstead_mcp_bin())
.current_dir(tmp.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn memstead-mcp (full)");
let mut stdin = child.stdin.take().expect("child stdin");
writeln!(stdin, "{}", initialize_request()).unwrap();
stdin.flush().unwrap();
drop(stdin);
let stdout = child.stdout.take().expect("child stdout");
let response = read_response_with_timeout(stdout, 1, Duration::from_secs(15))
.expect("server must start and answer initialize despite the broken mem");
assert_initialize_envelope(&response);
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn full_binary_serves_boot_diagnosis_on_unbootable_workspace() {
let tmp = TempDir::new().unwrap();
seed_workspace(tmp.path());
memstead_git_branch::test_support::init_real_mem_repo(tmp.path(), &[]);
std::fs::write(
tmp.path()
.join(".memstead")
.join("state")
.join("mounts.json"),
"this is not json {",
)
.unwrap();
let mut child = Command::new(memstead_mcp_bin())
.current_dir(tmp.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn memstead-mcp (full)");
let mut stdin = child.stdin.take().expect("child stdin");
writeln!(stdin, "{}", initialize_request()).unwrap();
writeln!(
stdin,
"{}",
serde_json::to_string(&serde_json::json!({
"jsonrpc": "2.0", "method": "notifications/initialized"
}))
.unwrap()
)
.unwrap();
writeln!(stdin, "{}", tools_call_request(2, "memstead_health")).unwrap();
stdin.flush().unwrap();
drop(stdin);
let stdout = child.stdout.take().expect("child stdout");
let response = read_response_with_timeout(stdout, 2, Duration::from_secs(15))
.expect("diagnostic shell must answer memstead_health");
let text = serde_json::to_string(&response).unwrap();
assert!(
text.contains("boot_diagnosis") && text.contains("WORKSPACE_STORE_PARSE"),
"health must carry the typed boot diagnosis: {text}"
);
let _ = child.kill();
let _ = child.wait();
}