use std::io::{Read as _, Write as _};
use std::net::TcpListener;
use std::path::PathBuf;
use std::process::{Command, Output};
use plugmem_host::{Config, Database};
struct TempDir(PathBuf);
impl TempDir {
fn new(tag: &str) -> Self {
let unique = format!(
"plugmem-cli-it-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let dir = std::env::temp_dir().join(unique);
std::fs::create_dir_all(&dir).unwrap();
Self(dir)
}
fn db(&self) -> PathBuf {
self.0.join("m.plugmem")
}
}
fn snapshot_file(base: &std::path::Path) -> PathBuf {
let m = std::fs::read(base).expect("manifest present");
let generation = u64::from_le_bytes(m[8..16].try_into().unwrap());
let mut p = base.to_path_buf().into_os_string();
p.push(format!(".snap.{generation}"));
PathBuf::from(p)
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn plugmem(db: &std::path::Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg("--db")
.arg(db)
.args(args)
.output()
.expect("run plugmem")
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(name)
}
#[test]
fn help_and_version_succeed() {
for flag in ["--help", "--version"] {
let out = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg(flag)
.output()
.unwrap();
assert!(out.status.success(), "{flag} should exit 0");
}
}
#[test]
fn no_command_is_a_usage_error() {
let out = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.output()
.unwrap();
assert_eq!(out.status.code(), Some(2));
}
#[test]
fn remember_recall_roundtrip() {
let tmp = TempDir::new("roundtrip");
let r = plugmem(
&tmp.db(),
&[
"remember",
"prefers tokio",
"--entity",
"user",
"--tag",
"pref",
],
);
assert_eq!(r.status.code(), Some(0));
assert!(
stdout(&r).starts_with("remembered fact 0"),
"{}",
stdout(&r)
);
let r = plugmem(&tmp.db(), &["recall", "tokio"]);
assert_eq!(r.status.code(), Some(0));
assert!(stdout(&r).contains("tokio"), "{}", stdout(&r));
}
#[test]
fn show_missing_is_exit_one() {
let tmp = TempDir::new("missing");
plugmem(&tmp.db(), &["remember", "a note"]);
let r = plugmem(&tmp.db(), &["show", "999"]);
assert_eq!(r.status.code(), Some(1));
assert!(stdout(&r).contains("not found"));
}
#[test]
fn json_output_is_parseable() {
let tmp = TempDir::new("json");
plugmem(
&tmp.db(),
&["remember", "uses tokio", "--entity", "plugmem"],
);
let r = plugmem(&tmp.db(), &["--json", "stats"]);
assert_eq!(r.status.code(), Some(0));
let v: serde_json::Value = serde_json::from_str(stdout(&r).trim()).unwrap();
assert_eq!(v["facts"], 1);
}
#[test]
fn env_var_selects_the_database() {
let tmp = TempDir::new("env");
let seed = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.env("PLUGMEM_DB", tmp.db())
.args(["remember", "from the env path"])
.output()
.unwrap();
assert_eq!(seed.status.code(), Some(0));
let read = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.env("PLUGMEM_DB", tmp.db())
.args(["show", "0"])
.output()
.unwrap();
assert!(
stdout(&read).contains("from the env path"),
"{}",
stdout(&read)
);
}
#[test]
fn a_locked_database_exits_one() {
let tmp = TempDir::new("locked");
let (_held, _report) = Database::open(tmp.db(), Config::default()).unwrap();
let r = plugmem(&tmp.db(), &["stats"]);
assert_eq!(r.status.code(), Some(1), "locked database → exit 1");
let msg = String::from_utf8_lossy(&r.stderr);
assert!(msg.contains("locked"), "stderr = {msg}");
}
#[test]
fn read_only_commands_coexist_with_a_live_writer() {
let tmp = TempDir::new("coexist");
assert!(
plugmem(&tmp.db(), &["remember", "a fact about tokio"])
.status
.success()
);
assert!(plugmem(&tmp.db(), &["checkpoint"]).status.success());
let (_writer, _r) = Database::open(tmp.db(), Config::default()).unwrap();
for cmd in [
["stats"].as_slice(),
&["show", "0"],
&["export"],
&["verify"],
&["recall", "tokio"],
] {
let r = plugmem(&tmp.db(), cmd);
assert_eq!(
r.status.code(),
Some(0),
"read command {cmd:?} must coexist with a live writer; stderr={}",
String::from_utf8_lossy(&r.stderr)
);
}
assert!(stdout(&plugmem(&tmp.db(), &["verify"])).contains("integrity ok"));
assert!(stdout(&plugmem(&tmp.db(), &["recall", "tokio"])).contains("tokio"));
assert_eq!(
plugmem(&tmp.db(), &["remember", "second writer"])
.status
.code(),
Some(1),
"a second writer is still locked out",
);
}
fn spawn_mock_embedder(dim: usize, responses: usize) -> (String, std::thread::JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let handle = std::thread::spawn(move || {
for _ in 0..responses {
let (mut sock, _) = listener.accept().unwrap();
let mut buf = vec![0u8; 65536];
let mut read = 0usize;
let body_start = loop {
read += sock.read(&mut buf[read..]).unwrap();
let head = String::from_utf8_lossy(&buf[..read]);
if let Some(at) = head.find("\r\n\r\n") {
let len: usize = head
.lines()
.find_map(|l| {
l.strip_prefix("content-length: ")
.or_else(|| l.strip_prefix("Content-Length: "))
})
.unwrap()
.trim()
.parse()
.unwrap();
if read >= at + 4 + len {
break at + 4;
}
}
};
let body: serde_json::Value = serde_json::from_slice(&buf[body_start..read]).unwrap();
let inputs = body["input"].as_array().unwrap();
let data: Vec<serde_json::Value> = inputs
.iter()
.enumerate()
.map(|(i, text)| {
let seed = text.as_str().unwrap().len() as f32;
let embedding: Vec<f32> = (0..dim).map(|j| (seed + j as f32).sin()).collect();
serde_json::json!({ "index": i, "embedding": embedding })
})
.collect();
let payload = serde_json::json!({ "data": data }).to_string();
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{payload}",
payload.len()
);
sock.write_all(response.as_bytes()).unwrap();
}
});
(format!("http://{addr}/v1/embeddings"), handle)
}
#[test]
fn recall_with_an_embedder_coexists_with_a_live_writer() {
let dim = 8;
let (url, server) = spawn_mock_embedder(dim, 2);
let tmp = TempDir::new("embed-coexist");
let config = tmp.0.join("config.toml");
std::fs::write(
&config,
format!("[engine]\ndim = {dim}\n\n[embedder]\nurl = \"{url}\"\nmodel = \"mock\"\n"),
)
.unwrap();
let plug = |args: &[&str]| {
Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg("--db")
.arg(tmp.db())
.arg("--config")
.arg(&config)
.args(args)
.output()
.unwrap()
};
assert!(
plug(&["remember", "a fact about tokio and работа"])
.status
.success()
);
assert!(plug(&["checkpoint"]).status.success());
let mut wcfg = Config::default();
wcfg.dim = dim;
let (_writer, _r) = Database::open(tmp.db(), wcfg).unwrap();
let r = plug(&["recall", "tokio"]);
assert_eq!(
r.status.code(),
Some(0),
"embedded recall must coexist with a live writer; stderr={}",
String::from_utf8_lossy(&r.stderr)
);
assert!(stdout(&r).contains("tokio"), "{}", stdout(&r));
server.join().unwrap();
}
#[test]
fn import_streams_the_file_in_batches_one_http_each() {
let dim = 8;
let (url, server) = spawn_mock_embedder(dim, 3);
let tmp = TempDir::new("import-chunk");
let config = tmp.0.join("config.toml");
std::fs::write(
&config,
format!("[engine]\ndim = {dim}\n\n[embedder]\nurl = \"{url}\"\nmodel = \"mock\"\n"),
)
.unwrap();
let facts = tmp.0.join("facts.jsonl");
let mut jsonl = String::new();
for i in 0..5 {
jsonl.push_str(&format!("{{\"text\":\"fact number {i}\"}}\n"));
}
std::fs::write(&facts, &jsonl).unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg("--db")
.arg(tmp.db())
.arg("--config")
.arg(&config)
.arg("import")
.arg(&facts)
.arg("--batch")
.arg("2")
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(0),
"stderr={}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
stdout(&out).contains("imported 5 facts"),
"{}",
stdout(&out)
);
server.join().unwrap();
}
#[test]
fn repl_session_over_the_binary_runs_and_checkpoints_on_exit() {
use std::io::Write as _;
use std::process::Stdio;
let tmp = TempDir::new("repl");
let mut child = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg("--db")
.arg(tmp.db())
.arg("repl")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"remember \"user likes tokio\"\nrecall tokio\nstats\nexit\n")
.unwrap();
let out = child.wait_with_output().unwrap();
assert!(out.status.success(), "repl exit: {:?}", out.status);
let so = String::from_utf8_lossy(&out.stdout);
assert!(so.contains("remembered fact 0"), "stdout={so}");
assert!(so.contains("tokio"), "stdout={so}");
let r = plugmem(&tmp.db(), &["recall", "tokio"]);
assert!(
r.status.success() && stdout(&r).contains("tokio"),
"post-repl recall: {}",
stdout(&r)
);
}
#[test]
fn config_file_is_accepted_and_missing_one_is_a_usage_error() {
let tmp = TempDir::new("config");
let with_config = |args: &[&str]| {
Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg("--db")
.arg(tmp.db())
.arg("--config")
.arg(fixture("config.toml"))
.args(args)
.output()
.unwrap()
};
assert_eq!(
with_config(&["remember", "hello from config", "--entity", "user"])
.status
.code(),
Some(0)
);
let r = with_config(&["recall", "config"]);
assert_eq!(r.status.code(), Some(0));
assert!(stdout(&r).contains("config"), "{}", stdout(&r));
let bad = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg("--db")
.arg(tmp.db())
.args(["--config", "/no/such/plugmem-config.toml", "stats"])
.output()
.unwrap();
assert_eq!(bad.status.code(), Some(2));
}
#[test]
fn embedder_enabled_env_overrides_a_configured_endpoint() {
let tmp = TempDir::new("embedder-enabled-env");
let config = tmp.0.join("config.toml");
std::fs::write(
&config,
"[engine]\ndim = 8\n[embedder]\nenabled = true\nurl = \"http://127.0.0.1:1/v1/embeddings\"\nmodel = \"mock\"\n",
)
.unwrap();
let disabled = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.args(["--config", config.to_str().unwrap(), "--db"])
.arg(tmp.db())
.args(["remember", "env-disabled"])
.env("PLUGMEM_EMBEDDER_ENABLED", "false")
.output()
.unwrap();
assert!(disabled.status.success(), "{}", stdout(&disabled));
let enabled = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.args(["--config", config.to_str().unwrap(), "--db"])
.arg(tmp.0.join("enabled.plugmem"))
.args(["remember", "env-enabled"])
.env("PLUGMEM_EMBEDDER_ENABLED", "true")
.output()
.unwrap();
assert!(!enabled.status.success());
}
#[test]
fn import_from_a_fixture_jsonl() {
let tmp = TempDir::new("import-fix");
let out = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg("--db")
.arg(tmp.db())
.arg("import")
.arg(fixture("facts.jsonl"))
.output()
.unwrap();
assert_eq!(out.status.code(), Some(0));
assert!(
stdout(&out).contains("imported 3 facts"),
"{}",
stdout(&out)
);
let r = plugmem(&tmp.db(), &["recall", "tokio"]);
assert!(
stdout(&r).contains("tokio") && stdout(&r).contains("#pref"),
"{}",
stdout(&r)
);
}
#[test]
fn verify_reports_a_clean_database() {
let tmp = TempDir::new("verify-ok");
plugmem(&tmp.db(), &["remember", "a clean fact"]);
let out = plugmem(&tmp.db(), &["verify"]);
assert!(out.status.success(), "verify of a clean db exits 0");
assert!(stdout(&out).contains("integrity ok"), "{}", stdout(&out));
}
#[test]
fn scrub_verifies_a_checkpointed_database() {
let tmp = TempDir::new("scrub-ok");
plugmem(&tmp.db(), &["remember", "a fact worth some bytes to scrub"]);
plugmem(&tmp.db(), &["checkpoint"]);
let out = plugmem(&tmp.db(), &["scrub"]);
assert!(
out.status.success(),
"scrub of a clean db exits 0: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(stdout(&out).contains("scrub ok"), "{}", stdout(&out));
}
#[test]
fn scrub_detects_on_disk_corruption() {
let tmp = TempDir::new("scrub-corrupt");
plugmem(&tmp.db(), &["remember", "a corruptible tokio fact"]);
plugmem(&tmp.db(), &["checkpoint"]);
let snap = snapshot_file(&tmp.db());
let mut bytes = std::fs::read(&snap).unwrap();
let at = bytes.windows(5).position(|w| w == b"tokio").unwrap();
bytes[at] ^= 0xFF;
std::fs::write(&snap, bytes).unwrap();
let out = plugmem(&tmp.db(), &["scrub"]);
assert_eq!(out.status.code(), Some(2), "corruption exits 2");
}
#[test]
fn recover_writes_a_clean_copy_and_preserves_the_source() {
let tmp = TempDir::new("recover");
plugmem(&tmp.db(), &["remember", "keep me"]);
plugmem(&tmp.db(), &["checkpoint"]); let dst = tmp.0.join("recovered.plugmem");
let out = plugmem(&tmp.db(), &["recover", dst.to_str().unwrap()]);
assert!(
out.status.success(),
"recover exits 0: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(stdout(&out).contains("recovered to"), "{}", stdout(&out));
assert!(dst.exists(), "the destination was written");
assert!(tmp.db().exists(), "the source is preserved");
assert!(plugmem(&dst, &["stats"]).status.success());
}
#[test]
fn recover_refuses_a_destination_equal_to_the_source() {
let tmp = TempDir::new("recover-same");
plugmem(&tmp.db(), &["remember", "a fact"]);
let out = plugmem(&tmp.db(), &["recover", tmp.db().to_str().unwrap()]);
assert_eq!(out.status.code(), Some(2), "dst == src is a usage error");
}
#[test]
fn the_token_budget_bounds_the_rendered_block() {
let tmp = TempDir::new("token-budget");
for i in 0..40 {
plugmem(
&tmp.db(),
&[
"remember",
&format!("fact number {i} about the deployment pipeline and its many stages"),
],
);
}
let generous = plugmem(&tmp.db(), &["recall", "deployment pipeline", "-k", "40"]);
let tight = plugmem(
&tmp.db(),
&[
"recall",
"deployment pipeline",
"-k",
"40",
"--token-budget",
"40",
],
);
assert!(generous.status.success() && tight.status.success());
let (big, small) = (stdout(&generous).len(), stdout(&tight).len());
assert!(
small < big,
"a tight budget must shrink the block: {small} vs {big} bytes"
);
}
#[test]
fn ef_is_accepted_and_does_not_change_a_lexical_answer() {
let tmp = TempDir::new("ef");
plugmem(&tmp.db(), &["remember", "the release ships on friday"]);
let plain = plugmem(&tmp.db(), &["recall", "release", "--json"]);
let with_ef = plugmem(&tmp.db(), &["recall", "release", "--ef", "64", "--json"]);
assert!(with_ef.status.success(), "--ef must be accepted");
assert_eq!(stdout(&plain), stdout(&with_ef));
}
#[test]
fn provenance_is_recorded_on_an_edge_and_comes_back_from_recall() {
let tmp = TempDir::new("provenance");
let out = plugmem(&tmp.db(), &["remember", "ann hired bob in march", "--json"]);
let source: serde_json::Value = serde_json::from_str(&stdout(&out)).expect("remember json");
let fact_id = source["id"].as_u64().expect("fact id");
let linked = plugmem(
&tmp.db(),
&[
"link",
"ann",
"hires",
"bob",
"--provenance",
&fact_id.to_string(),
],
);
assert!(linked.status.success(), "--provenance must be accepted");
let recalled = plugmem(&tmp.db(), &["recall", "--entity", "ann", "--json"]);
let res: serde_json::Value = serde_json::from_str(&stdout(&recalled)).expect("recall json");
let edges = res["edges"].as_array().expect("recall JSON carries edges");
assert!(!edges.is_empty(), "the graph source walked the edge");
assert!(
edges
.iter()
.any(|e| e["provenance"].as_u64() == Some(fact_id)),
"the edge names the fact it follows from: {edges:?}"
);
}
#[test]
fn an_edge_without_provenance_reports_none_rather_than_a_sentinel() {
let tmp = TempDir::new("no-provenance");
plugmem(&tmp.db(), &["link", "team", "hires", "bob"]);
let recalled = plugmem(&tmp.db(), &["recall", "--entity", "team", "--json"]);
let res: serde_json::Value = serde_json::from_str(&stdout(&recalled)).expect("recall json");
for edge in res["edges"].as_array().expect("edges") {
assert!(
edge["provenance"].is_null(),
"an unsourced edge reports null, not a sentinel: {edge}"
);
}
}
#[test]
fn the_fsync_policy_is_settable_from_the_config_file() {
let tmp = TempDir::new("fsync");
let cfg = tmp.0.join("config.toml");
std::fs::write(&cfg, "[maintenance]\nfsync = \"on_snapshot\"\n").unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg("--config")
.arg(&cfg)
.arg("--db")
.arg(tmp.db())
.args(["remember", "written under a relaxed fsync policy"])
.output()
.expect("run plugmem");
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
std::fs::write(&cfg, "[maintenance]\nfsync = \"on-snapshot\"\n").unwrap();
let bad = Command::new(env!("CARGO_BIN_EXE_plugmem-cli"))
.arg("--config")
.arg(&cfg)
.arg("--db")
.arg(tmp.db())
.args(["stats"])
.output()
.expect("run plugmem");
assert!(!bad.status.success(), "a bad fsync value must be refused");
}
#[test]
fn settings_help_lists_the_fsync_policy() {
let tmp = TempDir::new("settings-fsync");
let out = plugmem(&tmp.db(), &["help", "settings"]);
let text = stdout(&out);
assert!(
text.contains("fsync"),
"the settings catalogue documents every key it parses"
);
}
#[test]
fn the_round_trip_carries_edges_and_their_provenance() {
let src = TempDir::new("rt-src");
let out = plugmem(&src.db(), &["remember", "ann hired bob in march", "--json"]);
let fact: serde_json::Value = serde_json::from_str(&stdout(&out)).expect("remember json");
let fact_id = fact["id"].as_u64().expect("fact id");
plugmem(&src.db(), &["remember", "bob reviews the notes"]);
plugmem(
&src.db(),
&[
"link",
"ann",
"hires",
"bob",
"--provenance",
&fact_id.to_string(),
],
);
plugmem(&src.db(), &["link", "ann", "knows", "bob"]);
let dump = plugmem(&src.db(), &["export"]);
let text = stdout(&dump);
let facts = text
.lines()
.filter(|l| l.contains("\"kind\":\"fact\""))
.count();
let edges = text
.lines()
.filter(|l| l.contains("\"kind\":\"edge\""))
.count();
assert_eq!((facts, edges), (2, 2), "both kinds are written:\n{text}");
let kinds: Vec<&str> = text.lines().collect();
let last_fact = kinds
.iter()
.rposition(|l| l.contains("\"kind\":\"fact\""))
.unwrap();
let first_edge = kinds
.iter()
.position(|l| l.contains("\"kind\":\"edge\""))
.unwrap();
assert!(last_fact < first_edge, "facts must come before edges");
let dst = TempDir::new("rt-dst");
let file = src.0.join("dump.jsonl");
std::fs::write(&file, &text).unwrap();
let imported = plugmem(&dst.db(), &["import", file.to_str().unwrap(), "--json"]);
let report: serde_json::Value = serde_json::from_str(&stdout(&imported)).expect("import json");
assert_eq!(report["imported"], 2);
assert_eq!(report["edges"], 2, "edges are imported, not skipped");
let recalled = plugmem(&dst.db(), &["recall", "--entity", "ann", "--json"]);
let res: serde_json::Value = serde_json::from_str(&stdout(&recalled)).expect("recall json");
let got = res["edges"].as_array().expect("edges");
assert_eq!(got.len(), 2, "both edges came back");
assert_eq!(
got.iter().filter(|e| !e["provenance"].is_null()).count(),
1,
"exactly the sourced edge kept its provenance: {got:?}"
);
}
#[test]
fn provenance_is_retargeted_to_the_id_the_new_database_assigns() {
let src = TempDir::new("remap-src");
let out = plugmem(&src.db(), &["remember", "ann hired bob", "--json"]);
let old_id = serde_json::from_str::<serde_json::Value>(&stdout(&out)).unwrap()["id"]
.as_u64()
.unwrap();
plugmem(
&src.db(),
&[
"link",
"ann",
"hires",
"bob",
"--provenance",
&old_id.to_string(),
],
);
let file = src.0.join("dump.jsonl");
std::fs::write(&file, stdout(&plugmem(&src.db(), &["export"]))).unwrap();
let dst = TempDir::new("remap-dst");
for i in 0..3 {
plugmem(&dst.db(), &["remember", &format!("filler {i}")]);
}
plugmem(&dst.db(), &["import", file.to_str().unwrap()]);
let hit = plugmem(&dst.db(), &["recall", "ann hired", "--json"]);
let new_id =
serde_json::from_str::<serde_json::Value>(&stdout(&hit)).unwrap()["facts"][0]["id"]
.as_u64()
.unwrap();
assert_ne!(new_id, old_id, "the destination must assign a different id");
let recalled = plugmem(&dst.db(), &["recall", "--entity", "ann", "--json"]);
let res: serde_json::Value = serde_json::from_str(&stdout(&recalled)).unwrap();
assert!(
res["edges"]
.as_array()
.unwrap()
.iter()
.any(|e| e["provenance"].as_u64() == Some(new_id)),
"provenance follows the fact to its new id: {}",
res["edges"]
);
}
#[test]
fn a_file_written_before_edges_existed_still_imports() {
let tmp = TempDir::new("legacy");
let file = tmp.0.join("legacy.jsonl");
std::fs::write(
&file,
"{\"text\":\"legacy fact\",\"entity\":\"old\",\"tags\":[\"x\"],\"valid_from\":42}\n",
)
.unwrap();
let out = plugmem(&tmp.db(), &["import", file.to_str().unwrap(), "--json"]);
let report: serde_json::Value = serde_json::from_str(&stdout(&out)).expect("import json");
assert_eq!(report["imported"], 1);
assert_eq!(report["edges"], 0);
let hit = plugmem(&tmp.db(), &["recall", "legacy", "--json"]);
let res: serde_json::Value = serde_json::from_str(&stdout(&hit)).unwrap();
assert_eq!(
res["facts"][0]["valid_from"], 42,
"the old fields still apply"
);
}
#[test]
fn an_unknown_line_kind_is_refused_rather_than_skipped() {
let tmp = TempDir::new("bad-kind");
let file = tmp.0.join("bad.jsonl");
std::fs::write(&file, "{\"kind\":\"vector\",\"text\":\"x\"}\n").unwrap();
let out = plugmem(&tmp.db(), &["import", file.to_str().unwrap()]);
assert_eq!(
out.status.code(),
Some(2),
"an unknown kind is a usage error"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("line 1"), "the message names the line: {err}");
}