use mnemo_engine::{
CacheBudget, CacheFlushPolicy, CachePutOpts, KdfParams, Memory, MemoryType, Metric, Mnemo,
MnemoConfig, RecallRequest, ScoreWeights, SemanticCachePutOpts, Turn,
DEFAULT_SEMANTIC_THRESHOLD,
};
use tempfile::tempdir;
fn fast_cfg(dimensions: usize) -> MnemoConfig {
MnemoConfig {
dimensions,
kdf: KdfParams::fast(),
..Default::default()
}
}
fn vec4(a: f32, b: f32, c: f32, d: f32) -> Vec<f32> {
vec![a, b, c, d]
}
#[test]
fn create_insert_search_reopen_recall() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
assert!(db.is_empty());
let id = db
.remember(Memory::new(
"hello world",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.remember(Memory::new(
"goodbye",
MemoryType::Episodic,
vec4(0.0, 1.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
assert_eq!(db.len(), 2);
let hits = db
.search(&vec4(0.9, 0.1, 0.0, 0.0), 1, Metric::Cosine)
.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].0.content, "hello world");
let got = db.get(&id).unwrap();
assert_eq!(got.content, "hello world");
db.close().unwrap();
drop(db);
let mut db = Mnemo::open(&path, "pw").unwrap();
assert_eq!(db.len(), 2);
let res = db
.recall(&RecallRequest::new(vec4(0.9, 0.1, 0.0, 0.0)).top_k(2))
.unwrap();
assert_eq!(res.len(), 2);
assert_eq!(res[0].memory.content, "hello world");
}
#[test]
fn header_tamper_is_detected_by_v7_seal() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("tamper.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"x",
MemoryType::Working,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
db.close().unwrap();
drop(db);
const SEAL_TAG_OFF: usize = 254;
let mut bytes = read_bytes(&path);
bytes[SEAL_TAG_OFF] ^= 0x01;
write_bytes(&path, &bytes);
match Mnemo::open(&path, "pw") {
Err(MnemoError::HeaderTampered) => {}
Err(other) => panic!("expected HeaderTampered, got {other:?}"),
Ok(_) => panic!("expected Mnemo::open to fail after seal tamper, but it succeeded"),
}
}
#[test]
fn page_swap_attack_is_detected_by_aad() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("swap.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let id_a = db
.remember(Memory::new(
"ALPHA_AAA",
MemoryType::Working,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
let id_b = db
.remember(Memory::new(
"BETA_BBBB",
MemoryType::Working,
vec4(0.0, 1.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
let _ = db.get(&id_a).unwrap();
let _ = db.get(&id_b).unwrap();
db.close().unwrap();
drop(db);
const PAGE_SIZE: usize = 8192;
const PAGE_A: usize = 9;
const PAGE_B: usize = 10;
let mut bytes = read_bytes(&path);
let off_a = PAGE_A * PAGE_SIZE;
let off_b = PAGE_B * PAGE_SIZE;
assert!(
off_b + PAGE_SIZE <= bytes.len(),
"file too short for expected layout"
);
let nonce_a: [u8; 12] = bytes[off_a..off_a + 12].try_into().unwrap();
let nonce_b: [u8; 12] = bytes[off_b..off_b + 12].try_into().unwrap();
assert_ne!(
nonce_a, nonce_b,
"pages {PAGE_A} and {PAGE_B} should have distinct nonces — defaults moved?"
);
let (left, right) = bytes.split_at_mut(off_b);
left[off_a..off_a + PAGE_SIZE].swap_with_slice(&mut right[..PAGE_SIZE]);
write_bytes(&path, &bytes);
let mut db = Mnemo::open(&path, "pw").unwrap();
let err_a = db.get(&id_a).unwrap_err();
let err_b = db.get(&id_b).unwrap_err();
let is_auth_fail = |e: &MnemoError| matches!(e, MnemoError::PageAuthFailed(_));
assert!(
is_auth_fail(&err_a) || is_auth_fail(&err_b),
"at least one of the swapped pages must fail auth; got {err_a:?} / {err_b:?}"
);
}
#[test]
fn recall_does_not_rewrite_records() {
use std::fs;
let dir = tempdir().unwrap();
let path = dir.path().join("ro.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"a",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.remember(Memory::new(
"b",
MemoryType::Semantic,
vec4(0.0, 1.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
let baseline = fs::metadata(&path).unwrap().len();
for _ in 0..50 {
let req = RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0))
.top_k(2)
.track_access(false);
let _ = db.recall(&req).unwrap();
}
db.flush().unwrap();
assert_eq!(
fs::metadata(&path).unwrap().len(),
baseline,
"track_access=false recalls should not grow the file at all"
);
let mut prev = baseline;
for i in 0..20 {
let req = RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0)).top_k(2);
let _ = db.recall(&req).unwrap();
db.flush().unwrap();
let now = fs::metadata(&path).unwrap().len();
let growth = now - prev;
assert!(
growth < 50_000,
"iter {i}: per-recall+flush growth {growth} bytes — pre-v5 catalog rewrite path?"
);
prev = now;
}
let hits = db
.recall(
&RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0))
.top_k(1)
.track_access(false),
)
.unwrap();
assert!(
hits[0].memory.access_count > 0,
"track_access(true) recalls should have bumped access_count via the catalog"
);
}
#[test]
fn file_is_encrypted_at_rest() {
let dir = tempdir().unwrap();
let path = dir.path().join("enc.mnemo");
let secret = "TOP-SECRET-PLAINTEXT-MARKER";
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
secret,
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
db.close().unwrap();
drop(db);
let raw = std::fs::read(&path).unwrap();
let needle = secret.as_bytes();
let found = raw.windows(needle.len()).any(|w| w == needle);
assert!(
!found,
"plaintext content must not appear in the on-disk file"
);
}
#[test]
fn wrong_passphrase_is_rejected() {
let dir = tempdir().unwrap();
let path = dir.path().join("wp.mnemo");
let mut db = Mnemo::create(&path, "correct", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"x",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.close().unwrap();
drop(db);
assert!(Mnemo::open(&path, "wrong").is_err());
assert!(Mnemo::open(&path, "correct").is_ok());
}
#[test]
fn rekey_changes_the_passphrase() {
let dir = tempdir().unwrap();
let path = dir.path().join("rk.mnemo");
let mut db = Mnemo::create(&path, "old-pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"durable",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
db.rekey("new-pw", KdfParams::fast()).unwrap();
db.close().unwrap();
drop(db);
assert!(
Mnemo::open(&path, "old-pw").is_err(),
"old passphrase must fail"
);
let mut db = Mnemo::open(&path, "new-pw").unwrap();
assert_eq!(db.len(), 1);
let id = db.memories().unwrap()[0].id;
assert_eq!(db.get(&id).unwrap().content, "durable");
}
#[test]
fn copy_on_write_crash_safety() {
let dir = tempdir().unwrap();
let path = dir.path().join("cow.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"state-a",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
db.remember(Memory::new(
"uncommitted",
MemoryType::Semantic,
vec4(0.0, 1.0, 0.0, 0.0),
))
.unwrap();
drop(db);
let mut db = Mnemo::open(&path, "pw").unwrap();
assert_eq!(db.len(), 1, "only the flushed state should survive");
assert_eq!(db.memories().unwrap()[0].content, "state-a");
assert_eq!(
db.verify().unwrap(),
1,
"surviving record must decrypt and decode"
);
}
#[test]
fn ttl_expiry_hides_memories() {
let dir = tempdir().unwrap();
let path = dir.path().join("ttl.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(
Memory::new("ephemeral", MemoryType::Working, vec4(1.0, 0.0, 0.0, 0.0)).with_ttl(0),
)
.unwrap();
db.remember(Memory::new(
"permanent",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
let res = db
.recall(&RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0)).top_k(10))
.unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].memory.content, "permanent");
}
#[test]
fn delete_then_compact_drops_tombstones() {
let dir = tempdir().unwrap();
let path = dir.path().join("cmp.mnemo");
let path_str = path.to_str().unwrap();
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let keep = db
.remember(Memory::new(
"keep",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
let drop_id = db
.remember(Memory::new(
"drop",
MemoryType::Semantic,
vec4(0.0, 1.0, 0.0, 0.0),
))
.unwrap();
db.delete(&drop_id).unwrap();
db.flush().unwrap();
assert_eq!(db.len(), 1);
db.close().unwrap();
drop(db);
let report = Mnemo::compact_file(path_str, "pw").unwrap();
assert_eq!(report.before, 1);
assert_eq!(report.after, 1);
let mut db = Mnemo::open(&path, "pw").unwrap();
assert_eq!(db.len(), 1);
assert!(db.get(&keep).is_ok());
}
#[test]
fn recall_ranking_uses_importance() {
let dir = tempdir().unwrap();
let path = dir.path().join("rank.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(
Memory::new(
"low importance",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
)
.with_importance(0.0),
)
.unwrap();
db.remember(
Memory::new(
"high importance",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
)
.with_importance(1.0),
)
.unwrap();
db.flush().unwrap();
let res = db
.recall(&RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0)).top_k(2))
.unwrap();
assert_eq!(res.len(), 2);
assert_eq!(
res[0].memory.content, "high importance",
"with equal similarity, importance should break the tie"
);
assert!(res[0].score >= res[1].score);
}
#[test]
fn agent_scoping_filters_recall() {
let dir = tempdir().unwrap();
let path = dir.path().join("agent.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(
Memory::new(
"alice private",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
)
.with_agent("alice"),
)
.unwrap();
db.remember(
Memory::new(
"bob private",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
)
.with_agent("bob"),
)
.unwrap();
db.remember(
Memory::new(
"shared note",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
)
.with_agent("bob")
.with_scope(mnemo_engine::Scope::Shared),
)
.unwrap();
db.flush().unwrap();
let res = db
.recall(
&RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0))
.top_k(10)
.agent("alice"),
)
.unwrap();
let contents: Vec<&str> = res.iter().map(|r| r.memory.content.as_str()).collect();
assert!(contents.contains(&"alice private"));
assert!(contents.contains(&"shared note"));
assert!(
!contents.contains(&"bob private"),
"alice must not see bob's private memory"
);
}
#[test]
fn dimension_mismatch_is_rejected() {
let dir = tempdir().unwrap();
let path = dir.path().join("dim.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let bad = Memory::new("wrong size", MemoryType::Semantic, vec![1.0, 2.0]);
assert!(db.remember(bad).is_err());
}
#[test]
fn type_filter_restricts_recall() {
let dir = tempdir().unwrap();
let path = dir.path().join("type.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"a fact",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.remember(Memory::new(
"an event",
MemoryType::Episodic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
let res = db
.recall(
&RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0))
.top_k(10)
.types(vec![MemoryType::Episodic]),
)
.unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].memory.memory_type, MemoryType::Episodic);
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed.max(1))
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn unit(&mut self) -> f32 {
(self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
}
fn normalish(&mut self) -> f32 {
self.unit() + self.unit() + self.unit() + self.unit() - 2.0
}
}
fn sim_only() -> ScoreWeights {
ScoreWeights {
alpha: 1.0,
beta: 0.0,
gamma: 0.0,
delta: 0.0,
half_life_secs: 1.0,
}
}
#[test]
fn ann_index_recall_quality() {
let dir = tempdir().unwrap();
let path = dir.path().join("ann.mnemo");
let dims = 32;
let mut db = Mnemo::create(&path, "pw", fast_cfg(dims)).unwrap();
let mut rng = Rng::new(0xC0FFEE);
let n_clusters = 20;
let per_cluster = 30;
let centers: Vec<Vec<f32>> = (0..n_clusters)
.map(|_| (0..dims).map(|_| rng.normalish() * 5.0).collect())
.collect();
for c in ¢ers {
for _ in 0..per_cluster {
let v: Vec<f32> = c.iter().map(|&x| x + rng.normalish() * 0.3).collect();
db.remember(Memory::new("vec", MemoryType::Semantic, v))
.unwrap();
}
}
db.flush().unwrap();
let n = db.len();
assert_eq!(n, n_clusters * per_cluster);
let queries: Vec<Vec<f32>> = (0..30)
.map(|_| {
let ci = (rng.next_u64() as usize) % n_clusters;
centers[ci]
.iter()
.map(|&x| x + rng.normalish() * 0.3)
.collect()
})
.collect();
let exact: Vec<std::collections::HashSet<u128>> = queries
.iter()
.map(|q| {
db.search(q, 10, Metric::L2)
.unwrap()
.iter()
.map(|(m, _)| m.id.0)
.collect()
})
.collect();
let info = db.build_index().unwrap();
db.flush().unwrap();
assert!(db.has_index());
assert_eq!(info.vectors, n);
assert!(info.partitions >= 1 && info.subspaces >= 1);
let mut overlap = 0usize;
let mut total = 0usize;
for (q, want) in queries.iter().zip(&exact) {
let mut req = RecallRequest::new(q.clone())
.top_k(10)
.n_probe(20)
.n_rerank(200);
req.metric = Metric::L2;
req.weights = sim_only();
for h in db.recall(&req).unwrap() {
if want.contains(&h.memory.id.0) {
overlap += 1;
}
}
total += want.len();
}
let recall = overlap as f64 / total as f64;
assert!(
recall >= 0.95,
"ANN recall@10 was {recall:.3}, expected >= 0.95"
);
}
#[test]
fn index_survives_reopen() {
let dir = tempdir().unwrap();
let path = dir.path().join("persist-idx.mnemo");
let dims = 16;
let mut db = Mnemo::create(&path, "pw", fast_cfg(dims)).unwrap();
let mut rng = Rng::new(7);
for _ in 0..120 {
let v: Vec<f32> = (0..dims).map(|_| rng.normalish()).collect();
db.remember(Memory::new("m", MemoryType::Semantic, v))
.unwrap();
}
db.build_index().unwrap();
db.close().unwrap();
drop(db);
let mut db = Mnemo::open(&path, "pw").unwrap();
assert!(db.has_index(), "index must survive reopen");
let q: Vec<f32> = (0..dims).map(|_| rng.normalish()).collect();
let hits = db.recall(&RecallRequest::new(q).top_k(5)).unwrap();
assert!(!hits.is_empty());
}
#[test]
fn index_incremental_insert_is_searchable() {
let dir = tempdir().unwrap();
let path = dir.path().join("incremental.mnemo");
let dims = 16;
let mut db = Mnemo::create(&path, "pw", fast_cfg(dims)).unwrap();
let mut rng = Rng::new(99);
for _ in 0..100 {
let v: Vec<f32> = (0..dims).map(|_| rng.normalish()).collect();
db.remember(Memory::new("old", MemoryType::Semantic, v))
.unwrap();
}
db.build_index().unwrap();
let needle = vec![9.0f32; dims];
let needle_id = db
.remember(Memory::new("needle", MemoryType::Semantic, needle.clone()))
.unwrap();
db.flush().unwrap();
let mut req = RecallRequest::new(needle).top_k(1).n_probe(8).n_rerank(64);
req.metric = Metric::L2;
req.weights = sim_only();
let hits = db.recall(&req).unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(
hits[0].memory.id, needle_id,
"post-build insert must be indexed"
);
}
#[test]
fn index_drop_and_compact_rebuild() {
let dir = tempdir().unwrap();
let path = dir.path().join("idx-lifecycle.mnemo");
let path_str = path.to_str().unwrap();
let dims = 16;
let mut db = Mnemo::create(&path, "pw", fast_cfg(dims)).unwrap();
let mut rng = Rng::new(55);
for _ in 0..80 {
let v: Vec<f32> = (0..dims).map(|_| rng.normalish()).collect();
db.remember(Memory::new("m", MemoryType::Semantic, v))
.unwrap();
}
db.build_index().unwrap();
db.drop_index();
db.close().unwrap();
drop(db);
let mut db = Mnemo::open(&path, "pw").unwrap();
assert!(!db.has_index());
db.build_index().unwrap();
db.close().unwrap();
drop(db);
Mnemo::compact_file(path_str, "pw").unwrap();
let db = Mnemo::open(&path, "pw").unwrap();
assert!(db.has_index(), "compaction must rebuild the index");
}
fn read_bytes(p: &std::path::Path) -> Vec<u8> {
std::fs::read(p).unwrap()
}
fn write_bytes(p: &std::path::Path, b: &[u8]) {
std::fs::write(p, b).unwrap()
}
#[test]
fn wal_crash_recovery_replays_committed_txn() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
for i in 0..5 {
db.remember(Memory::new(
format!("mem-{i}"),
MemoryType::Semantic,
vec4((i as f32) + 1.0, 0.0, 0.0, 0.0),
))
.unwrap();
}
db.close().unwrap();
drop(db);
let snapshot = read_bytes(&path);
let mut db = Mnemo::open(&path, "pw").unwrap();
for i in 5..8 {
db.remember(Memory::new(
format!("mem-{i}"),
MemoryType::Semantic,
vec4((i as f32) + 1.0, 0.0, 0.0, 0.0),
))
.unwrap();
}
db.close().unwrap();
drop(db);
let file_b = read_bytes(&path);
let franken = dir.path().join("franken.mnemo");
let mut bytes = file_b.clone();
bytes[0..8192].copy_from_slice(&snapshot[0..8192]);
write_bytes(&franken, &bytes);
let mut db = Mnemo::open(&franken, "pw").unwrap();
assert_eq!(db.len(), 8, "WAL recovery must restore the committed txn");
let contents: Vec<String> = db
.memories()
.unwrap()
.into_iter()
.map(|m| m.content)
.collect();
assert!(contents.contains(&"mem-7".to_string()));
}
#[test]
fn wal_heals_torn_header() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
for i in 0..6 {
db.remember(Memory::new(
format!("k-{i}"),
MemoryType::Episodic,
vec4(i as f32, 1.0, 2.0, 3.0),
))
.unwrap();
}
db.close().unwrap();
drop(db);
let mut bytes = read_bytes(&path);
for b in bytes.iter_mut().take(260).skip(120) {
*b ^= 0xFF;
}
write_bytes(&path, &bytes);
let db = Mnemo::open(&path, "pw").unwrap();
assert_eq!(db.len(), 6, "torn header must self-heal from the WAL");
}
#[test]
fn wal_discards_uncommitted_garbage() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
for i in 0..5 {
db.remember(Memory::new(
format!("g-{i}"),
MemoryType::Working,
vec4((i as f32) + 1.0, 0.0, 0.0, 0.0),
))
.unwrap();
}
db.close().unwrap();
drop(db);
let mut bytes = read_bytes(&path);
let wal_pages = u64::from_le_bytes(bytes[194..202].try_into().unwrap()) as usize;
let region_end = (1 + wal_pages) * 8192;
for b in bytes.iter_mut().take(region_end).skip(8192) {
*b = 0xFF;
}
write_bytes(&path, &bytes);
let db = Mnemo::open(&path, "pw").unwrap();
assert_eq!(
db.len(),
5,
"a garbage WAL must not disturb a checkpointed db"
);
}
#[test]
fn wal_region_grows_for_large_catalog() {
let dir = tempdir().unwrap();
let path = dir.path().join("big.mnemo");
let n = 22_000;
let mut db = Mnemo::create(&path, "pw", fast_cfg(2)).unwrap();
let mut rng = Rng::new(7);
for _ in 0..n {
let v = vec![rng.unit(), rng.unit()];
db.remember(Memory::new("x", MemoryType::Semantic, v))
.unwrap();
}
db.close().unwrap();
drop(db);
let bytes = read_bytes(&path);
let wal_pages = u64::from_le_bytes(bytes[194..202].try_into().unwrap());
assert!(
wal_pages > 64,
"WAL should have grown well past the default (got {wal_pages})"
);
let db = Mnemo::open(&path, "pw").unwrap();
assert_eq!(db.len(), n);
}
#[test]
fn fresh_file_uses_small_wal_by_default() {
use std::fs;
let dir = tempdir().unwrap();
let make = |path: &std::path::Path, cfg: MnemoConfig| -> (u64, u64) {
let mut db = Mnemo::create(path, "pw", cfg).unwrap();
db.remember(Memory::new(
"seed",
MemoryType::Working,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
db.close().unwrap();
drop(db);
let bytes = fs::metadata(path).unwrap().len();
let raw = read_bytes(path);
let wal_pages = u64::from_le_bytes(raw[194..202].try_into().unwrap());
(bytes, wal_pages)
};
let (small_bytes, small_wal) = make(&dir.path().join("small.mnemo"), fast_cfg(4));
assert_eq!(small_wal, 8, "default wal_pages_initial should be 8");
assert!(
small_bytes < 200_000,
"fresh-default file ballooned: {small_bytes} bytes (expected < 200 KB)"
);
let big_cfg = MnemoConfig {
dimensions: 4,
kdf: KdfParams::fast(),
wal_pages_initial: 64,
..Default::default()
};
let (big_bytes, big_wal) = make(&dir.path().join("big.mnemo"), big_cfg);
assert_eq!(big_wal, 64, "explicit wal_pages_initial=64 should stick");
let delta = big_bytes.saturating_sub(small_bytes);
assert!(
delta >= 56 * 8192 - 8192, "explicit large WAL should add ~56 pages; default={small_bytes} big={big_bytes} (delta={delta})"
);
let tiny_cfg = MnemoConfig {
dimensions: 4,
kdf: KdfParams::fast(),
wal_pages_initial: 0,
..Default::default()
};
let (_tiny_bytes, tiny_wal) = make(&dir.path().join("tiny.mnemo"), tiny_cfg);
assert!(
tiny_wal >= 2,
"wal_pages_initial=0 should be clamped to the MIN_WAL_PAGES floor (got {tiny_wal})"
);
}
#[test]
fn nonce_unique_after_crashed_data_flush() {
use std::collections::HashMap;
const PAGE_SIZE: usize = 8192;
let dir = tempdir().unwrap();
let path = dir.path().join("crash.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"ORPHAN_TRANSACTION_AAA",
MemoryType::Working,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.__crash_partial_flush_for_testing().unwrap();
}
let snap1 = read_bytes(&path);
assert!(snap1.len() % PAGE_SIZE == 0, "file must be page-aligned");
let page_count = snap1.len() / PAGE_SIZE;
let mut phase1: HashMap<u64, [u8; 12]> = HashMap::new();
for page_no in 1..page_count as u64 {
let off = page_no as usize * PAGE_SIZE;
let nonce: [u8; 12] = snap1[off..off + 12].try_into().unwrap();
if nonce != [0u8; 12] {
phase1.insert(page_no, nonce);
}
}
assert!(
!phase1.is_empty(),
"Phase 1 should have written at least one encrypted data page; \
did __crash_partial_flush_for_testing actually flush?"
);
{
let mut db = Mnemo::open(&path, "pw").unwrap();
db.remember(Memory::new(
"REUSE_ATTEMPT_DIFFERENT_BBB",
MemoryType::Working,
vec4(0.0, 1.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
}
let snap2 = read_bytes(&path);
for (&page_no, &nonce_phase1) in &phase1 {
let off = page_no as usize * PAGE_SIZE;
if off + PAGE_SIZE > snap2.len() {
continue;
}
let nonce_phase2: [u8; 12] = snap2[off..off + 12].try_into().unwrap();
if nonce_phase2 == nonce_phase1 {
let ct_phase1 = &snap1[off + 12..off + PAGE_SIZE];
let ct_phase2 = &snap2[off + 12..off + PAGE_SIZE];
if ct_phase1 != ct_phase2 {
panic!(
"NONCE REUSE confirmed at page {page_no}: \
nonce {:02x?} was used to encrypt two different \
ciphertexts under the same DEK.\n\
This leaks keystream — anyone with both ciphertexts \
can compute plaintext_phase1 XOR plaintext_phase2 \
and breaks AES-GCM's authentication guarantees.\n\
(Phase 1.1 leasing fix not applied yet.)",
nonce_phase1
);
}
}
}
}
#[test]
fn bounded_cache_survives_eviction() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let mut ids = Vec::new();
for i in 0..300 {
let id = db
.remember(Memory::new(
format!("content-{i}"),
MemoryType::Semantic,
vec4(i as f32, 1.0, 2.0, 3.0),
))
.unwrap();
ids.push((id, i));
}
db.flush().unwrap();
db.set_cache_capacity(8);
for (id, i) in &ids {
let m = db.get(id).unwrap();
assert_eq!(m.content, format!("content-{i}"));
assert_eq!(m.vector[0], *i as f32);
}
let (used, cap) = db.page_cache_stats();
assert_eq!(cap, 8);
assert!(used <= 8, "cache held {used} clean pages, cap {cap}");
db.close().unwrap();
drop(db);
let mut db = Mnemo::open(&path, "pw").unwrap();
db.set_cache_capacity(4);
for (id, i) in &ids {
assert_eq!(db.get(id).unwrap().content, format!("content-{i}"));
}
assert!(db.page_cache_stats().0 <= 4);
}
#[test]
fn snapshots_record_every_flush() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
for i in 0..3 {
db.remember(Memory::new(
format!("m{i}"),
MemoryType::Semantic,
vec4((i as f32) + 1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
}
db.close().unwrap();
drop(db);
let db = Mnemo::open(&path, "pw").unwrap();
let snaps = db.snapshots();
assert_eq!(snaps.len(), 3);
for (i, s) in snaps.iter().enumerate() {
assert_eq!(s.txn_id, (i + 1) as u64);
assert_eq!(s.memory_count, (i + 1) as u64);
}
}
#[test]
fn max_snapshots_prunes_oldest_entries() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("cap.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.set_max_snapshots(10);
for i in 0..15 {
db.remember(Memory::new(
format!("m{i}"),
MemoryType::Semantic,
vec4((i as f32) + 1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
}
let snaps = db.snapshots();
assert_eq!(snaps.len(), 10, "cap=10 should keep exactly 10 snapshots");
let kept_ids: Vec<u64> = snaps.iter().map(|s| s.txn_id).collect();
assert_eq!(kept_ids, (6..=15).collect::<Vec<_>>());
let err = db.restore_to(3).expect_err("pruned txn must not resolve");
assert!(
matches!(err, MnemoError::NotFound(_)),
"expected NotFound for pruned txn_id 3; got {err:?}"
);
let restored = db.restore_to(10).expect("retained txn 10 must restore");
assert_eq!(restored.txn_id, 10);
assert_eq!(db.snapshots().len(), 10);
db.set_max_snapshots(0);
for i in 0..5 {
db.remember(Memory::new(
format!("more-{i}"),
MemoryType::Working,
vec4(0.0, (i as f32) + 1.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
}
assert!(
db.snapshots().len() > 10,
"cap=0 should let the manifest grow past the old cap"
);
}
#[test]
fn restore_rewinds_and_rolls_forward() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let mut ids = Vec::new();
for i in 0..3 {
let id = db
.remember(Memory::new(
format!("content-{i}"),
MemoryType::Semantic,
vec4((i as f32) + 1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap(); ids.push(id);
}
assert_eq!(db.len(), 3);
let info = db.restore_to(1).unwrap();
assert_eq!(info.memory_count, 1);
assert_eq!(db.len(), 1);
assert_eq!(db.get(&ids[0]).unwrap().content, "content-0");
assert!(db.get(&ids[2]).is_err(), "later memories must be gone");
db.close().unwrap();
drop(db);
let mut db = Mnemo::open(&path, "pw").unwrap();
assert_eq!(db.len(), 1);
db.restore_to(3).unwrap();
assert_eq!(db.len(), 3);
for (i, id) in ids.iter().enumerate() {
assert_eq!(db.get(id).unwrap().content, format!("content-{i}"));
}
assert_eq!(db.snapshots().len(), 5);
}
#[test]
fn restore_to_a_past_instant() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"first",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
std::thread::sleep(std::time::Duration::from_millis(1100));
db.remember(Memory::new(
"second",
MemoryType::Semantic,
vec4(0.0, 1.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
let snaps = db.snapshots();
let (t1, t2) = (snaps[0].created_at, snaps[1].created_at);
assert!(t2 > t1, "snapshots should differ in time");
let info = db.restore_to_time(t2).unwrap();
assert_eq!(info.txn_id, 2);
assert_eq!(db.len(), 2);
let info = db.restore_to_time(t1).unwrap();
assert_eq!(info.txn_id, 1);
assert_eq!(db.len(), 1);
assert!(db.restore_to_time(t1 - 3600).is_err());
}
#[test]
fn compaction_collapses_history() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let path_str = path.to_str().unwrap();
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
for i in 0..4 {
db.remember(Memory::new(
format!("m{i}"),
MemoryType::Episodic,
vec4((i as f32) + 1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
}
assert_eq!(db.snapshots().len(), 4);
db.close().unwrap();
drop(db);
Mnemo::compact_file(path_str, "pw").unwrap();
let db = Mnemo::open(&path, "pw").unwrap();
assert_eq!(db.len(), 4, "compaction keeps the live data");
assert_eq!(db.snapshots().len(), 1, "but resets the history");
}
#[test]
fn session_turns_written_as_working() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(3)).unwrap();
let (ids, sid) = {
let mut s = db.session("assistant-7");
let sid = s.id().to_string();
let a = s
.add_turn(Turn::user("hello", vec![1.0, 0.0, 0.0]))
.unwrap();
let b = s
.add_turn(Turn::assistant("hi there", vec![0.0, 1.0, 0.0]))
.unwrap();
assert_eq!(s.turn_count(), 2);
assert_eq!(s.agent(), "assistant-7");
(vec![a, b], sid)
};
db.flush().unwrap();
for id in &ids {
let m = db.get(id).unwrap();
assert_eq!(m.memory_type, MemoryType::Working);
assert_eq!(m.agent_id, "assistant-7");
assert_eq!(m.session_id.as_deref(), Some(sid.as_str()));
}
let first = db.get(&ids[0]).unwrap();
assert_eq!(
first.metadata.get("role").and_then(|v| v.as_str()),
Some("user")
);
}
#[test]
fn session_close_consolidates_to_episodic() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(3)).unwrap();
let ids = {
let mut s = db.session("agent-x");
let mut v = Vec::new();
for i in 0..3 {
v.push(
s.add_turn(Turn::user(
format!("turn-{i}"),
vec![(i as f32) + 1.0, 0.0, 0.0],
))
.unwrap(),
);
}
let promoted = s.close().unwrap();
assert_eq!(promoted, 3, "every working turn should be promoted");
v
};
for id in &ids {
assert_eq!(db.get(id).unwrap().memory_type, MemoryType::Episodic);
}
db.close().unwrap();
drop(db);
let mut db = Mnemo::open(&path, "pw").unwrap();
for id in &ids {
assert_eq!(db.get(id).unwrap().memory_type, MemoryType::Episodic);
}
}
#[test]
fn session_discard_deletes_turns() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(3)).unwrap();
let ids = {
let mut s = db.session("agent-x");
let a = s
.add_turn(Turn::user("scratch", vec![1.0, 0.0, 0.0]))
.unwrap();
let b = s
.add_turn(Turn::assistant("noise", vec![0.0, 1.0, 0.0]))
.unwrap();
let removed = s.discard().unwrap();
assert_eq!(removed, 2);
vec![a, b]
};
for id in &ids {
assert!(db.get(id).is_err(), "discarded turns must be gone");
}
assert_eq!(db.len(), 0);
}
#[test]
fn session_recall_is_agent_scoped() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(3)).unwrap();
db.remember(
Memory::new(
"bob-private-secret",
MemoryType::Semantic,
vec![1.0, 0.0, 0.0],
)
.with_agent("bob"),
)
.unwrap();
db.remember(
Memory::new("alice-fact", MemoryType::Semantic, vec![1.0, 0.0, 0.0]).with_agent("alice"),
)
.unwrap();
db.flush().unwrap();
let contents: Vec<String> = {
let mut s = db.session("alice");
let hits = s
.recall(RecallRequest::new(vec![1.0, 0.0, 0.0]).top_k(10))
.unwrap();
hits.into_iter().map(|h| h.memory.content).collect()
};
assert!(contents.contains(&"alice-fact".to_string()));
assert!(
!contents.contains(&"bob-private-secret".to_string()),
"another agent's private memory must not leak into a scoped recall"
);
}
#[test]
fn two_writers_on_same_file_collide_with_locked() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let _first = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
match Mnemo::open(&path, "pw") {
Err(MnemoError::Locked { path: p }) => {
assert_eq!(p, path, "Locked error should carry the requested path");
}
Err(other) => panic!("expected MnemoError::Locked, got {other:?}"),
Ok(_) => panic!("second writer must not succeed while the first holds the lock"),
}
}
#[test]
fn writer_blocks_read_only_open_on_all_platforms() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut w = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
w.remember(Memory::new(
"hello",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
w.flush().unwrap();
match Mnemo::open_read_only(&path, "pw") {
Err(MnemoError::Locked { path: p }) => assert_eq!(p, path),
Err(other) => panic!("expected Locked while writer is alive, got {other:?}"),
Ok(_) => panic!("read-only open must not succeed while writer holds exclusive"),
}
drop(w);
let mut r = Mnemo::open_read_only(&path, "pw").unwrap();
let hits = r
.recall(
&RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0))
.top_k(1)
.track_access(false),
)
.unwrap();
assert_eq!(hits.len(), 1);
}
#[test]
fn multiple_read_only_handles_coexist() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
{
let mut w = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
w.remember(Memory::new(
"seed",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
w.flush().unwrap();
}
let mut r1 = Mnemo::open_read_only(&path, "pw").unwrap();
let mut r2 = Mnemo::open_read_only(&path, "pw").unwrap();
let r3 = Mnemo::open_read_only(&path, "pw").unwrap();
let req = RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0))
.top_k(1)
.track_access(false);
assert_eq!(r1.recall(&req).unwrap().len(), 1);
assert_eq!(r2.recall(&req).unwrap().len(), 1);
drop(r3);
}
#[test]
fn read_only_handle_refuses_mutations() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
{
let mut w = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
w.remember(Memory::new(
"seed",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
w.flush().unwrap();
}
let mut r = Mnemo::open_read_only(&path, "pw").unwrap();
match r.remember(Memory::new(
"nope",
MemoryType::Semantic,
vec4(0.0, 1.0, 0.0, 0.0),
)) {
Err(MnemoError::ReadOnly) => {}
other => panic!("expected ReadOnly on remember, got {other:?}"),
}
match r.flush() {
Err(MnemoError::ReadOnly) => {}
other => panic!("expected ReadOnly on flush, got {other:?}"),
}
match r.rekey("new", KdfParams::fast()) {
Err(MnemoError::ReadOnly) => {}
other => panic!("expected ReadOnly on rekey, got {other:?}"),
}
match r.recall(&RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0)).top_k(1)) {
Err(MnemoError::ReadOnly) => {}
other => panic!("expected ReadOnly on tracked recall, got {other:?}"),
}
let hits = r
.recall(
&RecallRequest::new(vec4(1.0, 0.0, 0.0, 0.0))
.top_k(1)
.track_access(false),
)
.unwrap();
assert_eq!(hits.len(), 1);
r.close().unwrap();
}
#[test]
fn read_only_open_refused_when_wal_needs_recovery() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
{
let mut w = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
w.remember(Memory::new(
"crash-victim",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
w.flush().unwrap();
w.remember(Memory::new(
"crash-second",
MemoryType::Semantic,
vec4(0.0, 1.0, 0.0, 0.0),
))
.unwrap();
w.__crash_partial_flush_for_testing().unwrap();
}
match Mnemo::open_read_only(&path, "pw") {
Err(MnemoError::NeedsWriteOpen { reason }) => {
assert!(!reason.is_empty(), "reason should be non-empty");
}
Ok(_) => {
}
Err(other) => panic!("unexpected error from read_only open: {other:?}"),
}
}
fn cache_opts(ct: &str, ttl_secs: Option<u64>) -> CachePutOpts {
CachePutOpts {
content_type: ct.to_string(),
ttl_secs,
cost_hint_ms: None,
}
}
#[test]
fn cache_put_get_roundtrip_json_text_bytes() {
let dir = tempdir().unwrap();
let path = dir.path().join("cache.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let json_val = br#"{"model":"gpt-4","cost":0.03}"#;
let text_val = b"the user prefers dark mode";
let bin_val = &[0x00u8, 0x01, 0x02, 0xff, 0xfe][..];
db.cache_put("llm", "prompt-42", json_val, cache_opts("json", None))
.unwrap();
db.cache_put("prefs", "user-1", text_val, cache_opts("text", None))
.unwrap();
db.cache_put(
"http",
"GET /avatar.png",
bin_val,
cache_opts("bytes", None),
)
.unwrap();
db.flush().unwrap();
let got_json = db.cache_get("llm", "prompt-42").unwrap().expect("hit");
assert_eq!(got_json.value, json_val);
assert_eq!(got_json.content_type, "json");
let got_text = db.cache_get("prefs", "user-1").unwrap().expect("hit");
assert_eq!(got_text.value, text_val);
let got_bin = db
.cache_get("http", "GET /avatar.png")
.unwrap()
.expect("hit");
assert_eq!(got_bin.value, bin_val);
assert_eq!(got_bin.content_type, "bytes");
}
#[test]
fn cache_ttl_expiry_hides_entries_as_miss() {
let dir = tempdir().unwrap();
let path = dir.path().join("ttl.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.cache_put(
"llm",
"ephemeral",
b"gone by now",
cache_opts("text", Some(0)),
)
.unwrap();
db.flush().unwrap();
let hit = db.cache_get("llm", "ephemeral").unwrap();
assert!(hit.is_none(), "expired entry should read as miss");
let stats = db.cache_stats(Some("llm"));
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 1);
assert_eq!(stats.entries, 1);
}
#[test]
fn cache_stats_counters_track_hits_and_misses() {
let dir = tempdir().unwrap();
let path = dir.path().join("stats.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.cache_put("llm", "k1", b"v1", cache_opts("text", None))
.unwrap();
db.cache_put("llm", "k2", b"v2", cache_opts("text", None))
.unwrap();
db.flush().unwrap();
assert!(db.cache_get("llm", "k1").unwrap().is_some());
assert!(db.cache_get("llm", "k1").unwrap().is_some());
assert!(db.cache_get("llm", "k2").unwrap().is_some());
assert!(db.cache_get("llm", "missing-a").unwrap().is_none());
assert!(db.cache_get("llm", "missing-b").unwrap().is_none());
let s = db.cache_stats(Some("llm"));
assert_eq!(s.hits, 3);
assert_eq!(s.misses, 2);
assert!((s.hit_rate - 0.6).abs() < 1e-9, "hit_rate = {}", s.hit_rate);
assert_eq!(s.entries, 2);
assert!(s.bytes >= 4, "bytes should include both values");
let sw = db.cache_stats(None);
assert_eq!(sw.hits, 3);
assert_eq!(sw.misses, 2);
}
#[test]
fn cache_budget_evicts_lru_on_overflow() {
let dir = tempdir().unwrap();
let path = dir.path().join("budget.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.set_cache_budget(
"llm",
CacheBudget {
max_entries: 3,
max_bytes: 1 << 30,
},
);
db.cache_put("llm", "a", b"AAA", cache_opts("text", None))
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(1050));
db.cache_put("llm", "b", b"BBB", cache_opts("text", None))
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(1050));
db.cache_put("llm", "c", b"CCC", cache_opts("text", None))
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(1050));
db.cache_put("llm", "d", b"DDD", cache_opts("text", None))
.unwrap();
db.flush().unwrap();
let s = db.cache_stats(Some("llm"));
assert_eq!(s.entries, 3, "budget cap enforced after insert");
assert!(s.evictions >= 1, "at least one eviction recorded");
assert!(
db.cache_get("llm", "a").unwrap().is_none(),
"'a' should be evicted"
);
assert!(db.cache_get("llm", "b").unwrap().is_some());
assert!(db.cache_get("llm", "c").unwrap().is_some());
assert!(db.cache_get("llm", "d").unwrap().is_some());
}
#[test]
fn cache_persists_across_reopen() {
let dir = tempdir().unwrap();
let path = dir.path().join("persist.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.cache_put(
"llm",
"durable-key",
b"durable-value",
cache_opts("text", None),
)
.unwrap();
db.flush().unwrap();
}
let mut db = Mnemo::open(&path, "pw").unwrap();
let got = db.cache_get("llm", "durable-key").unwrap().expect("hit");
assert_eq!(got.value, b"durable-value");
}
#[test]
fn cache_delete_removes_entry() {
let dir = tempdir().unwrap();
let path = dir.path().join("del.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.cache_put("llm", "k", b"v", cache_opts("text", None))
.unwrap();
db.flush().unwrap();
assert!(db.cache_get("llm", "k").unwrap().is_some());
db.cache_delete("llm", "k").unwrap();
db.flush().unwrap();
assert!(db.cache_get("llm", "k").unwrap().is_none());
assert_eq!(db.cache_stats(Some("llm")).entries, 0);
}
#[test]
fn cache_purge_expired_only_leaves_fresh_entries() {
let dir = tempdir().unwrap();
let path = dir.path().join("purge.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.cache_put("llm", "fresh", b"stay", cache_opts("text", None))
.unwrap();
db.cache_put("llm", "expired", b"gone", cache_opts("text", Some(0)))
.unwrap();
db.flush().unwrap();
let n = db.cache_purge(Some("llm"), true).unwrap();
assert_eq!(n, 1, "only the expired entry should be purged");
db.flush().unwrap();
assert!(db.cache_get("llm", "fresh").unwrap().is_some());
assert!(db.cache_get("llm", "expired").unwrap().is_none());
}
#[test]
fn cache_batched_policy_auto_flushes_on_count_threshold() {
let dir = tempdir().unwrap();
let path = dir.path().join("batch.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.set_cache_flush_policy(CacheFlushPolicy::Batched {
max_dirty: 2,
max_age: std::time::Duration::from_secs(3600),
});
db.cache_put("llm", "b1", b"v1", cache_opts("text", None))
.unwrap();
db.cache_put("llm", "b2", b"v2", cache_opts("text", None))
.unwrap();
}
let mut db = Mnemo::open(&path, "pw").unwrap();
assert!(
db.cache_get("llm", "b1").unwrap().is_some(),
"b1 should have auto-committed"
);
assert!(
db.cache_get("llm", "b2").unwrap().is_some(),
"b2 should have auto-committed"
);
}
#[test]
fn cache_batched_policy_uncommitted_entries_are_misses_after_crash() {
let dir = tempdir().unwrap();
let path = dir.path().join("crash.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.set_cache_flush_policy(CacheFlushPolicy::Batched {
max_dirty: 99,
max_age: std::time::Duration::from_secs(3600),
});
db.cache_put("llm", "will-be-lost", b"gone", cache_opts("text", None))
.unwrap();
}
let mut db = Mnemo::open(&path, "pw").unwrap();
assert!(
db.cache_get("llm", "will-be-lost").unwrap().is_none(),
"uncommitted batched entry must be a miss on reopen (never corruption)"
);
assert_eq!(db.cache_stats(None).entries, 0);
}
#[test]
fn cache_read_only_handle_can_read_but_not_write() {
let dir = tempdir().unwrap();
let path = dir.path().join("ro.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.cache_put("llm", "k", b"v", cache_opts("text", None))
.unwrap();
db.flush().unwrap();
}
let mut r = Mnemo::open_read_only(&path, "pw").unwrap();
let hit = r.cache_get("llm", "k").unwrap().expect("read still works");
assert_eq!(hit.value, b"v");
use mnemo_engine::MnemoError;
match r.cache_put("llm", "new", b"x", cache_opts("text", None)) {
Err(MnemoError::ReadOnly) => {}
other => panic!("expected ReadOnly on cache_put, got {other:?}"),
}
match r.cache_delete("llm", "k") {
Err(MnemoError::ReadOnly) => {}
other => panic!("expected ReadOnly on cache_delete, got {other:?}"),
}
match r.cache_purge(None, false) {
Err(MnemoError::ReadOnly) => {}
other => panic!("expected ReadOnly on cache_purge, got {other:?}"),
}
}
#[test]
fn semantic_hit_above_threshold_returns_top_1() {
let dir = tempdir().unwrap();
let path = dir.path().join("sem.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let v_a = vec4(1.0, 0.0, 0.0, 0.0);
let v_b = vec4(0.0, 1.0, 0.0, 0.0);
db.cache_put_semantic(
"llm",
"prompt-a",
v_a.clone(),
b"answer-a",
SemanticCachePutOpts::new("bge-large-en-v1.5"),
)
.unwrap();
db.cache_put_semantic(
"llm",
"prompt-b",
v_b.clone(),
b"answer-b",
SemanticCachePutOpts::new("bge-large-en-v1.5"),
)
.unwrap();
db.cache_put("llm", "exact-only", b"no-vec", cache_opts("text", None))
.unwrap();
db.flush().unwrap();
let query = vec4(0.98, 0.20, 0.0, 0.0);
let hit = db
.cache_get_semantic(
"llm",
&query,
DEFAULT_SEMANTIC_THRESHOLD,
"bge-large-en-v1.5",
)
.unwrap()
.expect("expected a hit for the near-a query");
assert_eq!(hit.0.value, b"answer-a", "top-1 should be prompt-a");
assert!(hit.1 >= DEFAULT_SEMANTIC_THRESHOLD, "sim = {}", hit.1);
}
#[test]
fn semantic_miss_below_threshold_returns_none() {
let dir = tempdir().unwrap();
let path = dir.path().join("sem.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.cache_put_semantic(
"llm",
"prompt-a",
vec4(1.0, 0.0, 0.0, 0.0),
b"answer-a",
SemanticCachePutOpts::new("bge-large-en-v1.5"),
)
.unwrap();
db.flush().unwrap();
let query = vec4(0.0, 1.0, 0.0, 0.0);
let miss = db
.cache_get_semantic(
"llm",
&query,
DEFAULT_SEMANTIC_THRESHOLD,
"bge-large-en-v1.5",
)
.unwrap();
assert!(miss.is_none(), "orthogonal query must not hit");
let hit = db
.cache_get_semantic("llm", &query, -1.0, "bge-large-en-v1.5")
.unwrap();
assert!(
hit.is_some(),
"with threshold -1.0 the top-1 should always surface"
);
}
#[test]
fn semantic_model_mismatch_is_a_miss() {
let dir = tempdir().unwrap();
let path = dir.path().join("sem.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let v = vec4(1.0, 0.0, 0.0, 0.0);
db.cache_put_semantic(
"llm",
"prompt-a",
v.clone(),
b"answer-a",
SemanticCachePutOpts::new("bge-large-en-v1.5"),
)
.unwrap();
db.flush().unwrap();
let miss = db
.cache_get_semantic(
"llm",
&v,
DEFAULT_SEMANTIC_THRESHOLD,
"text-embedding-3-small",
)
.unwrap();
assert!(
miss.is_none(),
"different-model hit is a semantics bug; must miss"
);
let hit = db
.cache_get_semantic("llm", &v, DEFAULT_SEMANTIC_THRESHOLD, "bge-large-en-v1.5")
.unwrap()
.expect("same-model exact-vector query must hit");
assert_eq!(hit.0.value, b"answer-a");
}
#[test]
fn semantic_dimension_mismatch_is_rejected() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("dim.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let three_dims = vec![1.0, 0.0, 0.0];
let err = db
.cache_put_semantic("llm", "k", three_dims, b"v", SemanticCachePutOpts::new("m"))
.unwrap_err();
match err {
MnemoError::DimensionMismatch {
expected: 4,
got: 3,
} => {}
other => panic!("expected DimensionMismatch, got {other:?}"),
}
}
#[test]
fn exact_key_and_semantic_entries_coexist_in_same_namespace() {
let dir = tempdir().unwrap();
let path = dir.path().join("coexist.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.cache_put("llm", "exact", b"exact-value", cache_opts("text", None))
.unwrap();
db.cache_put_semantic(
"llm",
"sem",
vec4(1.0, 0.0, 0.0, 0.0),
b"sem-value",
SemanticCachePutOpts::new("bge"),
)
.unwrap();
db.flush().unwrap();
}
let mut db = Mnemo::open(&path, "pw").unwrap();
let e = db
.cache_get("llm", "exact")
.unwrap()
.expect("exact-key still hits");
assert_eq!(e.value, b"exact-value");
let s = db
.cache_get_semantic(
"llm",
&vec4(1.0, 0.0, 0.0, 0.0),
DEFAULT_SEMANTIC_THRESHOLD,
"bge",
)
.unwrap()
.expect("semantic entry still hits");
assert_eq!(s.0.value, b"sem-value");
let orth = db
.cache_get_semantic("llm", &vec4(0.0, 0.0, 0.0, 1.0), -1.0, "bge")
.unwrap()
.expect("some entry must surface with threshold=-1");
assert_eq!(s.0.value, b"sem-value");
assert_eq!(orth.0.value, b"sem-value");
}
#[test]
fn v7_file_migrates_to_v8_with_empty_cache() {
let dir = tempdir().unwrap();
let path = dir.path().join("migrated.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::new(
"sanity",
MemoryType::Semantic,
vec4(1.0, 0.0, 0.0, 0.0),
))
.unwrap();
db.flush().unwrap();
}
let db = Mnemo::open_read_only(&path, "pw").unwrap();
let s = db.cache_stats(None);
assert_eq!(s.entries, 0, "empty cache directory loads clean");
assert_eq!(s.bytes, 0);
assert_eq!(db.len(), 1);
}
use mnemo_engine::{Embedder, EmbedderIdentity};
struct FakeEmbedder {
identity: EmbedderIdentity,
}
impl FakeEmbedder {
fn new(name: &str, dims: usize, normalize: bool) -> Self {
Self {
identity: EmbedderIdentity {
name: name.into(),
dimensions: dims,
normalize,
},
}
}
}
impl Embedder for FakeEmbedder {
fn identity(&self) -> EmbedderIdentity {
self.identity.clone()
}
fn embed(&self, texts: &[&str]) -> mnemo_engine::Result<Vec<Vec<f32>>> {
let dims = self.identity.dimensions;
let mut out = Vec::with_capacity(texts.len());
for t in texts {
let hash: usize = t.bytes().map(|b| b as usize).sum();
let axis = hash % dims;
let mut v = vec![0.0f32; dims];
v[axis] = 1.0;
out.push(v);
}
Ok(out)
}
}
struct ZeroEmbedder {
identity: EmbedderIdentity,
}
impl Embedder for ZeroEmbedder {
fn identity(&self) -> EmbedderIdentity {
self.identity.clone()
}
fn embed(&self, texts: &[&str]) -> mnemo_engine::Result<Vec<Vec<f32>>> {
Ok(texts
.iter()
.map(|_| vec![0.0f32; self.identity.dimensions])
.collect())
}
}
#[test]
fn set_embedder_dim_mismatch_is_rejected() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("em.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let wrong = FakeEmbedder::new("wrong-dim", 8, true);
match db.set_embedder(wrong) {
Err(MnemoError::DimensionMismatch { expected, got }) => {
assert_eq!(expected, 4);
assert_eq!(got, 8);
}
other => panic!("expected DimensionMismatch, got {other:?}"),
}
}
#[test]
fn set_embedder_identity_mismatch_rejected_after_persist() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("em.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::scaffold_manifest(4)).unwrap();
db.flush().unwrap();
db.set_embedder(FakeEmbedder::new("MiniLM", 4, true))
.unwrap();
}
let mut db = Mnemo::open(&path, "pw").unwrap();
match db.set_embedder(FakeEmbedder::new("bge", 4, true)) {
Err(MnemoError::Invalid(msg)) => {
assert!(msg.contains(EmbedderIdentity::MISMATCH_MSG), "msg = {msg}");
assert!(msg.contains("MiniLM"));
assert!(msg.contains("bge"));
}
other => panic!("expected Invalid identity mismatch, got {other:?}"),
}
db.set_embedder(FakeEmbedder::new("MiniLM", 4, true))
.unwrap();
}
#[test]
fn zero_vector_remember_rejected() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("z.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let m = Memory::new("empty", MemoryType::Semantic, vec![0.0, 0.0, 0.0, 0.0]);
match db.remember(m) {
Err(MnemoError::Invalid(msg)) => {
assert!(msg.contains("all-zero vector"), "msg = {msg}");
}
other => panic!("expected Invalid for zero vector, got {other:?}"),
}
let m = Memory::new("ok", MemoryType::Semantic, vec![1.0, 0.0, 0.0, 0.0]);
db.remember(m).unwrap();
}
#[test]
fn remember_text_from_text_round_trip() {
let dir = tempdir().unwrap();
let path = dir.path().join("t.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.set_embedder(FakeEmbedder::new("fake-4d", 4, true))
.unwrap();
db.remember_text("alpha", MemoryType::Semantic).unwrap();
db.remember_text("beta", MemoryType::Episodic).unwrap();
db.flush().unwrap();
let fake = FakeEmbedder::new("fake-4d", 4, true);
let req = mnemo_engine::RecallRequest::from_text(&fake, "alpha")
.unwrap()
.top_k(2);
let hits = db.recall(&req).unwrap();
assert!(!hits.is_empty(), "recall returned nothing");
assert_eq!(hits[0].memory.content, "alpha");
}
#[test]
fn remember_text_without_embedder_fails_clearly() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("ne.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
match db.remember_text("hello", MemoryType::Semantic) {
Err(MnemoError::Invalid(msg)) => assert!(msg.contains("no embedder")),
other => panic!("expected Invalid 'no embedder', got {other:?}"),
}
}
#[test]
fn zero_vector_embedder_remember_text_rejected() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("z2.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.set_embedder(ZeroEmbedder {
identity: EmbedderIdentity {
name: "zero".into(),
dimensions: 4,
normalize: false,
},
})
.unwrap();
match db.remember_text("anything", MemoryType::Semantic) {
Err(MnemoError::Invalid(msg)) => {
assert!(msg.contains("all-zero vector"), "msg = {msg}");
}
other => panic!("expected Invalid for zero-vec embedder, got {other:?}"),
}
}
fn remember_nested_manifest(db: &mut Mnemo, dims: usize, name: &str, normalize: bool) {
let mut m = Memory::scaffold_manifest(dims);
let obj = serde_json::json!({
"name": name,
"dimensions": dims,
"normalize": normalize,
});
m.metadata.insert("embedder".into(), obj);
db.remember(m).unwrap();
db.flush().unwrap();
}
#[test]
fn set_embedder_reads_nested_manifest_canonically() {
let dir = tempdir().unwrap();
let path = dir.path().join("nested.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
remember_nested_manifest(&mut db, 4, "sentence-transformers/all-MiniLM-L6-v2", true);
db.set_embedder(FakeEmbedder::new(
"sentence-transformers/all-MiniLM-L6-v2",
4,
true,
))
.unwrap();
}
let mut db = Mnemo::open(&path, "pw").unwrap();
let onboarding = db.about().unwrap();
let manifest = &onboarding[0];
let nested = manifest
.metadata
.get("embedder")
.and_then(|v| v.as_object());
assert!(nested.is_some(), "nested embedder object must be present");
assert_eq!(
nested.unwrap().get("name").and_then(|v| v.as_str()),
Some("sentence-transformers/all-MiniLM-L6-v2")
);
assert!(
manifest.metadata.get("embedder_name").is_none(),
"flat embedder_name must NOT have been upserted alongside the nested form"
);
assert!(manifest.metadata.get("embedder_dimensions").is_none());
assert!(manifest.metadata.get("embedder_normalize").is_none());
}
#[test]
fn set_embedder_nested_manifest_mismatch_rejected() {
use mnemo_engine::MnemoError;
let dir = tempdir().unwrap();
let path = dir.path().join("nested-mismatch.mnemo");
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
remember_nested_manifest(&mut db, 4, "sentence-transformers/all-MiniLM-L6-v2", true);
match db.set_embedder(FakeEmbedder::new("bge-large-en-v1.5", 4, true)) {
Err(MnemoError::Invalid(msg)) => {
assert!(msg.contains(EmbedderIdentity::MISMATCH_MSG), "msg = {msg}");
assert!(msg.contains("sentence-transformers/all-MiniLM-L6-v2"));
assert!(msg.contains("bge-large-en-v1.5"));
}
other => panic!("expected Invalid identity mismatch, got {other:?}"),
}
}
#[test]
fn set_embedder_accepts_legacy_flat_keys() {
let dir = tempdir().unwrap();
let path = dir.path().join("legacy.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
let mut m = Memory::scaffold_manifest(4);
m.metadata
.insert("embedder_name".into(), serde_json::json!("MiniLM"));
m.metadata
.insert("embedder_dimensions".into(), serde_json::json!(4));
m.metadata
.insert("embedder_normalize".into(), serde_json::json!(true));
db.remember(m).unwrap();
db.flush().unwrap();
db.set_embedder(FakeEmbedder::new("MiniLM", 4, true))
.unwrap();
}
}
#[test]
fn set_embedder_on_read_only_handle_adopts_without_write() {
let dir = tempdir().unwrap();
let path = dir.path().join("ro-em.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
remember_nested_manifest(&mut db, 4, "fake-4d", true);
db.set_embedder(FakeEmbedder::new("fake-4d", 4, true))
.unwrap();
db.remember_text("alpha", MemoryType::Semantic).unwrap();
db.flush().unwrap();
}
let mut r = Mnemo::open_read_only(&path, "pw").unwrap();
r.set_embedder(FakeEmbedder::new("fake-4d", 4, true))
.unwrap();
let fake = FakeEmbedder::new("fake-4d", 4, true);
let req = mnemo_engine::RecallRequest::from_text(&fake, "alpha")
.unwrap()
.track_access(false);
let hits = r.recall(&req).unwrap();
assert!(
!hits.is_empty(),
"recall_text on read-only handle returned nothing"
);
assert_eq!(hits[0].memory.content, "alpha");
}
#[test]
fn set_embedder_on_read_only_handle_with_no_manifest_identity_still_works() {
let dir = tempdir().unwrap();
let path = dir.path().join("ro-fresh.mnemo");
{
let mut db = Mnemo::create(&path, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::scaffold_manifest(4)).unwrap();
db.set_embedder(FakeEmbedder::new("fake-4d", 4, true))
.unwrap();
db.remember_text("beta", MemoryType::Semantic).unwrap();
db.flush().unwrap();
}
let dir2 = tempdir().unwrap();
let path2 = dir2.path().join("ro-fresh2.mnemo");
{
let mut db = Mnemo::create(&path2, "pw", fast_cfg(4)).unwrap();
db.remember(Memory::scaffold_manifest(4)).unwrap();
db.remember(Memory::new(
"gamma",
MemoryType::Semantic,
vec![0.0, 0.0, 1.0, 0.0],
))
.unwrap();
db.flush().unwrap();
}
let mut r = Mnemo::open_read_only(&path2, "pw").unwrap();
r.set_embedder(FakeEmbedder::new("fake-4d", 4, true))
.unwrap();
assert!(r.embedder().is_some());
}