use std::sync::Arc;
use ipld_core::ipld::Ipld;
use mnem_core::id::{EdgeId, NodeId};
use mnem_core::objects::Edge;
use mnem_core::repo::ReadonlyRepo;
use mnem_core::store::{Blockstore, MemoryBlockstore, MemoryOpHeadsStore, OpHeadsStore};
fn fresh_repo() -> ReadonlyRepo {
let bs: Arc<dyn Blockstore> = Arc::new(MemoryBlockstore::new());
let ohs: Arc<dyn OpHeadsStore> = Arc::new(MemoryOpHeadsStore::new());
ReadonlyRepo::init(bs, ohs).expect("fresh repo init")
}
#[test]
fn commit_memory_writes_node_with_auto_stamped_temporal_props() {
let repo = fresh_repo();
let mut tx = repo.start_transaction();
let id = tx
.commit_memory(
"Note",
"morning meeting with alice",
[("topic".to_string(), Ipld::String("ops".into()))],
)
.expect("commit_memory ok");
let repo = tx.commit("agent", "note").expect("commit ok");
let node = repo
.lookup_node(&id)
.expect("lookup ok")
.expect("node present");
assert_eq!(node.ntype, "Note");
assert_eq!(node.summary.as_deref(), Some("morning meeting with alice"));
assert_eq!(
node.props.get("topic"),
Some(&Ipld::String("ops".into())),
"caller-supplied prop must round-trip"
);
let created = match node.props.get("mnem:created_at") {
Some(Ipld::Integer(n)) => *n,
other => panic!("expected Integer for mnem:created_at, got {other:?}"),
};
let updated = match node.props.get("mnem:updated_at") {
Some(Ipld::Integer(n)) => *n,
other => panic!("expected Integer for mnem:updated_at, got {other:?}"),
};
assert!(
created > 0 && updated > 0,
"temporal stamps must be positive (got created={created} updated={updated})"
);
assert_eq!(
created, updated,
"on first write, created_at and updated_at coincide"
);
}
#[test]
fn retrieve_surfaces_commit_memory_node_without_filter() {
let repo = fresh_repo();
let mut tx = repo.start_transaction();
let id = tx
.commit_memory(
"Note",
"morning meeting with alice",
[("topic".to_string(), Ipld::String("ops".into()))],
)
.unwrap();
let repo = tx.commit("agent", "note").unwrap();
let result = repo
.retrieve()
.label("Note")
.execute()
.expect("retrieve ok");
let ids: Vec<_> = result.items.iter().map(|i| i.node.id).collect();
assert!(
ids.contains(&id),
"unfiltered retrieve must surface the committed node, got ids={ids:?}"
);
}
#[test]
fn temporal_filter_excludes_future_bound_past() {
let repo = fresh_repo();
let mut tx = repo.start_transaction();
let id = tx
.commit_memory(
"Note",
"morning meeting with alice",
[("topic".to_string(), Ipld::String("ops".into()))],
)
.unwrap();
let repo = tx.commit("agent", "note").unwrap();
let result = repo
.retrieve()
.label("Note")
.where_created_before(1)
.execute()
.expect("retrieve ok");
let ids: Vec<_> = result.items.iter().map(|i| i.node.id).collect();
assert!(
!ids.contains(&id),
"where_created_before(past_t) must exclude the node, got ids={ids:?}"
);
}
#[test]
fn temporal_filter_includes_anything_after_zero() {
let repo = fresh_repo();
let mut tx = repo.start_transaction();
let id = tx
.commit_memory(
"Note",
"morning meeting with alice",
[("topic".to_string(), Ipld::String("ops".into()))],
)
.unwrap();
let repo = tx.commit("agent", "note").unwrap();
let result = repo
.retrieve()
.label("Note")
.where_created_after(0)
.execute()
.expect("retrieve ok");
let ids: Vec<_> = result.items.iter().map(|i| i.node.id).collect();
assert!(
ids.contains(&id),
"where_created_after(0) must surface the node, got ids={ids:?}"
);
}
#[test]
fn tombstone_excludes_node_and_include_tombstoned_surfaces_it() {
let repo = fresh_repo();
let mut tx = repo.start_transaction();
let id = tx
.commit_memory(
"Note",
"morning meeting with alice",
[("topic".to_string(), Ipld::String("ops".into()))],
)
.unwrap();
let repo = tx.commit("agent", "note").unwrap();
let mut tx2 = repo.start_transaction();
tx2.tombstone_node(id, "user revoked").unwrap();
let repo = tx2.commit("agent", "revoke").unwrap();
let result = repo
.retrieve()
.label("Note")
.execute()
.expect("retrieve ok");
let ids: Vec<_> = result.items.iter().map(|i| i.node.id).collect();
assert!(
!ids.contains(&id),
"tombstoned node must be excluded by default, got ids={ids:?}"
);
let result = repo
.retrieve()
.label("Note")
.include_tombstoned(true)
.execute()
.expect("retrieve ok");
let ids: Vec<_> = result.items.iter().map(|i| i.node.id).collect();
assert!(
ids.contains(&id),
"include_tombstoned(true) must surface the tombstoned node, got ids={ids:?}"
);
}
#[test]
fn edge_between_two_notes_surfaces_on_incoming_edges() {
let repo = fresh_repo();
let mut tx = repo.start_transaction();
let src_id = tx
.commit_memory(
"Note",
"source note",
[("role".to_string(), Ipld::String("src".into()))],
)
.unwrap();
let dst_id = tx
.commit_memory(
"Note",
"dest note",
[("role".to_string(), Ipld::String("dst".into()))],
)
.unwrap();
let edge = Edge::new(EdgeId::new_v7(), "references", src_id, dst_id);
let edge_id = edge.id;
tx.add_edge(&edge).unwrap();
let repo = tx.commit("agent", "notes + edge").unwrap();
let incoming = repo
.incoming_edges(&dst_id, None)
.expect("incoming_edges ok");
assert!(
incoming.iter().any(|e| e.id == edge_id
&& e.src == src_id
&& e.dst == dst_id
&& e.etype == "references"),
"incoming_edges(dst) must surface the src->dst edge, got {incoming:#?}"
);
}
#[test]
fn temporal_filter_is_lenient_on_nodes_without_reserved_props() {
let repo = fresh_repo();
let mut tx = repo.start_transaction();
let legacy = mnem_core::objects::Node::new(NodeId::new_v7(), "Note").with_summary("no stamp");
tx.add_node(&legacy).unwrap();
let repo = tx.commit("agent", "legacy").unwrap();
let result = repo
.retrieve()
.label("Note")
.where_created_after(10_000_000_000_000)
.where_created_before(10_000_000_000_001)
.where_updated_after(10_000_000_000_000)
.where_updated_before(10_000_000_000_001)
.execute()
.expect("retrieve ok");
let ids: Vec<_> = result.items.iter().map(|i| i.node.id).collect();
assert!(
ids.contains(&legacy.id),
"legacy node without reserved props must pass every temporal check (lenient rule), got ids={ids:?}"
);
}