fn cmd(binary: &str) -> std::process::Command {
let mut c = std::process::Command::new(binary);
c.env("AI_MEMORY_NO_CONFIG", "1");
c
}
#[test]
fn test_cli_store_and_recall() {
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-cli-test-{}.db", uuid::Uuid::new_v4()));
let binary = env!("CARGO_BIN_EXE_ai-memory");
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"test-project",
"-T",
"Rust is great",
"--content",
"Rust provides memory safety without garbage collection",
"--tags",
"rust,language",
"-p",
"8",
])
.output()
.unwrap();
assert!(
output.status.success(),
"store failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stored: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(stored["tier"], "long");
assert_eq!(stored["namespace"], "test-project");
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"recall",
"Rust memory safety",
"-n",
"test-project",
])
.output()
.unwrap();
assert!(
output.status.success(),
"recall failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let recalled: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(recalled["count"].as_u64().unwrap() >= 1);
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"search",
"Rust",
])
.output()
.unwrap();
assert!(output.status.success());
let searched: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(searched["count"].as_u64().unwrap() >= 1);
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "list"])
.output()
.unwrap();
assert!(output.status.success());
let listed: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(listed["count"].as_u64().unwrap() >= 1);
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
assert!(output.status.success());
let stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(stats["total"].as_u64().unwrap() >= 1);
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "namespaces"])
.output()
.unwrap();
assert!(output.status.success());
let ns: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(!ns["namespaces"].as_array().unwrap().is_empty());
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "export"])
.output()
.unwrap();
assert!(output.status.success());
let exported: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(exported["count"].as_u64().unwrap() >= 1);
let id = stored["id"].as_str().unwrap();
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "delete", id])
.output()
.unwrap();
assert!(output.status.success());
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_deduplication() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-dedup-test-{}.db", uuid::Uuid::new_v4()));
for content in ["first version", "second version"] {
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-T",
"same title",
"-n",
"same-ns",
"--content",
content,
"-p",
"5",
])
.output()
.unwrap();
assert!(output.status.success());
}
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
let stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
stats["total"].as_u64().unwrap(),
1,
"deduplication failed — expected 1 memory"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_gc_removes_expired() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-gc-test-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"short",
"-T",
"ephemeral thought",
"--content",
"goes away",
])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "gc"])
.output()
.unwrap();
assert!(output.status.success());
let gc: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(gc["expired_deleted"].as_u64().unwrap(), 0);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_content_size_limit() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-size-test-{}.db", uuid::Uuid::new_v4()));
let huge_content = "x".repeat(70_000);
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-T",
"too big",
"--content",
&huge_content,
])
.output()
.unwrap();
assert!(!output.status.success(), "should reject oversized content");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_import_export_roundtrip() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db1 = dir.join(format!("ai-memory-export-{}.db", uuid::Uuid::new_v4()));
let db2 = dir.join(format!("ai-memory-import-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db1.to_str().unwrap(),
"store",
"-t",
"long",
"-T",
"portable memory",
"--content",
"travels between machines",
])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args(["--db", db1.to_str().unwrap(), "export"])
.output()
.unwrap();
assert!(output.status.success());
let export_output = cmd(binary)
.args(["--db", db1.to_str().unwrap(), "export"])
.output()
.unwrap();
let mut child = cmd(binary)
.args(["--db", db2.to_str().unwrap(), "--json", "import"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(&export_output.stdout)
.unwrap();
let result = child.wait_with_output().unwrap();
assert!(
result.status.success(),
"import failed: {}",
String::from_utf8_lossy(&result.stderr)
);
let output = cmd(binary)
.args(["--db", db2.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
let stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
stats["total"].as_u64().unwrap() >= 1,
"import roundtrip failed"
);
let _ = std::fs::remove_file(&db1);
let _ = std::fs::remove_file(&db2);
}
#[test]
fn test_reject_empty_title() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-val-title-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-T",
"",
"--content",
"some content",
])
.output()
.unwrap();
assert!(!output.status.success(), "should reject empty title");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_reject_bad_source() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-val-source-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-T",
"test",
"--content",
"content",
"-S",
"invalid_source",
])
.output()
.unwrap();
assert!(!output.status.success(), "should reject bad source");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_reject_bad_namespace() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-val-ns-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-T",
"test",
"--content",
"content",
"-n",
"bad namespace",
])
.output()
.unwrap();
assert!(
!output.status.success(),
"should reject namespace with spaces"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_reject_oversized_content() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-val-size-{}.db", uuid::Uuid::new_v4()));
let huge = "x".repeat(70_000);
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-T",
"huge",
"--content",
&huge,
])
.output()
.unwrap();
assert!(!output.status.success(), "should reject oversized content");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_reject_bad_priority() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-val-prio-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-T",
"test",
"--content",
"content",
"-p",
"0",
])
.output()
.unwrap();
assert!(!output.status.success(), "should reject priority 0");
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-T",
"test2",
"--content",
"content",
"-p",
"11",
])
.output()
.unwrap();
assert!(!output.status.success(), "should reject priority 11");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_reject_bad_confidence() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-val-conf-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-T",
"test",
"--content",
"content",
"--confidence",
"1.5",
])
.output()
.unwrap();
assert!(!output.status.success(), "should reject confidence > 1.0");
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-T",
"test2",
"--content",
"content",
"--confidence",
"-0.1",
])
.output()
.unwrap();
assert!(!output.status.success(), "should reject confidence < 0.0");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_recall_priority_order() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-order-{}.db", uuid::Uuid::new_v4()));
for (title, priority) in [
("alpha recall test", "2"),
("beta recall test", "9"),
("gamma recall test", "5"),
] {
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"order-test",
"-T",
title,
"--content",
&format!("content about recall testing for {}", title),
"-p",
priority,
])
.output()
.unwrap();
assert!(
output.status.success(),
"store failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"recall",
"recall test",
"-n",
"order-test",
])
.output()
.unwrap();
assert!(output.status.success());
let recalled: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let memories = recalled["memories"].as_array().unwrap();
assert!(memories.len() >= 2, "should recall at least 2 memories");
let first_priority = memories[0]["priority"].as_i64().unwrap();
let second_priority = memories[1]["priority"].as_i64().unwrap();
assert!(
first_priority >= second_priority,
"higher priority should come first: {} vs {}",
first_priority,
second_priority
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_ttl_assignment() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-ttl-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"short",
"-n",
"ttl-test",
"-T",
"short lived",
"--content",
"expires soon",
])
.output()
.unwrap();
assert!(output.status.success());
let short: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
short["expires_at"].is_string(),
"short-term should have expires_at"
);
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"mid",
"-n",
"ttl-test",
"-T",
"mid lived",
"--content",
"expires later",
])
.output()
.unwrap();
assert!(output.status.success());
let mid: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
mid["expires_at"].is_string(),
"mid-term should have expires_at"
);
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"ttl-test",
"-T",
"long lived",
"--content",
"never expires",
])
.output()
.unwrap();
assert!(output.status.success());
let long: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
long["expires_at"].is_null(),
"long-term should NOT have expires_at"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_auto_promotion() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!(
"ai-memory-promote-auto-{}.db",
uuid::Uuid::new_v4()
));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"mid",
"-n",
"promo-test",
"-T",
"promotable memory",
"--content",
"this memory should be promoted after enough accesses",
])
.output()
.unwrap();
assert!(output.status.success());
let stored: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let id = stored["id"].as_str().unwrap().to_string();
for _ in 0..6 {
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"recall",
"promotable memory",
"-n",
"promo-test",
])
.output()
.unwrap();
assert!(output.status.success());
}
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "get", &id])
.output()
.unwrap();
assert!(output.status.success());
let got: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
got["memory"]["tier"], "long",
"memory should have been auto-promoted to long"
);
assert!(
got["memory"]["expires_at"].is_null(),
"promoted memory should have no expiry"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_forget_by_pattern() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-forget-{}.db", uuid::Uuid::new_v4()));
for (title, content) in [
("keep this one", "permanent important data"),
("forget alpha", "ephemeral data to remove"),
("forget beta", "ephemeral data to discard"),
] {
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-t",
"long",
"-n",
"forget-test",
"-T",
title,
"--content",
content,
])
.output()
.unwrap();
assert!(output.status.success());
}
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
let stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(stats["total"].as_u64().unwrap(), 3);
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"forget",
"-p",
"ephemeral",
"-n",
"forget-test",
])
.output()
.unwrap();
assert!(output.status.success());
let forgot: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
forgot["deleted"].as_u64().unwrap() >= 1,
"should have deleted at least 1"
);
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
let stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
stats["total"].as_u64().unwrap() < 3,
"total should have decreased"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_namespace_isolation() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-nsiso-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-t",
"long",
"-n",
"ns-a",
"-T",
"alpha secret data",
"--content",
"isolation test alpha content data",
])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-t",
"long",
"-n",
"ns-b",
"-T",
"beta secret data",
"--content",
"isolation test beta content data",
])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"recall",
"secret data",
"-n",
"ns-a",
])
.output()
.unwrap();
assert!(output.status.success());
let recalled: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
for mem in recalled["memories"].as_array().unwrap() {
assert_eq!(
mem["namespace"].as_str().unwrap(),
"ns-a",
"namespace isolation broken: found ns-b memory in ns-a recall"
);
}
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_link_creation() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-link-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"link-test",
"-T",
"link source",
"--content",
"source content",
])
.output()
.unwrap();
assert!(output.status.success());
let src: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let src_id = src["id"].as_str().unwrap().to_string();
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"link-test",
"-T",
"link target",
"--content",
"target content",
])
.output()
.unwrap();
assert!(output.status.success());
let tgt: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let tgt_id = tgt["id"].as_str().unwrap().to_string();
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"link",
&src_id,
&tgt_id,
"-r",
"related_to",
])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "get", &src_id])
.output()
.unwrap();
assert!(output.status.success());
let got: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let links = got["links"].as_array().unwrap();
assert!(!links.is_empty(), "links should not be empty after linking");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_consolidation() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-consol-{}.db", uuid::Uuid::new_v4()));
let mut ids = Vec::new();
for (title, content) in [
(
"consol alpha",
"first piece of knowledge about consolidation",
),
(
"consol beta",
"second piece of knowledge about consolidation",
),
(
"consol gamma",
"third piece of knowledge about consolidation",
),
] {
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"mid",
"-n",
"consol-test",
"-T",
title,
"--content",
content,
])
.output()
.unwrap();
assert!(output.status.success());
let stored: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
ids.push(stored["id"].as_str().unwrap().to_string());
}
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
let stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(stats["total"].as_u64().unwrap(), 3);
let ids_str = ids.join(",");
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"consolidate",
&ids_str,
"-T",
"consolidated knowledge",
"-s",
"all three pieces combined",
"-n",
"consol-test",
])
.output()
.unwrap();
assert!(
output.status.success(),
"consolidate failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
let stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
stats["total"].as_u64().unwrap() < 3,
"total should have decreased after consolidation"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_promote_command() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-promote-cmd-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"mid",
"-n",
"promote-test",
"-T",
"to be promoted",
"--content",
"this will become long-term",
])
.output()
.unwrap();
assert!(output.status.success());
let stored: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let id = stored["id"].as_str().unwrap().to_string();
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "promote", &id])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "get", &id])
.output()
.unwrap();
assert!(output.status.success());
let got: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(got["memory"]["tier"], "long");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_namespaces_command() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-ns-cmd-{}.db", uuid::Uuid::new_v4()));
for (ns, title) in [("ns-alpha", "alpha mem"), ("ns-beta", "beta mem")] {
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-t",
"long",
"-n",
ns,
"-T",
title,
"--content",
"test content",
])
.output()
.unwrap();
assert!(output.status.success());
}
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "namespaces"])
.output()
.unwrap();
assert!(output.status.success());
let ns: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let namespaces = ns["namespaces"].as_array().unwrap();
let ns_names: Vec<&str> = namespaces
.iter()
.map(|n| n["namespace"].as_str().unwrap())
.collect();
assert!(ns_names.contains(&"ns-alpha"), "should contain ns-alpha");
assert!(ns_names.contains(&"ns-beta"), "should contain ns-beta");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_unicode_handling() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-unicode-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"unicode-test",
"-T",
"Memoria en espanol y japones",
"--content",
"Contenido con acentos: cafe, nino, resumen. Also Japanese: konnichiwa sekai",
])
.output()
.unwrap();
assert!(output.status.success(), "store with unicode failed");
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"recall",
"espanol japones",
"-n",
"unicode-test",
])
.output()
.unwrap();
assert!(output.status.success());
let recalled: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
recalled["count"].as_u64().unwrap() >= 1,
"should recall unicode memory"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_boundary_priority_min() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-bnd-pmin-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-T",
"min priority",
"--content",
"boundary test",
"-p",
"1",
])
.output()
.unwrap();
assert!(output.status.success(), "priority=1 should be valid");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_boundary_priority_max() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-bnd-pmax-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-T",
"max priority",
"--content",
"boundary test",
"-p",
"10",
])
.output()
.unwrap();
assert!(output.status.success(), "priority=10 should be valid");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_boundary_confidence_zero() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-bnd-c0-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-T",
"zero confidence",
"--content",
"boundary test",
"--confidence",
"0.0",
])
.output()
.unwrap();
assert!(output.status.success(), "confidence=0.0 should be valid");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_boundary_confidence_one() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-bnd-c1-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-T",
"full confidence",
"--content",
"boundary test",
"--confidence",
"1.0",
])
.output()
.unwrap();
assert!(output.status.success(), "confidence=1.0 should be valid");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_boundary_max_title_length() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-bnd-tlen-{}.db", uuid::Uuid::new_v4()));
let long_title = "a".repeat(512);
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-T",
&long_title,
"--content",
"boundary test",
])
.output()
.unwrap();
assert!(output.status.success(), "512-char title should be valid");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_export_includes_links() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-explink-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"explink",
"-T",
"export link src",
"--content",
"source",
])
.output()
.unwrap();
let src: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let src_id = src["id"].as_str().unwrap().to_string();
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"explink",
"-T",
"export link tgt",
"--content",
"target",
])
.output()
.unwrap();
let tgt: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let tgt_id = tgt["id"].as_str().unwrap().to_string();
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"link",
&src_id,
&tgt_id,
])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "export"])
.output()
.unwrap();
assert!(output.status.success());
let exported: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let links = exported["links"].as_array().unwrap();
assert!(!links.is_empty(), "export should include links");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_import_roundtrip_count_match() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db1 = dir.join(format!("ai-memory-irt-src-{}.db", uuid::Uuid::new_v4()));
let db2 = dir.join(format!("ai-memory-irt-dst-{}.db", uuid::Uuid::new_v4()));
for i in 0..3 {
let output = cmd(binary)
.args([
"--db",
db1.to_str().unwrap(),
"store",
"-t",
"long",
"-n",
"irt-test",
"-T",
&format!("roundtrip mem {}", i),
"--content",
&format!("content for roundtrip {}", i),
])
.output()
.unwrap();
assert!(output.status.success());
}
let output = cmd(binary)
.args(["--db", db1.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
let src_stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let src_count = src_stats["total"].as_u64().unwrap();
let export_output = cmd(binary)
.args(["--db", db1.to_str().unwrap(), "export"])
.output()
.unwrap();
assert!(export_output.status.success());
let mut child = cmd(binary)
.args(["--db", db2.to_str().unwrap(), "--json", "import"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(&export_output.stdout)
.unwrap();
let result = child.wait_with_output().unwrap();
assert!(result.status.success());
let output = cmd(binary)
.args(["--db", db2.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
let dst_stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let dst_count = dst_stats["total"].as_u64().unwrap();
assert_eq!(
src_count, dst_count,
"import count should match export count"
);
let _ = std::fs::remove_file(&db1);
let _ = std::fs::remove_file(&db2);
}
#[test]
fn test_update_via_cli() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-update-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"update-test",
"-T",
"original title",
"--content",
"original content",
])
.output()
.unwrap();
assert!(output.status.success());
let stored: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let id = stored["id"].as_str().unwrap().to_string();
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"update",
&id,
"-T",
"updated title",
])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "get", &id])
.output()
.unwrap();
assert!(output.status.success());
let got: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(got["memory"]["title"], "updated title");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_stats_accuracy() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-statsacc-{}.db", uuid::Uuid::new_v4()));
let count = 5;
for i in 0..count {
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-t",
"long",
"-n",
"stats-test",
"-T",
&format!("stats mem {}", i),
"--content",
&format!("content {}", i),
])
.output()
.unwrap();
assert!(output.status.success());
}
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "stats"])
.output()
.unwrap();
let stats: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
stats["total"].as_u64().unwrap(),
count,
"stats total should match stored count"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_gc_preserves_long_term() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-gckeep-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"short",
"-n",
"gc-test",
"-T",
"short lived gc test",
"--content",
"will have TTL",
])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"store",
"-t",
"long",
"-n",
"gc-test",
"-T",
"long lived gc test",
"--content",
"will persist forever",
])
.output()
.unwrap();
assert!(output.status.success());
let long_stored: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let long_id = long_stored["id"].as_str().unwrap().to_string();
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "gc"])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "--json", "get", &long_id])
.output()
.unwrap();
assert!(
output.status.success(),
"long-term memory should survive GC"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_search_with_since_future() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-since-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"store",
"-t",
"long",
"-n",
"since-test",
"-T",
"searchable since test",
"--content",
"this should not appear with future since",
])
.output()
.unwrap();
assert!(output.status.success());
let output = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"--json",
"search",
"searchable",
"--since",
"2099-01-01T00:00:00Z",
])
.output()
.unwrap();
assert!(output.status.success());
let results: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
results["count"].as_u64().unwrap(),
0,
"future --since should return 0 results"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_health_endpoint() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-health-{}.db", uuid::Uuid::new_v4()));
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let mut child = cmd(binary)
.args([
"--db",
db_path.to_str().unwrap(),
"serve",
"--port",
&port.to_string(),
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.unwrap();
let url = format!("http://127.0.0.1:{}/api/v1/health", port);
let mut ok = false;
for _ in 0..30 {
std::thread::sleep(std::time::Duration::from_millis(100));
if let Ok(output) = std::process::Command::new("curl")
.args(["-s", "-o", "/dev/null", "-w", "%{http_code}", &url])
.output()
{
let code = String::from_utf8_lossy(&output.stdout);
if code == "200" {
ok = true;
break;
}
}
}
let _ = child.kill();
let _ = child.wait();
assert!(ok, "health endpoint should return 200");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_initialize() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-mcp-init-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(
stdin,
r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{}}}}"#
)
.ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let resp: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("invalid JSON response");
assert_eq!(resp["jsonrpc"], "2.0");
assert_eq!(resp["id"], 1);
assert_eq!(resp["result"]["serverInfo"]["name"], "ai-memory");
assert_eq!(resp["result"]["protocolVersion"], "2024-11-05");
assert!(resp["result"]["capabilities"]["tools"].is_object());
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_tools_list() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-mcp-tools-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(
stdin,
r#"{{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{{}}}}"#
)
.ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let resp: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("invalid JSON response");
let tools = resp["result"]["tools"]
.as_array()
.expect("tools should be array");
assert_eq!(tools.len(), 17, "expected 17 MCP tools");
let tool_names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
assert!(tool_names.contains(&"memory_store"));
assert!(tool_names.contains(&"memory_recall"));
assert!(tool_names.contains(&"memory_search"));
assert!(tool_names.contains(&"memory_list"));
assert!(tool_names.contains(&"memory_delete"));
assert!(tool_names.contains(&"memory_promote"));
assert!(tool_names.contains(&"memory_forget"));
assert!(tool_names.contains(&"memory_stats"));
assert!(tool_names.contains(&"memory_update"));
assert!(tool_names.contains(&"memory_get"));
assert!(tool_names.contains(&"memory_link"));
assert!(tool_names.contains(&"memory_get_links"));
assert!(tool_names.contains(&"memory_consolidate"));
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_store_and_recall() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-mcp-store-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"memory_store","arguments":{{"title":"MCP test memory","content":"This was stored via MCP protocol","tier":"long","priority":8}}}}}}"#).ok();
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{{"name":"memory_recall","arguments":{{"context":"MCP test"}}}}}}"#).ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.trim().lines().collect();
assert_eq!(lines.len(), 2, "expected 2 responses");
let store_resp: serde_json::Value =
serde_json::from_str(lines[0]).expect("invalid store response");
assert_eq!(store_resp["id"], 1);
assert!(store_resp["result"]["content"][0]["text"]
.as_str()
.unwrap()
.contains("\"id\""));
let recall_resp: serde_json::Value =
serde_json::from_str(lines[1]).expect("invalid recall response");
assert_eq!(recall_resp["id"], 2);
let recall_text = recall_resp["result"]["content"][0]["text"]
.as_str()
.unwrap();
assert!(recall_text.contains("MCP test memory"));
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_invalid_jsonrpc_version() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-mcp-ver-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(
stdin,
r#"{{"jsonrpc":"1.0","id":1,"method":"initialize","params":{{}}}}"#
)
.ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let resp: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("invalid JSON response");
assert!(
resp["error"].is_object(),
"expected error for invalid jsonrpc version"
);
assert_eq!(resp["error"]["code"], -32600);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_unknown_tool() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-mcp-unk-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"nonexistent_tool","arguments":{{}}}}}}"#).ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let resp: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("invalid JSON response");
let text = resp["result"]["content"][0]["text"].as_str().unwrap_or("");
assert!(text.contains("unknown tool"), "expected unknown tool error");
assert_eq!(resp["result"]["isError"], true);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_missing_tool_name() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-mcp-noname-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"arguments":{{}}}}}}"#).ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let resp: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("invalid JSON response");
assert!(
resp["error"].is_object(),
"expected error for missing tool name"
);
assert_eq!(resp["error"]["code"], -32602);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_stats() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-mcp-stats-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"memory_stats","arguments":{{}}}}}}"#).ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let resp: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("invalid JSON response");
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
assert!(text.contains("total"));
assert!(text.contains("by_tier"));
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_prompts_list() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!("ai-memory-mcp-prompts-{}.db", uuid::Uuid::new_v4()));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(
stdin,
r#"{{"jsonrpc":"2.0","id":1,"method":"prompts/list","params":{{}}}}"#
)
.ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let resp: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("invalid JSON response");
let prompts = resp["result"]["prompts"]
.as_array()
.expect("prompts should be array");
assert_eq!(prompts.len(), 2);
assert_eq!(prompts[0]["name"], "recall-first");
assert_eq!(prompts[1]["name"], "memory-workflow");
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_prompts_get_recall_first() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!(
"ai-memory-mcp-prompt-get-{}.db",
uuid::Uuid::new_v4()
));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":1,"method":"prompts/get","params":{{"name":"recall-first"}}}}"#).ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let resp: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("invalid JSON response");
let messages = resp["result"]["messages"]
.as_array()
.expect("messages should be array");
assert_eq!(messages.len(), 1);
let text = messages[0]["content"]["text"]
.as_str()
.expect("text content");
assert!(
text.contains("RECALL FIRST"),
"should contain recall-first rule"
);
assert!(text.contains("TOON"), "should mention TOON format");
assert!(
text.contains("memory_recall"),
"should reference memory_recall tool"
);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn test_mcp_recall_default_toon() {
let binary = env!("CARGO_BIN_EXE_ai-memory");
let dir = std::env::temp_dir();
let db_path = dir.join(format!(
"ai-memory-mcp-toon-def-{}.db",
uuid::Uuid::new_v4()
));
let output = cmd(binary)
.args(["--db", db_path.to_str().unwrap(), "mcp"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"memory_store","arguments":{{"title":"TOON default test","content":"Testing.","tier":"long","namespace":"test"}}}}}}"#).ok();
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{{"name":"memory_recall","arguments":{{"context":"TOON test","namespace":"test"}}}}}}"#).ok();
}
drop(child.stdin.take());
child.wait_with_output()
})
.expect("failed to run mcp");
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert!(
lines.len() >= 2,
"expected >=2 responses, got {}",
lines.len()
);
let recall_resp: serde_json::Value =
serde_json::from_str(lines[1]).expect("invalid recall response");
let text = recall_resp["result"]["content"][0]["text"]
.as_str()
.expect("recall text");
assert!(
text.contains("memories[") || text.starts_with("count:"),
"default should be TOON compact, got: {}",
&text[..text.len().min(100)]
);
assert!(text.contains("|"), "should contain pipe delimiters");
let _ = std::fs::remove_file(&db_path);
}