use plugmem_core::{
Config, Error, FactId, MemStorage, Memory, RecallQuery, RememberInput, Storage,
};
fn cfg(dim: usize) -> Config {
let mut c = Config::default();
c.dim = dim;
c.shards_facts = 16;
c.shards_entities = 8;
c.shards_edges = 8;
c.shards_temporal = 8;
c.shards_postings = 32;
c
}
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> f32 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((self.0 >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0
}
fn vector(&mut self, dim: usize) -> Vec<f32> {
(0..dim).map(|_| self.next()).collect()
}
}
fn cosine(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
dot / (na * nb)
}
fn vquery<'a>(now: u64, v: &'a [f32], k: usize) -> RecallQuery<'a> {
RecallQuery {
now,
text: None,
vector: Some(v),
tags: &[],
entities: &[],
as_of: None,
range: None,
k,
token_budget: None,
include_closed: false,
ef: None,
}
}
#[test]
fn recall_by_vector_surfaces_the_match() {
let dim = 64;
let (mut mem, mut store) = (Memory::new(cfg(dim)).unwrap(), MemStorage::new());
let mut rng = Lcg(1);
let a = rng.vector(dim);
let b = rng.vector(dim);
let c = rng.vector(dim);
for (i, v) in [&a, &b, &c].into_iter().enumerate() {
mem.remember(
&mut store,
RememberInput {
vector: Some(v),
..RememberInput::text(1000, ["alpha", "beta", "gamma"][i])
},
)
.unwrap();
}
let out = mem.recall(vquery(1000, &b, 3)).unwrap();
assert_eq!(out.facts[0].id, FactId(1));
assert!(!out.rendered.is_empty());
}
#[test]
fn flat_recall_matches_bruteforce() {
let dim = 128;
let n = 300u64;
let (mut mem, mut store) = (Memory::new(cfg(dim)).unwrap(), MemStorage::new());
let mut rng = Lcg(0xabcd);
let mut stored: Vec<Vec<f32>> = Vec::new();
for _ in 0..n {
let v = rng.vector(dim);
mem.remember(
&mut store,
RememberInput {
vector: Some(&v),
..RememberInput::text(1000, "v")
},
)
.unwrap();
stored.push(v);
}
let query = rng.vector(dim);
let k = 10;
let out = mem.recall(vquery(1000, &query, k)).unwrap();
let got: Vec<u32> = out.facts.iter().map(|f| f.id.0).collect();
let mut truth: Vec<(u32, f32)> = stored
.iter()
.enumerate()
.map(|(i, v)| (i as u32, cosine(&query, v)))
.collect();
truth.sort_by(|a, b| b.1.total_cmp(&a.1));
let want: Vec<u32> = truth.iter().take(k).map(|&(i, _)| i).collect();
let hits = got.iter().filter(|id| want.contains(id)).count();
let recall = hits as f32 / k as f32;
assert!(recall >= 0.8, "flat recall@{k} = {recall} (got {got:?})");
}
#[test]
fn vector_source_respects_liveness() {
let dim = 32;
let (mut mem, mut store) = (Memory::new(cfg(dim)).unwrap(), MemStorage::new());
let mut rng = Lcg(7);
let v = rng.vector(dim);
let out = mem
.remember(
&mut store,
RememberInput {
vector: Some(&v),
..RememberInput::text(1000, "secret")
},
)
.unwrap();
assert_eq!(mem.recall(vquery(1000, &v, 5)).unwrap().facts[0].id, out.id);
mem.forget(&mut store, 2000, out.id).unwrap();
assert!(mem.recall(vquery(2000, &v, 5)).unwrap().facts.is_empty());
let mut mem2 = Memory::new(cfg(dim)).unwrap();
let mut store2 = MemStorage::new();
mem2.remember(
&mut store2,
RememberInput {
vector: Some(&v),
..RememberInput::text(5000, "later")
},
)
.unwrap();
let mut q = vquery(3000, &v, 5);
q.as_of = Some(3000);
assert!(mem2.recall(q).unwrap().facts.is_empty());
}
#[test]
fn vector_similar_detection_fires() {
let dim = 64;
let (mut mem, mut store) = (Memory::new(cfg(dim)).unwrap(), MemStorage::new());
let mut rng = Lcg(99);
let base = rng.vector(dim);
mem.remember(
&mut store,
RememberInput {
entity: Some("user"),
vector: Some(&base),
..RememberInput::text(1000, "one two three")
},
)
.unwrap();
let mut near = base.clone();
near[0] += 0.001;
let out = mem
.remember(
&mut store,
RememberInput {
entity: Some("user"),
vector: Some(&near),
..RememberInput::text(2000, "four five six")
},
)
.unwrap();
assert_eq!(out.similar.len(), 1);
assert_eq!(out.similar[0].id, FactId(0));
assert_eq!(
out.similar[0].reason,
plugmem_core::SimilarReason::VectorCosine
);
}
#[test]
fn snapshot_roundtrip_with_vectors_is_canonical() {
let dim = 48;
let (mut mem, mut store) = (Memory::new(cfg(dim)).unwrap(), MemStorage::new());
let mut rng = Lcg(0xfeed);
let mut vecs = Vec::new();
for i in 0..40u64 {
let v = rng.vector(dim);
let has = i % 2 == 0;
mem.remember(
&mut store,
RememberInput {
vector: has.then_some(&v[..]),
..RememberInput::text(i * 1000, "fact text here")
},
)
.unwrap();
vecs.push((has, v));
}
let bytes = mem.snapshot_bytes(0);
let (loaded, report) = Memory::from_bytes(Some(&bytes), &[], cfg(dim)).unwrap();
assert_eq!(report.replayed, 0);
assert_eq!(loaded.snapshot_bytes(0), bytes);
let (has, ref v) = vecs[8];
assert!(has);
let a = mem.recall(vquery(40_000, v, 5)).unwrap();
let b = loaded.recall(vquery(40_000, v, 5)).unwrap();
assert_eq!(a.rendered, b.rendered);
assert_eq!(a.facts[0].id, FactId(8));
}
#[test]
fn truncated_vector_snapshot_errors_at_load() {
let dim = 32;
let (mut mem, mut store) = (Memory::new(cfg(dim)).unwrap(), MemStorage::new());
let mut rng = Lcg(5);
for i in 0..20u64 {
let v = rng.vector(dim);
mem.remember(
&mut store,
RememberInput {
vector: Some(&v),
..RememberInput::text(i * 1000, "text")
},
)
.unwrap();
}
let bytes = mem.snapshot_bytes(0);
for cut in (0..bytes.len()).step_by(53) {
assert!(
Memory::from_bytes(Some(&bytes[..cut]), &[], cfg(dim)).is_err(),
"truncation to {cut} accepted"
);
}
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn a_vector_open_never_panics_and_verify_catches_corruption() {
let dim = 32;
let c = cfg(dim);
let (mut mem, mut store) = (Memory::new(c.clone()).unwrap(), MemStorage::new());
let mut rng = Lcg(5);
let mut last = Vec::new();
for i in 0..20u64 {
let v = rng.vector(dim);
mem.remember(
&mut store,
RememberInput {
vector: Some(&v),
..RememberInput::text(i * 1000, "text")
},
)
.unwrap();
last = v;
}
let bytes = mem.snapshot_bytes(0);
let mut verify_caught = false;
for at in (0..bytes.len()).step_by(17) {
let mut b = bytes.clone();
b[at] ^= 0x20;
let cc = c.clone();
let q = last.clone();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let Ok((m, _)) = Memory::from_bytes(Some(&b), &[], cc) else {
return false; };
let stats = m.stats();
for i in 0..stats.next_fact {
let _ = m.get(FactId(i));
}
let _ = m.recall(vquery(1000, &q, 5)); let _ = m.snapshot_bytes(0);
m.verify().is_err()
}));
match outcome {
Ok(caught) => verify_caught |= caught,
Err(_) => panic!("a vector access panicked after a flip at {at}"),
}
}
assert!(
verify_caught,
"verify() must catch at least one deferred vector/content corruption"
);
}
#[test]
fn journal_replay_reproduces_vectors() {
let dim = 40;
let (mut mem, mut store) = (Memory::new(cfg(dim)).unwrap(), MemStorage::new());
let mut rng = Lcg(0x2468);
for i in 0..30u64 {
let v = rng.vector(dim);
mem.remember(
&mut store,
RememberInput {
entity: Some(["user", "plugmem"][(i % 2) as usize]),
vector: (i % 3 != 0).then_some(&v[..]),
..RememberInput::text(i * 1000, "some remembered text")
},
)
.unwrap();
}
let (reopened, report) = Memory::open(&mut store, cfg(dim)).unwrap();
assert!(report.replayed > 0 && report.skipped == 0);
assert_eq!(reopened.snapshot_bytes(0), mem.snapshot_bytes(0));
}
#[test]
fn vector_error_surface() {
let (mut m0, mut s0) = (Memory::new(cfg(0)).unwrap(), MemStorage::new());
let v = [1.0f32, 2.0];
assert_eq!(
m0.remember(
&mut s0,
RememberInput {
vector: Some(&v),
..RememberInput::text(1, "x")
}
)
.unwrap_err(),
Error::Invalid("vector given but dim is 0")
);
let (mut m, mut s) = (Memory::new(cfg(64)).unwrap(), MemStorage::new());
let short = [0.5f32; 10];
assert!(matches!(
m.remember(
&mut s,
RememberInput {
vector: Some(&short),
..RememberInput::text(1, "x")
}
)
.unwrap_err(),
Error::DimMismatch { got: 10, want: 64 }
));
let zero = [0.0f32; 64];
assert_eq!(
m.remember(
&mut s,
RememberInput {
vector: Some(&zero),
..RememberInput::text(1, "x")
}
)
.unwrap_err(),
Error::Invalid("vector must be nonzero")
);
}
#[test]
fn journal_vector_dim_mismatch_is_corrupt() {
let (mut mem, mut store) = (Memory::new(cfg(4)).unwrap(), MemStorage::new());
mem.remember(
&mut store,
RememberInput {
vector: Some(&[1.0, 2.0, 3.0, 4.0]),
..RememberInput::text(1, "x")
},
)
.unwrap();
let journal = store.read_journal().unwrap();
assert_eq!(
Memory::from_bytes(None, &journal, cfg(8)).unwrap_err(),
Error::Corrupt("journal vector dimension disagrees with dim")
);
}