use std::process::Command;
use mnemo::{KdfParams, Memory, MemoryType, Mnemo, MnemoConfig};
use tempfile::tempdir;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_mnemo")
}
fn fast_cfg(dimensions: usize) -> MnemoConfig {
MnemoConfig {
dimensions,
kdf: KdfParams::fast(),
..Default::default()
}
}
fn seed_db() -> (tempfile::TempDir, std::path::PathBuf, &'static str, String) {
let dir = tempdir().unwrap();
let path = dir.path().join("smoke.mnemo");
let pp = "smoke-pw";
let mut db = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
let semantic_id = db
.remember(
Memory::new(
"the user prefers tea",
MemoryType::Semantic,
vec![1.0, 0.0, 0.0, 0.0],
)
.with_agent("assistant")
.with_importance(0.9),
)
.unwrap();
db.remember(
Memory::new(
"ran the deploy script",
MemoryType::Episodic,
vec![0.0, 1.0, 0.0, 0.0],
)
.with_agent("assistant")
.with_importance(0.4),
)
.unwrap();
db.remember(
Memory::new(
"escalate via pager",
MemoryType::Procedural,
vec![0.0, 0.0, 1.0, 0.0],
)
.with_agent("ops")
.with_importance(0.6),
)
.unwrap();
db.flush().unwrap();
db.close().unwrap();
(dir, path, pp, semantic_id.to_string())
}
fn run_cli(args: &[&str], pp: &str) -> (std::process::ExitStatus, String, String) {
let out = Command::new(bin())
.args(args)
.env("MNEMO_PASSPHRASE", pp)
.output()
.expect("failed to invoke mnemo binary");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
(out.status, stdout, stderr)
}
#[test]
fn list_table_shows_three_memories() {
let (_dir, path, pp, _) = seed_db();
let (status, stdout, stderr) =
run_cli(&["list", path.to_str().unwrap(), "--format", "table"], pp);
assert!(status.success(), "list failed: {stderr}");
assert!(
stdout.contains("showing 3 of 3 memories"),
"header missing: {stdout}"
);
assert!(
stdout.contains("the user prefers tea"),
"content missing: {stdout}"
);
assert!(stdout.contains("[semantic]"), "type tag missing: {stdout}");
}
#[test]
fn list_filters_by_type_and_agent() {
let (_dir, path, pp, _) = seed_db();
let (status, stdout, _) = run_cli(
&[
"list",
path.to_str().unwrap(),
"--type",
"procedural",
"--agent",
"ops",
],
pp,
);
assert!(status.success());
assert!(stdout.contains("escalate via pager"));
assert!(!stdout.contains("prefers tea"));
assert!(!stdout.contains("deploy script"));
}
#[test]
fn list_json_emits_total_and_count() {
let (_dir, path, pp, _) = seed_db();
let (status, stdout, _) = run_cli(
&[
"list",
path.to_str().unwrap(),
"--format",
"json",
"--limit",
"2",
],
pp,
);
assert!(status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).expect("json output");
assert_eq!(v["total"], 3);
assert_eq!(v["count"], 2);
let memories = v["memories"].as_array().expect("memories array");
assert_eq!(memories.len(), 2);
assert!(
memories[0].get("vector").is_none(),
"vector should be omitted"
);
}
#[test]
fn list_jsonl_with_vector_includes_embedding() {
let (_dir, path, pp, _) = seed_db();
let (status, stdout, _) = run_cli(
&[
"list",
path.to_str().unwrap(),
"--format",
"jsonl",
"--vector",
],
pp,
);
assert!(status.success());
let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
assert_eq!(lines.len(), 3);
for line in &lines {
let v: serde_json::Value = serde_json::from_str(line).expect("jsonl line");
assert!(v["vector"].as_array().is_some(), "missing vector: {line}");
}
}
#[test]
fn get_by_id_table_and_json() {
let (_dir, path, pp, semantic_id) = seed_db();
let (status, stdout, _) = run_cli(
&["get", path.to_str().unwrap(), &semantic_id, "--verbose"],
pp,
);
assert!(status.success());
assert!(
stdout.contains(&semantic_id),
"id missing in table output: {stdout}"
);
assert!(stdout.contains("the user prefers tea"));
assert!(stdout.contains("imp=0.90"));
assert!(stdout.contains("scope"));
let (status, stdout, _) = run_cli(
&[
"get",
path.to_str().unwrap(),
&semantic_id,
"--format",
"json",
],
pp,
);
assert!(status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).expect("json from get");
assert_eq!(v["content"], "the user prefers tea");
assert_eq!(v["memory_type"], "Semantic");
assert!(
v.get("vector").is_none(),
"vector should be omitted by default"
);
}
#[test]
fn get_missing_id_fails_cleanly() {
let (_dir, path, pp, _) = seed_db();
let (status, _stdout, stderr) = run_cli(
&["get", path.to_str().unwrap(), "01ARZ3NDEKTSV4RRFFQ69G5FAV"],
pp,
);
assert!(!status.success(), "missing id should exit non-zero");
assert!(
stderr.contains("not found"),
"expected NotFound message, got: {stderr}"
);
}
#[test]
fn recall_returns_query_nearest_first() {
let (_dir, path, pp, _) = seed_db();
let (status, stdout, _) = run_cli(
&[
"recall",
path.to_str().unwrap(),
"--query",
"0.9,0.1,0.0,0.0",
"--top-k",
"2",
],
pp,
);
assert!(status.success());
let first_line = stdout.lines().next().unwrap_or("");
assert!(
first_line.contains("the user prefers tea"),
"expected tea first: {stdout}"
);
assert!(first_line.starts_with("score="));
}
#[test]
fn recall_json_carries_score_and_similarity() {
let (_dir, path, pp, _) = seed_db();
let (status, stdout, _) = run_cli(
&[
"recall",
path.to_str().unwrap(),
"--query",
"0.9,0.1,0.0,0.0",
"--top-k",
"1",
"--format",
"json",
],
pp,
);
assert!(status.success());
let v: serde_json::Value = serde_json::from_str(&stdout).expect("json from recall");
assert_eq!(v["count"], 1);
let hits = v["hits"].as_array().expect("hits array");
assert!(hits[0]["score"].is_number());
assert!(hits[0]["similarity"].is_number());
assert_eq!(hits[0]["memory"]["content"], "the user prefers tea");
}
#[test]
fn recall_filters_by_type_excludes_other_kinds() {
let (_dir, path, pp, _) = seed_db();
let (status, stdout, _) = run_cli(
&[
"recall",
path.to_str().unwrap(),
"--query",
"1.0,0.0,0.0,0.0",
"--type",
"procedural",
],
pp,
);
assert!(status.success());
assert!(stdout.contains("escalate via pager"));
assert!(!stdout.contains("prefers tea"));
assert!(!stdout.contains("deploy script"));
}
#[test]
fn recall_query_file_json_works() {
let (dir, path, pp, _) = seed_db();
let qf = dir.path().join("q.json");
std::fs::write(&qf, "[1.0, 0.0, 0.0, 0.0]").unwrap();
let (status, stdout, stderr) = run_cli(
&[
"recall",
path.to_str().unwrap(),
"--query-file",
qf.to_str().unwrap(),
"--top-k",
"1",
],
pp,
);
assert!(status.success(), "recall failed: {stderr}");
assert!(stdout.contains("the user prefers tea"), "got: {stdout}");
}
#[test]
fn recall_query_file_whitespace_works() {
let (dir, path, pp, _) = seed_db();
let qf = dir.path().join("q.txt");
std::fs::write(&qf, "1.0\n0.0\n0.0\n0.0\n").unwrap();
let (status, stdout, stderr) = run_cli(
&[
"recall",
path.to_str().unwrap(),
"--query-file",
qf.to_str().unwrap(),
"--top-k",
"1",
],
pp,
);
assert!(status.success(), "recall failed: {stderr}");
assert!(stdout.contains("the user prefers tea"), "got: {stdout}");
}
#[test]
fn recall_requires_query_source() {
let (_dir, path, pp, _) = seed_db();
let (status, _stdout, stderr) =
run_cli(&["recall", path.to_str().unwrap(), "--top-k", "1"], pp);
assert!(!status.success(), "recall without query should fail");
assert!(
stderr.contains("--query") || stderr.contains("query"),
"stderr should mention missing query, got: {stderr}"
);
}
#[test]
fn mcp_serve_initialize_list_and_call_tools() {
use std::io::Write;
use std::process::{Command, Stdio};
let dir = tempdir().unwrap();
let path = dir.path().join("mcp.mnemo");
let pp = "smoke-pw";
{
let mut db = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
db.remember(
Memory::new(
"the user prefers tea",
MemoryType::Semantic,
vec![1.0, 0.0, 0.0, 0.0],
)
.with_importance(0.9),
)
.unwrap();
db.flush().unwrap();
}
let mut out_opt = None;
let mut last_stderr = String::new();
for attempt in 0..5u32 {
if attempt > 0 {
let backoff_ms = 100u64 << (attempt - 1);
std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
}
let mut child = Command::new(bin())
.args(["serve", path.to_str().unwrap(), "--mcp"])
.env("MNEMO_PASSPHRASE", pp)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn mnemo serve --mcp");
let stdin = child.stdin.as_mut().expect("child stdin missing");
let script = [
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}"#,
r#"{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}"#,
r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#,
r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"stats","arguments":{}}}"#,
];
for line in script {
writeln!(stdin, "{line}").unwrap();
}
drop(child.stdin.take());
let run = child.wait_with_output().expect("child wait failed");
if !run.status.success() {
let stderr_snapshot = String::from_utf8_lossy(&run.stderr).into_owned();
if stderr_snapshot.contains("file already locked") {
last_stderr = stderr_snapshot;
continue;
}
out_opt = Some(run);
break;
}
out_opt = Some(run);
break;
}
let out = out_opt.unwrap_or_else(|| {
panic!(
"mnemo serve --mcp lost the fork/flock race on all 5 attempts \
(bounded retry exhausted); last stderr:\n{last_stderr}"
)
});
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
out.status.success(),
"server exited non-zero: status={:?} stderr={stderr}",
out.status
);
let lines: Vec<&str> = stdout.lines().filter(|l| !l.trim().is_empty()).collect();
assert!(
lines.len() >= 3,
"expected ≥3 response lines (init, tools/list, stats), got {}: {stdout}",
lines.len()
);
let init: serde_json::Value = serde_json::from_str(lines[0]).expect("init parse");
assert_eq!(init["jsonrpc"], "2.0");
assert_eq!(init["id"], 1);
assert_eq!(init["result"]["protocolVersion"], "2025-06-18");
assert_eq!(init["result"]["serverInfo"]["name"], "mnemo");
let list: serde_json::Value = serde_json::from_str(lines[1]).expect("tools/list parse");
let tools = list["result"]["tools"].as_array().expect("tools array");
let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
for expected in [
"about",
"remember",
"recall",
"forget",
"list",
"snapshot_list",
"stats",
] {
assert!(
names.contains(&expected),
"missing tool '{expected}' in {names:?}"
);
}
let stats: serde_json::Value = serde_json::from_str(lines[2]).expect("stats parse");
let content = &stats["result"]["content"][0]["text"];
assert!(
content.is_string(),
"stats content should be a text block: {stats}"
);
let stats_body: serde_json::Value =
serde_json::from_str(content.as_str().unwrap()).expect("stats body parse");
assert_eq!(
stats_body["memories"], 1,
"seeded exactly one live memory: {stats_body}"
);
}
#[test]
fn mcp_serve_refuses_without_passphrase() {
use std::process::{Command, Stdio};
let dir = tempdir().unwrap();
let path = dir.path().join("mcp-nopw.mnemo");
{
let mut db = Mnemo::create(&path, "smoke-pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"x",
MemoryType::Semantic,
vec![1.0, 0.0, 0.0, 0.0],
))
.unwrap();
db.flush().unwrap();
}
let out = Command::new(bin())
.args(["serve", path.to_str().unwrap(), "--mcp"])
.env_remove("MNEMO_PASSPHRASE")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("spawn failed");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
!out.status.success(),
"expected non-zero exit without passphrase"
);
assert!(
stderr.contains("MNEMO_PASSPHRASE"),
"stderr should mention the env var, got: {stderr}"
);
}
fn python_cmd() -> &'static str {
if cfg!(windows) {
"python"
} else {
"python3"
}
}
fn write_py_script(dir: &std::path::Path, name: &str, body: &str) -> std::path::PathBuf {
let p = dir.join(name);
std::fs::write(&p, body).unwrap();
p
}
const FAKE_EMBED_PY: &str = r#"
import sys, json, time
DIMS = 4
def encode(s):
axis = sum(bytearray(s.encode("utf-8"))) % DIMS
v = [0.0] * DIMS
v[axis] = float(len(s)) + 1.0 # non-zero, text-dependent
return v
for line in sys.stdin:
s = json.loads(line)
if s == "HANG":
time.sleep(3600)
elif s == "JUNK":
print("not-valid-json", flush=True)
elif s == "DOUBLE":
print(json.dumps(encode(s)), flush=True)
print(json.dumps(encode("stowaway")), flush=True)
else:
print(json.dumps(encode(s)), flush=True)
"#;
fn fake_vector_for(text: &str) -> Vec<f32> {
let axis = text.bytes().map(|b| b as usize).sum::<usize>() % 4;
let mut v = vec![0.0f32; 4];
v[axis] = text.len() as f32 + 1.0;
v
}
const BANNER_EMBED_PY: &str = r#"
import sys, json
print("[banner] model loaded", flush=True)
DIMS = 4
for line in sys.stdin:
s = json.loads(line)
axis = sum(bytearray(s.encode("utf-8"))) % DIMS
v = [0.0] * DIMS
v[axis] = float(len(s)) + 1.0
print(json.dumps(v), flush=True)
"#;
const CRASHING_EMBED_PY: &str = r#"
import sys, json
for line in sys.stdin:
_ = json.loads(line)
sys.exit(1)
"#;
const WRONG_WIDTH_EMBED_PY: &str = r#"
import sys, json
for line in sys.stdin:
_ = json.loads(line)
print(json.dumps([1.0, 2.0]), flush=True)
"#;
const ZERO_VECTOR_EMBED_PY: &str = r#"
import sys, json
for line in sys.stdin:
_ = json.loads(line)
print(json.dumps([0.0, 0.0, 0.0, 0.0]), flush=True)
"#;
const NON_FINITE_VECTOR_EMBED_PY: &str = r#"
import sys, json
for line in sys.stdin:
_ = json.loads(line)
print(json.dumps([1.0, 1e40, 2.0, 3.0]), flush=True)
"#;
const STARTUP_MARKER_EMBED_PY: &str = r#"
import sys, json, os
with open(os.environ["MNEMO_TEST_MARKER"], "a") as f:
f.write("start\n")
DIMS = 4
for line in sys.stdin:
s = json.loads(line)
axis = sum(bytearray(s.encode("utf-8"))) % DIMS
v = [0.0] * DIMS
v[axis] = float(len(s)) + 1.0
print(json.dumps(v), flush=True)
"#;
fn embed_cmd_arg(script_path: &std::path::Path) -> String {
format!("{} \"{}\"", python_cmd(), script_path.display())
}
#[test]
fn cli_remember_and_recall_text_round_trip() {
let dir = tempdir().unwrap();
let path = dir.path().join("em.mnemo");
let pp = "em-pw";
{
let mut db = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
db.remember(Memory::scaffold_manifest(4)).unwrap();
db.flush().unwrap();
}
let script = write_py_script(dir.path(), "fake.py", FAKE_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let out = Command::new(bin())
.args([
"remember",
path.to_str().unwrap(),
"--text",
"alpha",
"--embed-cmd",
&ecmd,
"--embed-name",
"fake-4d",
"--embed-normalize",
"true",
])
.env("MNEMO_PASSPHRASE", pp)
.output()
.expect("spawn mnemo remember");
assert!(
out.status.success(),
"mnemo remember failed: status={:?} stdout={} stderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
let ulid = String::from_utf8(out.stdout).unwrap();
assert!(
ulid.trim().len() >= 26,
"expected a ULID on stdout, got {ulid:?}"
);
let out = Command::new(bin())
.args([
"recall",
path.to_str().unwrap(),
"--text",
"alpha",
"--embed-cmd",
&ecmd,
"--embed-name",
"fake-4d",
"--top-k",
"5",
"--format",
"json",
"--no-track",
])
.env("MNEMO_PASSPHRASE", pp)
.output()
.expect("spawn mnemo recall");
assert!(
out.status.success(),
"mnemo recall failed: status={:?} stderr={}",
out.status,
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
stdout.contains("alpha"),
"recall --text output should contain the stored content, got: {stdout}"
);
}
#[test]
fn cli_recall_text_without_embed_cmd_errors_cleanly() {
let dir = tempdir().unwrap();
let path = dir.path().join("em-noec.mnemo");
let pp = "em-pw";
{
let _ = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
}
let out = Command::new(bin())
.args([
"recall",
path.to_str().unwrap(),
"--text",
"alpha",
])
.env("MNEMO_PASSPHRASE", pp)
.output()
.expect("spawn mnemo recall");
assert!(
!out.status.success(),
"recall --text without --embed-cmd should fail"
);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
combined.to_lowercase().contains("embed"),
"error should mention embedder, got: {combined}"
);
}
#[test]
fn cli_remember_text_crashing_embedder_errors_cleanly() {
let dir = tempdir().unwrap();
let path = dir.path().join("em-crash.mnemo");
let pp = "em-pw";
{
let _ = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
}
let script = write_py_script(dir.path(), "crash.py", CRASHING_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let out = Command::new(bin())
.args([
"remember",
path.to_str().unwrap(),
"--text",
"boom",
"--embed-cmd",
&ecmd,
"--embed-name",
"fake-4d",
])
.env("MNEMO_PASSPHRASE", pp)
.output()
.expect("spawn mnemo remember");
assert!(
!out.status.success(),
"remember with a crashing embedder should fail"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.to_lowercase().contains("embed-cmd") || stderr.to_lowercase().contains("embedder"),
"stderr should mention the embedder failure, got: {stderr}"
);
}
#[test]
fn cli_remember_text_wrong_width_errors_cleanly() {
let dir = tempdir().unwrap();
let path = dir.path().join("em-ww.mnemo");
let pp = "em-pw";
{
let _ = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
}
let script = write_py_script(dir.path(), "ww.py", WRONG_WIDTH_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let out = Command::new(bin())
.args([
"remember",
path.to_str().unwrap(),
"--text",
"two-dims",
"--embed-cmd",
&ecmd,
"--embed-name",
"fake-4d",
])
.env("MNEMO_PASSPHRASE", pp)
.output()
.expect("spawn mnemo remember");
assert!(
!out.status.success(),
"remember with wrong-width embedder should fail"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("dim") || stderr.contains("2"),
"stderr should mention the dimension mismatch, got: {stderr}"
);
}
#[test]
fn mcp_serve_with_embedder_text_params_and_persistent_child() {
use std::io::Write;
use std::process::{Command, Stdio};
let dir = tempdir().unwrap();
let path = dir.path().join("mcp-em.mnemo");
let pp = "em-pw";
{
let mut db = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
db.remember(Memory::scaffold_manifest(4)).unwrap();
db.flush().unwrap();
}
let marker = dir.path().join("startup.marker");
let script = write_py_script(dir.path(), "startup.py", STARTUP_MARKER_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let mut out_opt = None;
let mut last_stderr = String::new();
for attempt in 0..5u32 {
if attempt > 0 {
let backoff_ms = 100u64 << (attempt - 1);
std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
}
let mut child = Command::new(bin())
.args([
"serve",
path.to_str().unwrap(),
"--mcp",
"--embed-cmd",
&ecmd,
"--embed-name",
"fake-4d",
])
.env("MNEMO_PASSPHRASE", pp)
.env("MNEMO_TEST_MARKER", &marker)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn mnemo serve --mcp");
let stdin = child.stdin.as_mut().expect("child stdin missing");
let script = [
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}"#,
r#"{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}"#,
r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"remember","arguments":{"content":"alpha","memory_type":"semantic","text":"alpha"}}}"#,
r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"remember","arguments":{"content":"beta","memory_type":"semantic","text":"beta"}}}"#,
r#"{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"recall","arguments":{"text":"alpha","top_k":5}}}"#,
];
for line in script {
writeln!(stdin, "{line}").unwrap();
}
drop(child.stdin.take());
let out = child.wait_with_output().expect("child wait");
last_stderr = String::from_utf8_lossy(&out.stderr).to_string();
if out.status.success() {
out_opt = Some(out);
break;
}
if !last_stderr.contains("file already locked") {
panic!(
"mnemo serve --mcp failed (attempt {attempt}): status={:?} stderr={last_stderr}",
out.status
);
}
}
let out = out_opt.unwrap_or_else(|| {
panic!("mnemo serve --mcp never acquired the lock: stderr={last_stderr}")
});
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("\"id\":2"),
"expected a response to remember call id 2, got stdout: {stdout}"
);
assert!(
stdout.contains("\"id\":4"),
"expected a response to recall call id 4, got stdout: {stdout}"
);
assert!(
stdout.contains("alpha"),
"expected 'alpha' in recall output, got: {stdout}"
);
let marker_text = std::fs::read_to_string(&marker).unwrap_or_default();
let starts = marker_text.lines().filter(|l| *l == "start").count();
assert_eq!(
starts, 1,
"embedder child should have started once; marker file: {marker_text:?}"
);
}
#[test]
fn mcp_text_param_without_embedder_returns_helpful_error() {
use std::io::Write;
use std::process::{Command, Stdio};
let dir = tempdir().unwrap();
let path = dir.path().join("mcp-noem.mnemo");
let pp = "em-pw";
{
let _ = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
}
let mut out_opt = None;
let mut last_stderr = String::new();
for attempt in 0..5u32 {
if attempt > 0 {
let backoff_ms = 100u64 << (attempt - 1);
std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
}
let mut child = Command::new(bin())
.args(["serve", path.to_str().unwrap(), "--mcp"])
.env("MNEMO_PASSPHRASE", pp)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn mnemo serve --mcp");
let stdin = child.stdin.as_mut().unwrap();
let script = [
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}"#,
r#"{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}"#,
r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"recall","arguments":{"text":"anything"}}}"#,
];
for line in script {
writeln!(stdin, "{line}").unwrap();
}
drop(child.stdin.take());
let out = child.wait_with_output().expect("child wait");
last_stderr = String::from_utf8_lossy(&out.stderr).to_string();
if out.status.success() {
out_opt = Some(out);
break;
}
if !last_stderr.contains("file already locked") {
panic!(
"mnemo serve --mcp failed (attempt {attempt}): status={:?} stderr={last_stderr}",
out.status
);
}
}
let out = out_opt.unwrap_or_else(|| {
panic!("mnemo serve --mcp never acquired the lock: stderr={last_stderr}")
});
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("--embed-cmd") || stdout.contains("embedder"),
"expected a helpful error mentioning --embed-cmd, got: {stdout}"
);
}
#[test]
fn cli_remember_text_hang_times_out_bounded() {
let dir = tempdir().unwrap();
let path = dir.path().join("hang.mnemo");
let pp = "em-pw";
{
let _ = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
}
let script = write_py_script(dir.path(), "hang.py", FAKE_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let start = std::time::Instant::now();
let out = Command::new(bin())
.args([
"remember",
path.to_str().unwrap(),
"--text",
"HANG",
"--embed-cmd",
&ecmd,
"--embed-name",
"fake-4d",
"--embed-timeout",
"2",
])
.env("MNEMO_PASSPHRASE", pp)
.output()
.expect("spawn mnemo remember");
let elapsed = start.elapsed();
assert!(
!out.status.success(),
"remember on HANG should error (child never replies)"
);
assert!(
elapsed < std::time::Duration::from_secs(20),
"hang must trip the --embed-timeout bound; elapsed={elapsed:?}"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("timed out") || stderr.contains("embed-cmd"),
"stderr should name the timeout, got: {stderr}"
);
}
#[test]
fn embed_cmd_recovers_after_junk_reply_with_correct_vector() {
use mnemo::{EmbedCmd, Embedder, EmbedderIdentity};
let dir = tempdir().unwrap();
let script = write_py_script(dir.path(), "junk.py", FAKE_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let identity = EmbedderIdentity {
name: "fake-4d".into(),
dimensions: 4,
normalize: true,
};
let em = EmbedCmd::new(&ecmd, identity)
.unwrap()
.with_timeout(std::time::Duration::from_secs(5));
let err = em.embed(&["JUNK"]).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("malformed JSON") || msg.contains("respawn"),
"JUNK should raise a protocol error, got: {msg}"
);
let out = em.embed(&["abc"]).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0], fake_vector_for("abc"));
}
#[test]
fn embed_cmd_recovers_after_double_reply_with_correct_vector() {
use mnemo::{EmbedCmd, Embedder, EmbedderIdentity};
let dir = tempdir().unwrap();
let script = write_py_script(dir.path(), "double.py", FAKE_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let identity = EmbedderIdentity {
name: "fake-4d".into(),
dimensions: 4,
normalize: true,
};
let em = EmbedCmd::new(&ecmd, identity)
.unwrap()
.with_timeout(std::time::Duration::from_secs(5));
let out = em.embed(&["DOUBLE"]).unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0], fake_vector_for("DOUBLE"));
std::thread::sleep(std::time::Duration::from_millis(200));
let next = em.embed(&["abc"]);
match next {
Ok(v) => {
assert_eq!(v.len(), 1);
assert_eq!(
v[0],
fake_vector_for("abc"),
"next call MUST return abc's vector, never a leftover"
);
}
Err(e) => {
let msg = format!("{e}");
assert!(
msg.contains("respawn") || msg.contains("outside a reply"),
"if first call errors it must be a clear protocol error, got: {msg}"
);
let recovery = em.embed(&["abc"]).unwrap();
assert_eq!(recovery[0], fake_vector_for("abc"));
}
}
}
#[test]
fn embed_cmd_rejects_all_zero_vector_and_respawns() {
use mnemo::{EmbedCmd, Embedder, EmbedderIdentity};
let dir = tempdir().unwrap();
let script = write_py_script(dir.path(), "zero.py", ZERO_VECTOR_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let identity = EmbedderIdentity {
name: "fake-4d".into(),
dimensions: 4,
normalize: true,
};
let em = EmbedCmd::new(&ecmd, identity)
.unwrap()
.with_timeout(std::time::Duration::from_secs(5));
let err = em.embed(&["anything"]).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("all-zero") && msg.contains("respawn"),
"zero vector must surface as the dedicated protocol error, got: {msg}"
);
let err2 = em.embed(&["abc"]).unwrap_err();
assert!(format!("{err2}").contains("all-zero"));
}
#[test]
fn embed_cmd_rejects_non_finite_vector_and_respawns() {
use mnemo::{EmbedCmd, Embedder, EmbedderIdentity};
let dir = tempdir().unwrap();
let script = write_py_script(dir.path(), "non_finite.py", NON_FINITE_VECTOR_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let identity = EmbedderIdentity {
name: "fake-4d".into(),
dimensions: 4,
normalize: true,
};
let em = EmbedCmd::new(&ecmd, identity)
.unwrap()
.with_timeout(std::time::Duration::from_secs(5));
let err = em.embed(&["anything"]).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("non-finite") && msg.contains("respawn"),
"±∞ reply must surface as the dedicated protocol error, got: {msg}"
);
}
#[test]
fn embed_cmd_detects_stdout_banner_before_requests() {
use mnemo::{EmbedCmd, Embedder, EmbedderIdentity};
let dir = tempdir().unwrap();
let script = write_py_script(dir.path(), "banner.py", BANNER_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let identity = EmbedderIdentity {
name: "fake-4d".into(),
dimensions: 4,
normalize: true,
};
let em = EmbedCmd::new(&ecmd, identity)
.unwrap()
.with_timeout(std::time::Duration::from_secs(5));
std::thread::sleep(std::time::Duration::from_millis(200));
let err = em.embed(&["anything"]).unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("stdout") || msg.contains("outside a reply") || msg.contains("malformed"),
"banner should surface as a stdout-protocol error, got: {msg}"
);
}
#[test]
fn embed_cmd_two_thousand_text_batch_does_not_deadlock() {
use mnemo::{EmbedCmd, Embedder, EmbedderIdentity};
let dir = tempdir().unwrap();
let script = write_py_script(dir.path(), "batch.py", FAKE_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let identity = EmbedderIdentity {
name: "fake-4d".into(),
dimensions: 4,
normalize: true,
};
let em = EmbedCmd::new(&ecmd, identity)
.unwrap()
.with_timeout(std::time::Duration::from_secs(30));
let texts: Vec<String> = (0..2000).map(|i| format!("text-{i:04}")).collect();
let refs: Vec<&str> = texts.iter().map(|s| s.as_str()).collect();
let out = em.embed(&refs).unwrap();
assert_eq!(out.len(), 2000);
for i in [0, 123, 999, 1999] {
assert_eq!(
out[i],
fake_vector_for(&texts[i]),
"vector at index {i} must correspond to its input text"
);
}
}
#[test]
fn mcp_hang_bounded_and_server_survives_for_next_call() {
use std::io::Write;
use std::process::{Command, Stdio};
let dir = tempdir().unwrap();
let path = dir.path().join("mcp-hang.mnemo");
let pp = "em-pw";
{
let mut db = Mnemo::create(&path, pp, fast_cfg(4)).unwrap();
db.remember(Memory::scaffold_manifest(4)).unwrap();
db.flush().unwrap();
}
let script = write_py_script(dir.path(), "hang.py", FAKE_EMBED_PY);
let ecmd = embed_cmd_arg(&script);
let mut out_opt = None;
let mut last_stderr = String::new();
for attempt in 0..5u32 {
if attempt > 0 {
let backoff_ms = 100u64 << (attempt - 1);
std::thread::sleep(std::time::Duration::from_millis(backoff_ms));
}
let mut child = Command::new(bin())
.args([
"serve",
path.to_str().unwrap(),
"--mcp",
"--embed-cmd",
&ecmd,
"--embed-name",
"fake-4d",
"--embed-timeout",
"2",
])
.env("MNEMO_PASSPHRASE", pp)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn mnemo serve --mcp");
let stdin = child.stdin.as_mut().unwrap();
let script = [
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}"#,
r#"{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}"#,
r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"remember","arguments":{"content":"will_hang","memory_type":"semantic","text":"HANG"}}}"#,
r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"remember","arguments":{"content":"survives","memory_type":"semantic","text":"survives"}}}"#,
];
for line in script {
writeln!(stdin, "{line}").unwrap();
}
drop(child.stdin.take());
let start = std::time::Instant::now();
let out = child.wait_with_output().expect("child wait");
let elapsed = start.elapsed();
last_stderr = String::from_utf8_lossy(&out.stderr).to_string();
if out.status.success() {
assert!(
elapsed < std::time::Duration::from_secs(30),
"server should return before 30s even with a HANG; elapsed={elapsed:?}"
);
out_opt = Some(out);
break;
}
if !last_stderr.contains("file already locked") {
panic!(
"mnemo serve --mcp failed (attempt {attempt}): status={:?} stderr={last_stderr}",
out.status
);
}
}
let out = out_opt.unwrap_or_else(|| {
panic!("mnemo serve --mcp never acquired the lock: stderr={last_stderr}")
});
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("\"id\":2") && (stdout.contains("timed out") || stdout.contains("error")),
"expected an RPC error on the HANG call id 2, got stdout: {stdout}"
);
assert!(
stdout.contains("\"id\":3"),
"expected a response to call id 3 after the HANG; got: {stdout}"
);
assert!(
!stdout
.lines()
.any(|l| l.contains("\"id\":3") && l.contains("error")),
"call id 3 (post-HANG) should have succeeded, not errored: {stdout}"
);
}