use std::process::Command;
use tempfile::TempDir;
fn areev(args: &[&str]) -> (bool, String, String) {
let out = Command::new(env!("CARGO_BIN_EXE_areev"))
.args(args)
.env_remove("AREEV_DB")
.output()
.expect("spawn areev");
(
out.status.success(),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
)
}
fn find_python() -> Option<&'static str> {
["python3", "python"].into_iter().find(|c| {
Command::new(c)
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
})
}
const FAKE_LLM_PY: &str = r#"
import json, sys
d = json.loads(sys.stdin.read())
op = d.get("op", "")
if op == "probe":
print(json.dumps({"model": "fake-extractor-1"}))
elif op == "extract":
assert "instructions" in d and "content" in d
print(json.dumps({"facts": [
{"subject": "john", "relation": "prefers", "object": "window seat", "confidence": 0.9},
{"subject": "john", "relation": "guess", "object": "likes jazz", "confidence": 0.2},
]}))
elif op == "ground":
print(json.dumps({"results": [
{"id": c["id"], "supported": "guess" not in c["claim"], "reason": "checked"}
for c in d.get("claims", [])
]}))
else:
print(json.dumps({}))
"#;
const GARBAGE_LLM_PY: &str = r#"
import json, sys
d = json.loads(sys.stdin.read())
if d.get("op") == "probe":
print(json.dumps({"model": "garbage-1"}))
else:
print("I'm afraid I can't do that.")
"#;
const DYING_LLM_PY: &str = r#"
import json, sys
d = json.loads(sys.stdin.read())
if d.get("op") == "probe":
print(json.dumps({"model": "dying-1"}))
else:
sys.exit(3)
"#;
struct Fake {
_dir: TempDir,
cmd: String,
db: String,
}
fn fake(script: &str) -> Option<Fake> {
let py = find_python()?;
let dir = TempDir::new().unwrap();
let path = dir.path().join("fake_llm.py");
std::fs::write(&path, script).unwrap();
let cmd = format!("{py} {}", path.display());
let db = dir.path().join("m.db").to_str().unwrap().to_string();
Some(Fake { _dir: dir, cmd, db })
}
fn remember(db: &str, content: &str, extra: &[&str]) -> (bool, serde_json::Value, String) {
let mut args = vec![
"remember", "--db", db, "--ns", "caller", "--content", content, "--observer", "voice-agent",
];
args.extend_from_slice(extra);
let (ok, out, err) = areev(&args);
let json = serde_json::from_str(out.trim()).unwrap_or_else(|e| {
panic!("remember did not print JSON ({e}): stdout={out:?} stderr={err:?}")
});
(ok, json, err)
}
fn cal(db: &str, query: &str) -> serde_json::Value {
let (ok, out, err) = areev(&["cal", query, "--db", db, "--ns", "caller"]);
assert!(ok, "cal failed: {err}");
serde_json::from_str(out.trim()).expect("cal printed JSON")
}
fn grains(v: &serde_json::Value) -> &Vec<serde_json::Value> {
v["grains"].as_array().expect("grains array")
}
#[test]
fn extracts_facts_with_model_provenance() {
let Some(f) = fake(FAKE_LLM_PY) else {
eprintln!("skipping: no python on PATH");
return;
};
let (ok, res, err) = remember(
&f.db,
"I always want a window seat.",
&["--llm-cmd", &f.cmd],
);
assert!(ok, "remember failed: {err}");
assert_eq!(res["model"], "fake-extractor-1");
assert_eq!(res["proposed"], 2);
assert_eq!(res["dropped"], 0);
assert_eq!(res["verification_status"], "unverified");
assert_eq!(res["facts"].as_array().unwrap().len(), 2);
let obs = res["event"].as_str().unwrap();
let rows = cal(&f.db, r#"RECALL facts WHERE verification_status = "unverified""#);
assert_eq!(grains(&rows).len(), 2, "{rows}");
for g in grains(&rows) {
assert_eq!(g["fields"]["derived_from"], obs);
assert_eq!(g["fields"]["source_type"], "derived");
assert_eq!(g["fields"]["extractor_model"], "fake-extractor-1");
}
let obs_rows = cal(&f.db, &format!(r#"RECALL events WHERE hash = "{obs}""#));
let stored = serde_json::to_string(&obs_rows).unwrap();
assert!(stored.contains("window seat"), "raw content missing: {stored}");
}
#[test]
fn grounding_drops_unsupported_facts_and_marks_the_rest_verified() {
let Some(f) = fake(FAKE_LLM_PY) else {
eprintln!("skipping: no python on PATH");
return;
};
let (ok, res, err) = remember(
&f.db,
"I always want a window seat.",
&["--llm-cmd", &f.cmd, "--ground-cmd", &f.cmd],
);
assert!(ok, "remember failed: {err}");
assert_eq!(res["proposed"], 2);
assert_eq!(res["dropped"], 1, "the ungrounded guess is dropped: {res}");
assert_eq!(res["facts"].as_array().unwrap().len(), 1);
assert_eq!(res["verification_status"], "verified");
let rows = cal(&f.db, r#"RECALL facts WHERE verification_status = "verified""#);
assert_eq!(grains(&rows).len(), 1);
assert_eq!(grains(&rows)[0]["fields"]["relation"], "prefers");
}
#[test]
fn the_confidence_floor_drops_low_confidence_facts() {
let Some(f) = fake(FAKE_LLM_PY) else {
eprintln!("skipping: no python on PATH");
return;
};
let (ok, res, err) = remember(
&f.db,
"I always want a window seat.",
&["--llm-cmd", &f.cmd, "--min-confidence", "0.5"],
);
assert!(ok, "remember failed: {err}");
assert_eq!(res["proposed"], 2);
assert_eq!(res["dropped"], 1);
assert_eq!(res["facts"].as_array().unwrap().len(), 1);
}
#[test]
fn garbage_from_the_model_still_keeps_the_source_text() {
let Some(f) = fake(GARBAGE_LLM_PY) else {
eprintln!("skipping: no python on PATH");
return;
};
let (ok, res, err) = remember(&f.db, "some raw note", &["--llm-cmd", &f.cmd]);
assert!(ok, "an unreadable response is not an error: {err}");
assert_eq!(res["facts"].as_array().unwrap().len(), 0);
assert_eq!(res["proposed"], 0);
assert_eq!(res["event"].as_str().unwrap().len(), 64);
let rows = cal(&f.db, "RECALL events");
assert_eq!(grains(&rows).len(), 1, "the raw text survived: {rows}");
}
#[test]
fn a_failed_extraction_reports_the_event_and_exits_non_zero() {
let Some(f) = fake(DYING_LLM_PY) else {
eprintln!("skipping: no python on PATH");
return;
};
let (ok, res, _err) = remember(&f.db, "some raw note", &["--llm-cmd", &f.cmd]);
assert!(!ok, "a failed extraction must exit non-zero: {res}");
assert_eq!(res["event"].as_str().unwrap().len(), 64);
assert!(res["error"].is_string(), "the failure is reported: {res}");
let rows = cal(&f.db, "RECALL events");
assert_eq!(grains(&rows).len(), 1, "the raw text survived: {rows}");
}
#[test]
fn dry_run_extracts_but_stores_nothing() {
let Some(f) = fake(FAKE_LLM_PY) else {
eprintln!("skipping: no python on PATH");
return;
};
let (ok, res, err) = remember(
&f.db,
"I always want a window seat.",
&["--llm-cmd", &f.cmd, "--dry-run"],
);
assert!(ok, "dry run failed: {err}");
assert_eq!(res["dry_run"], true);
assert_eq!(res["facts"].as_array().unwrap().len(), 2);
assert!(res["event"].is_null(), "nothing was stored: {res}");
assert_eq!(grains(&cal(&f.db, "RECALL events")).len(), 0);
assert_eq!(grains(&cal(&f.db, "RECALL facts")).len(), 0);
}
#[test]
fn host_supplied_facts_never_call_the_model() {
let Some(f) = fake(DYING_LLM_PY) else {
eprintln!("skipping: no python on PATH");
return;
};
let facts = r#"[{"subject":"john","relation":"diet","object":"vegetarian","confidence":0.95}]"#;
let (ok, res, err) = remember(
&f.db,
"I'm vegetarian.",
&["--facts", facts, "--llm-cmd", &f.cmd],
);
assert!(ok, "remember failed: {err}");
assert_eq!(res["facts"].as_array().unwrap().len(), 1);
assert!(res["model"].is_null(), "no model ran: {res}");
let rows = cal(&f.db, "RECALL facts");
assert_eq!(grains(&rows).len(), 1);
assert!(grains(&rows)[0]["fields"]["verification_status"].is_null(), "{rows}");
}
#[test]
fn malformed_facts_json_is_rejected_rather_than_stored_empty() {
let dir = TempDir::new().unwrap();
let db = dir.path().join("m.db");
let db = db.to_str().unwrap();
let (ok, _out, err) = areev(&[
"remember", "--db", db, "--ns", "caller", "--content", "note", "--facts",
r#"[{"subject":"john"}]"#,
]);
assert!(!ok, "an incomplete triple must not become an empty-subject fact");
assert!(err.contains("facts[0]"), "error names the row: {err}");
}
#[test]
fn cli_and_mcp_remember_produce_the_same_grain() {
use std::io::Write as IoWrite;
use std::process::Stdio;
let dir = TempDir::new().unwrap();
let cli_db = dir.path().join("cli.db").to_str().unwrap().to_string();
let mcp_db = dir.path().join("mcp.db").to_str().unwrap().to_string();
let content = "caller asked about refunds";
let (ok, cli_res, err) = {
let (ok, out, err) = areev(&[
"remember", "--db", &cli_db, "--ns", "caller", "--content", content,
"--observer", "voice-agent", "--session-id", "call-1", "--role", "user",
]);
let json: serde_json::Value = serde_json::from_str(out.trim()).expect("remember JSON");
(ok, json, err)
};
assert!(ok, "cli remember failed: {err}");
let mut child = Command::new(env!("CARGO_BIN_EXE_areev"))
.args(["serve", "--mcp", "--db", &mcp_db, "--ns", "caller"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.env_remove("AREEV_DB")
.spawn()
.unwrap();
{
let stdin = child.stdin.as_mut().unwrap();
let call = serde_json::json!({
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {"name": "areev_remember", "arguments": {
"content": content, "observer": "voice-agent",
"session_id": "call-1", "role": "user"}}});
writeln!(stdin, "{call}").unwrap();
}
let out = child.wait_with_output().unwrap();
let line = String::from_utf8_lossy(&out.stdout);
let resp: serde_json::Value = serde_json::from_str(line.lines().next().unwrap()).unwrap();
let text = resp["result"]["content"][0]["text"].as_str().expect("tool result text");
let mcp_res: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(mcp_res["stored_as"], "event");
assert_eq!(mcp_res["hash"], mcp_res["event"], "MCP keeps its documented `hash` key");
let shape = |db: &str, hash: &str| -> serde_json::Value {
let rows = cal(db, &format!(r#"RECALL events WHERE hash = "{hash}""#));
let g = grains(&rows)[0].clone();
assert_eq!(g["grain_type"], "event");
let f = &g["fields"];
serde_json::json!({
"content": f["content"], "session_id": f["session_id"],
"role": f["role"], "observer": f["observer"], "namespace": f["namespace"],
})
};
assert_eq!(
shape(&cli_db, cli_res["event"].as_str().unwrap()),
shape(&mcp_db, mcp_res["event"].as_str().unwrap()),
"same input must yield the same grain on both surfaces"
);
}
#[test]
fn a_typo_in_role_is_rejected_before_anything_is_written() {
let dir = TempDir::new().unwrap();
let db = dir.path().join("m.db");
let db = db.to_str().unwrap();
let (ok, _out, err) = areev(&[
"remember", "--db", db, "--ns", "caller", "--content", "hi", "--role", "usr",
]);
assert!(!ok, "a bad role must not be silently dropped");
assert!(err.contains("user|assistant|system|tool"), "error lists the roles: {err}");
assert_eq!(grains(&cal(db, "RECALL events")).len(), 0);
let (ok, _out, err) = areev(&[
"remember", "--db", db, "--ns", "caller", "--content", "hi", "--role", "usr", "--dry-run",
]);
assert!(!ok, "--dry-run must validate too: {err}");
}
#[test]
fn without_a_model_remember_stores_the_event_alone() {
let dir = TempDir::new().unwrap();
let db = dir.path().join("m.db");
let db = db.to_str().unwrap();
let (ok, res, err) = remember(db, "just a note", &[]);
assert!(ok, "remember failed: {err}");
assert_eq!(res["facts"].as_array().unwrap().len(), 0);
assert!(res["model"].is_null());
assert_eq!(res["event"].as_str().unwrap().len(), 64);
}