//! Cross-binary integration test. //! //! Spawns `aether-recall-mcp` and `aether-ask` against a shared data //! directory. Issues a record via the MCP transport, then a query via //! the HTTP transport. Asserts the second process sees what the //! first wrote. If this fails, the system is not an integrated whole. use serde_json::json; use std::io::{Read, Write}; use std::net::TcpStream; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// Find the per-target binaries. We resolve via the per-target /// `CARGO_BIN_EXE_` for our own crate's bin, and via the /// workspace target/release path for sibling crates' bins. fn bin_mcp() -> PathBuf { let env = format!("CARGO_BIN_EXE_{}", "aether-recall-mcp"); PathBuf::from(std::env::var(&env).expect("CARGO_BIN_EXE_aether-recall-mcp")) } fn bin_ask() -> PathBuf { // Sibling-crate binaries are not exposed via CARGO_BIN_EXE_ in this // crate's tests; they sit in workspace target/{debug,release}. // We use the absolute workspace path because the test's CWD isn't // guaranteed to be the workspace root. // ../target/debug/aether-ask from this crate's tests/ dir at // crates/aether-recall-mcp/tests/ is too fragile; instead use CARGO_MANIFEST_DIR. // But we don't know which profile. Probe both. let manifest = std::env::var("CARGO_MANIFEST_DIR") .expect("CARGO_MANIFEST_DIR"); let crate_dir = PathBuf::from(manifest); // crates/aether-recall-mcp let workspace_dir = crate_dir .parent().and_then(|p| p.parent()).expect("workspace dir") .to_path_buf(); for profile in ["release", "debug"] { let p = workspace_dir.join("target").join(profile).join("aether-ask"); if p.exists() { return p; } } panic!("aether-ask binary not found under {workspace_dir:?}/target/{{release,debug}}"); } struct McpClient { child: Child, } impl McpClient { fn spawn(data_dir: &Path) -> Self { let mut cmd = Command::new(bin_mcp()); cmd.env("AETHER_RECALL_DIR", data_dir) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()); Self { child: cmd.spawn().expect("spawn mcp") } } fn send(&mut self, req: serde_json::Value) -> serde_json::Value { let stdin = self.child.stdin.as_mut().unwrap(); writeln!(stdin, "{}", serde_json::to_string(&req).unwrap()).unwrap(); stdin.flush().unwrap(); let stdout = self.child.stdout.as_mut().unwrap(); let mut buf = Vec::new(); let mut byte = [0u8; 1]; while stdout.read(&mut byte).unwrap() > 0 { buf.push(byte[0]); if byte[0] == b'\n' { break; } } serde_json::from_slice(&buf).unwrap() } } impl Drop for McpClient { fn drop(&mut self) { let _ = self.child.kill(); } } #[test] fn mcp_record_then_mcp_lookup_returns_hit() { let dir = tmp(); let mut m = McpClient::spawn(&dir); m.send(json!({ "jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"record_outcome","arguments":{ "failure":"E0308 canonical test","resolution":"use .to_string()","worked":true }} })); let r = m.send(json!({ "jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"lookup_resolution","arguments":{"failure":"E0308 canonical test"}} })); let content = r["result"]["content"][0]["text"].as_str().unwrap(); assert!(content.contains("\"hit\":true"), "got: {content}"); assert!(content.contains("use .to_string()")); } #[test] fn ask_returns_value_after_mcp_recorded() { let dir = tmp(); // 1. MCP records a fact in process A { let mut m = McpClient::spawn(&dir); m.send(json!({ "jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"record_outcome","arguments":{ "failure":"cross-binary integration", "resolution":"verified by separate processes", "worked":true }} })); } // process A drops → killed // 2. ASK spins up in process B, hits the SAME data dir, asserts hit let port = ephemeral_port(); let mut ask_child = Command::new(bin_ask()) .env("AETHER_RECALL_DIR", &dir) .env("AETHER_ASK_PORT", port.to_string()) .env("OLLAMA_PORT", "1") // unreachable → fast neural-fail path .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("spawn ask"); let addr = format!("127.0.0.1:{}", port); for _ in 0..60 { if TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(100)).is_ok() { break; } std::thread::sleep(Duration::from_millis(50)); } let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap(); s.set_read_timeout(Some(Duration::from_secs(3))).ok(); let body = json!({"question":"cross-binary integration"}).to_string(); let req = format!( "POST /ask HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body ); s.write_all(req.as_bytes()).unwrap(); let mut buf = Vec::new(); s.read_to_end(&mut buf).ok(); let text = String::from_utf8_lossy(&buf).to_string(); let (_head, body) = text.split_once("\r\n\r\n").unwrap_or(("", &text)); let v: serde_json::Value = serde_json::from_str(body).expect("json"); let _ = ask_child.kill(); assert_eq!(v["source"].as_str(), Some("deterministic"), "ASK must hit the cached MCP record across processes; got: {v:?}"); assert_eq!(v["answer"].as_str(), Some("verified by separate processes")); } fn tmp() -> PathBuf { let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); let p = std::env::temp_dir().join(format!("cross-bin-it-{stamp}")); std::fs::create_dir_all(&p).unwrap(); p } fn ephemeral_port() -> u16 { let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); 33_000 + ((stamp % 1000) as u16) }