//! End-to-end integration tests for aether-recall-mcp. //! //! Spawns the actual binary, talks JSON-RPC over stdio, verifies //! the round-trip and the canonical-form abstraction. use serde_json::json; use std::io::{Read, Write}; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; fn binary_path() -> PathBuf { PathBuf::from(env!("CARGO_BIN_EXE_aether-recall-mcp")) } struct Mcp { child: Child, } impl Mcp { fn spawn(data_dir: &PathBuf) -> Self { let mut cmd = Command::new(binary_path()); cmd.env("AETHER_RECALL_DIR", data_dir) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); let child = cmd.spawn().expect("spawn aether-recall-mcp"); Self { child } } fn send(&mut self, req: serde_json::Value) -> serde_json::Value { let stdin = self.child.stdin.as_mut().expect("stdin"); let s = serde_json::to_string(&req).unwrap(); writeln!(stdin, "{}", s).unwrap(); stdin.flush().unwrap(); let stdout = self.child.stdout.as_mut().expect("stdout"); 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 Mcp { fn drop(&mut self) { let _ = self.child.kill(); } } #[test] fn full_lifecycle() { let dir = tempdir_in("/tmp"); let mut m = Mcp::spawn(&dir); // 1. initialize let r = m.send(json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})); assert_eq!(r["result"]["serverInfo"]["name"], "aether-recall"); // 2. cold lookup → miss let r = m.send(json!({ "jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"lookup_resolution","arguments":{"failure":"error[E0308]: expected i32, found &str"}} })); let content = r["result"]["content"][0]["text"].as_str().unwrap(); assert!(content.contains("\"hit\":false"), "got: {content}"); // 3. record_outcome let r = m.send(json!({ "jsonrpc":"2.0","id":3,"method":"tools/call", "params":{"name":"record_outcome","arguments":{"failure":"error[E0308]: expected i32, found &str","resolution":"use .to_string()","worked":true}} })); let content = r["result"]["content"][0]["text"].as_str().unwrap(); assert!(content.contains("\"recorded\":true")); // 4. warm lookup → hit let r = m.send(json!({ "jsonrpc":"2.0","id":4,"method":"tools/call", "params":{"name":"lookup_resolution","arguments":{"failure":"error[E0308]: expected i32, found &str"}} })); let content = r["result"]["content"][0]["text"].as_str().unwrap(); assert!(content.contains("\"hit\":true"), "got: {content}"); assert!(content.contains("use .to_string()"), "got: {content}"); // 5. path-equivalent surface form → hit (canonical abstraction) let r = m.send(json!({ "jsonrpc":"2.0","id":5,"method":"tools/call", "params":{"name":"lookup_resolution","arguments":{"failure":"error[E0308]: expected i32, found &str\n --> src/foo.rs:42:5"}} })); let content = r["result"]["content"][0]["text"].as_str().unwrap(); assert!(content.contains("\"hit\":true"), "got: {content}"); // 6. memory_stats: 3 lookups, 2 hits, 1 miss let r = m.send(json!({ "jsonrpc":"2.0","id":6,"method":"tools/call", "params":{"name":"memory_stats","arguments":{}} })); let content = r["result"]["content"][0]["text"].as_str().unwrap(); assert!(content.contains("\"lookups\":3")); assert!(content.contains("\"hits\":2")); assert!(content.contains("\"misses\":1")); assert!(content.contains("\"hit_rate\":0.6")); // 7. compact succeeds let r = m.send(json!({ "jsonrpc":"2.0","id":7,"method":"tools/call", "params":{"name":"compact","arguments":{}} })); let content = r["result"]["content"][0]["text"].as_str().unwrap(); assert!(content.contains("\"compacted\":true")); // 8. error: empty failure let r = m.send(json!({ "jsonrpc":"2.0","id":8,"method":"tools/call", "params":{"name":"lookup_resolution","arguments":{"failure":""}} })); assert_eq!(r["result"]["isError"], true); let content = r["result"]["content"][0]["text"].as_str().unwrap(); assert!(content.contains("\"tool\":\"lookup_resolution\"")); } fn tempdir_in(parent: &str) -> PathBuf { use std::time::{SystemTime, UNIX_EPOCH}; let stamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); let p = PathBuf::from(parent).join(format!("aether-recall-mcp-it-{}", stamp)); std::fs::create_dir_all(&p).unwrap(); p }