use memnite_cli::{AddSpec, App, EventCtx, UpdatePatch};
use memnite_core::Scope;
fn ctx() -> EventCtx {
EventCtx {
ts: "2026-06-27T12:00:00Z".to_string(),
engine: "cli".to_string(),
machine: "test".to_string(),
}
}
fn ctx_at(ts: &str) -> EventCtx {
EventCtx {
ts: ts.to_string(),
engine: "cli".to_string(),
machine: "test".to_string(),
}
}
fn spec(title: &str) -> AddSpec {
AddSpec {
title: title.to_string(),
body: "body".to_string(),
mem_type: "decision".to_string(),
scope: Scope::Agent,
project: "proj".to_string(),
topic_key: None,
anchors: vec![],
tags: vec![],
}
}
#[test]
fn export_writes_one_json_line_per_event() {
let dir = tempfile::tempdir().unwrap();
let root = tempfile::tempdir().unwrap();
let app = App::open(dir.path()).unwrap();
app.add(spec("m1"), root.path(), &ctx()).unwrap();
app.add(spec("m2"), root.path(), &ctx()).unwrap();
let mut buf = Vec::new();
let n = app.export(None, &mut buf).unwrap();
assert_eq!(n, 2);
let text = String::from_utf8(buf).unwrap();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines.len(), 2);
let l0: memnite_core::Event = serde_json::from_str(lines[0]).unwrap();
let l1: memnite_core::Event = serde_json::from_str(lines[1]).unwrap();
assert!(l0.lamport <= l1.lamport);
}
#[test]
fn export_since_filters_by_lamport() {
let dir = tempfile::tempdir().unwrap();
let root = tempfile::tempdir().unwrap();
let app = App::open(dir.path()).unwrap();
app.add(spec("m1"), root.path(), &ctx()).unwrap(); app.add(spec("m2"), root.path(), &ctx()).unwrap(); app.add(spec("m3"), root.path(), &ctx()).unwrap();
let mut all = Vec::new();
assert_eq!(app.export(None, &mut all).unwrap(), 3);
let mut some = Vec::new();
assert_eq!(app.export(Some(2), &mut some).unwrap(), 2); }
fn patch_title(t: &str) -> UpdatePatch {
UpdatePatch {
title: Some(t.to_string()),
..Default::default()
}
}
#[test]
fn export_import_roundtrip_into_empty() {
let dira = tempfile::tempdir().unwrap();
let dirb = tempfile::tempdir().unwrap();
let root = tempfile::tempdir().unwrap();
let a = App::open(dira.path()).unwrap();
a.add(spec("voyage embeddings"), root.path(), &ctx())
.unwrap();
a.add(spec("stripe billing"), root.path(), &ctx()).unwrap();
let mut bundle = Vec::new();
a.export(None, &mut bundle).unwrap();
let b = App::open(dirb.path()).unwrap();
let sum = b.import(bundle.as_slice()).unwrap();
assert_eq!(sum.imported, 2);
assert_eq!(b.search("voyage").unwrap().len(), 1);
assert_eq!(b.search("stripe").unwrap().len(), 1);
}
#[test]
fn import_is_idempotent() {
let dira = tempfile::tempdir().unwrap();
let dirb = tempfile::tempdir().unwrap();
let root = tempfile::tempdir().unwrap();
let a = App::open(dira.path()).unwrap();
a.add(spec("m1"), root.path(), &ctx()).unwrap();
a.add(spec("m2"), root.path(), &ctx()).unwrap();
let mut bundle = Vec::new();
a.export(None, &mut bundle).unwrap();
let b = App::open(dirb.path()).unwrap();
let s1 = b.import(bundle.as_slice()).unwrap();
assert_eq!(s1.imported, 2);
assert_eq!(s1.duplicates, 0);
let s2 = b.import(bundle.as_slice()).unwrap();
assert_eq!(s2.imported, 0);
assert_eq!(s2.duplicates, 2);
}
#[test]
fn import_skips_corrupt_lines() {
let dira = tempfile::tempdir().unwrap();
let dirb = tempfile::tempdir().unwrap();
let root = tempfile::tempdir().unwrap();
let a = App::open(dira.path()).unwrap();
a.add(spec("good"), root.path(), &ctx()).unwrap();
let mut bundle = Vec::new();
a.export(None, &mut bundle).unwrap();
let mut text = String::from_utf8(bundle).unwrap();
text.push_str("this is not json\n");
let b = App::open(dirb.path()).unwrap();
let sum = b.import(text.as_bytes()).unwrap();
assert_eq!(sum.imported, 1);
assert_eq!(sum.corrupt, 1);
}
#[test]
fn import_event_with_lower_lamport_rebuilds_correctly() {
let root = tempfile::tempdir().unwrap();
let dira = tempfile::tempdir().unwrap();
let dirb = tempfile::tempdir().unwrap();
let a = App::open(dira.path()).unwrap();
a.add(spec("a-mem"), root.path(), &ctx()).unwrap(); let mut bundle = Vec::new();
a.export(None, &mut bundle).unwrap();
let b = App::open(dirb.path()).unwrap();
b.add(spec("b-mem-1"), root.path(), &ctx()).unwrap(); b.add(spec("b-mem-2"), root.path(), &ctx()).unwrap(); b.import(bundle.as_slice()).unwrap();
assert_eq!(b.search("a-mem").unwrap().len(), 1);
assert_eq!(b.search("b-mem-1").unwrap().len(), 1);
assert_eq!(b.search("b-mem-2").unwrap().len(), 1);
}
#[test]
fn merge_converges_deterministically() {
let dira = tempfile::tempdir().unwrap();
let dirb = tempfile::tempdir().unwrap();
let root = tempfile::tempdir().unwrap();
let a = App::open(dira.path()).unwrap();
let id = a.add(spec("base"), root.path(), &ctx()).unwrap().memory_id;
let mut seed = Vec::new();
a.export(None, &mut seed).unwrap();
let b = App::open(dirb.path()).unwrap();
b.import(seed.as_slice()).unwrap();
a.update(&id, patch_title("from-A"), root.path(), &ctx())
.unwrap();
b.update(&id, patch_title("from-B"), root.path(), &ctx())
.unwrap();
let mut ba = Vec::new();
a.export(None, &mut ba).unwrap();
let mut bb = Vec::new();
b.export(None, &mut bb).unwrap();
a.import(bb.as_slice()).unwrap();
b.import(ba.as_slice()).unwrap();
let ta = a.get(&id).unwrap().unwrap().title;
let tb = b.get(&id).unwrap().unwrap().title;
assert_eq!(ta, tb);
assert!(ta == "from-A" || ta == "from-B");
}
#[test]
fn import_empty_bundle_is_noop() {
let dir = tempfile::tempdir().unwrap();
let app = App::open(dir.path()).unwrap();
let sum = app.import(b"".as_slice()).unwrap();
assert_eq!(sum.imported, 0);
assert_eq!(sum.duplicates, 0);
assert_eq!(sum.corrupt, 0);
}
#[test]
fn import_blank_lines_only_is_noop() {
let dir = tempfile::tempdir().unwrap();
let app = App::open(dir.path()).unwrap();
let sum = app.import(b"\n \n\n".as_slice()).unwrap();
assert_eq!(sum.imported, 0);
assert_eq!(sum.corrupt, 0);
}
#[test]
fn reviewed_event_converges_across_sync() {
let dira = tempfile::tempdir().unwrap();
let dirb = tempfile::tempdir().unwrap();
let root = tempfile::tempdir().unwrap();
let a = App::open(dira.path()).unwrap();
let id = a
.add(
spec("ancient decision"),
root.path(),
&ctx_at("2020-01-01T00:00:00Z"),
)
.unwrap()
.memory_id;
a.mark(&id, &ctx_at("2026-07-01T00:00:00Z")).unwrap();
let mut buf = Vec::new();
a.export(None, &mut buf).unwrap();
let b = App::open(dirb.path()).unwrap();
b.import(std::io::Cursor::new(buf)).unwrap();
let on_b = b.get(&id).unwrap().unwrap();
assert_eq!(on_b.updated_ts, "2026-07-01T00:00:00Z");
assert_eq!(on_b.status, "stable");
assert!(b
.review_list("2026-07-10T00:00:00Z", None, None)
.unwrap()
.is_empty());
}