use serde_json::Value;
use tempfile::TempDir;
use velesdb_memory::{
ExtractError, ExtractedFact, Extractor, HashEmbedder, MemoryError, MemoryService, Metadata,
DEFAULT_DIMENSION,
};
fn meta(key: &str, value: Value) -> Metadata {
let mut m = Metadata::new();
m.insert(key.to_string(), value);
m
}
struct StubExtractor;
impl Extractor for StubExtractor {
fn extract(&self, _text: &str) -> Result<Vec<ExtractedFact>, ExtractError> {
Ok(vec![
ExtractedFact {
text: "Alice ships the parser in Rust.".to_string(),
entities: vec!["rust".to_string(), "parser".to_string()],
},
ExtractedFact {
text: "Bob maintains the Rust toolchain.".to_string(),
entities: vec!["rust".to_string()],
},
])
}
}
struct FailingExtractor;
impl Extractor for FailingExtractor {
fn extract(&self, _text: &str) -> Result<Vec<ExtractedFact>, ExtractError> {
Err(ExtractError::Backend("model offline".to_string()))
}
}
fn service() -> (TempDir, MemoryService<HashEmbedder>) {
let dir = tempfile::tempdir().expect("tempdir");
let svc = MemoryService::open(dir.path(), HashEmbedder::new(DEFAULT_DIMENSION))
.expect("open service");
(dir, svc)
}
#[test]
fn remember_extracted_stores_every_fact() {
let (_dir, svc) = service();
let ids = svc
.remember_extracted("Alice and Bob both work in Rust.", &StubExtractor, None)
.expect("extract and remember");
assert_eq!(ids.len(), 2, "both extracted facts are stored");
assert_ne!(ids[0], ids[1]);
}
#[test]
fn why_traverses_the_auto_built_graph() {
let (_dir, svc) = service();
svc.remember_extracted("Alice and Bob both work in Rust.", &StubExtractor, None)
.expect("extract and remember");
let explanation = svc.why("parser shipped in rust", 2, None).expect("why");
assert!(
explanation.nodes.len() > 1,
"graph is alive: why() reaches beyond the seed, got {} node(s)",
explanation.nodes.len()
);
let reaches_sibling = explanation
.nodes
.iter()
.any(|node| node.content.contains("Bob"));
assert!(
reaches_sibling,
"why() hops through the shared `rust` topic to Bob's fact: {:?}",
explanation
.nodes
.iter()
.map(|node| node.content.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn shared_topic_collapses_onto_one_hub() {
let (_dir, svc) = service();
let first = svc
.remember_extracted("x", &StubExtractor, None)
.expect("first");
let second = svc
.remember_extracted("y", &StubExtractor, None)
.expect("second");
assert_eq!(first, second, "identical facts are idempotent across calls");
}
#[test]
fn recall_excludes_entity_hubs() {
let (_dir, svc) = service();
svc.remember_extracted("Alice and Bob both work in Rust.", &StubExtractor, None)
.expect("extract and remember");
let hits = svc.recall("rust", 8, None).expect("recall");
assert!(!hits.is_empty(), "the facts are recalled");
assert!(
hits.iter().all(|hit| !hit.content.starts_with("Entity:")),
"recall must not surface entity hubs: {:?}",
hits.iter().map(|hit| &hit.content).collect::<Vec<_>>()
);
}
#[test]
fn why_seed_is_a_fact_not_a_hub() {
let (_dir, svc) = service();
svc.remember_extracted("Alice and Bob both work in Rust.", &StubExtractor, None)
.expect("extract and remember");
let explanation = svc.why("rust", 2, None).expect("why");
assert!(!explanation.nodes.is_empty(), "why finds a seed");
assert!(
!explanation.nodes[0].content.starts_with("Entity:"),
"the seed (primary answer) must be a real fact, got {:?}",
explanation.nodes[0].content
);
}
#[test]
fn empty_text_is_rejected() {
let (_dir, svc) = service();
assert!(matches!(
svc.remember_extracted(" ", &StubExtractor, None),
Err(MemoryError::EmptyFact)
));
}
#[test]
fn extractor_failure_is_surfaced() {
let (_dir, svc) = service();
assert!(matches!(
svc.remember_extracted("anything", &FailingExtractor, None),
Err(MemoryError::Extract(ExtractError::Backend(_)))
));
}
#[test]
fn user_metadata_kind_entity_is_not_excluded() {
let (_dir, svc) = service();
let id = svc
.remember(
"Orders entity is processed nightly",
&[],
Some(&meta("kind", Value::String("entity".to_string()))),
)
.expect("remember with kind=entity");
let hits = svc
.recall("orders entity nightly", 8, None)
.expect("recall");
assert!(
hits.iter().any(|h| h.id == id),
"a user fact tagged kind=entity must still be recalled"
);
}
#[test]
fn reserved_veles_key_is_rejected() {
let (_dir, svc) = service();
assert!(matches!(
svc.remember("sneaky", &[], Some(&meta("_veles_hub", Value::Bool(true)))),
Err(MemoryError::ReservedKey(k)) if k == "_veles_hub"
));
assert!(matches!(
svc.recall(
"q",
5,
Some(&meta("content", Value::String("x".to_string())))
),
Err(MemoryError::ReservedKey(_))
));
}
#[test]
fn reingesting_the_same_text_does_not_duplicate_edges() {
let (_dir, svc) = service();
svc.remember_extracted("Alice and Bob both work in Rust.", &StubExtractor, None)
.expect("first ingest");
let after_first = svc.why("parser shipped in rust", 2, None).expect("why 1");
svc.remember_extracted("Alice and Bob both work in Rust.", &StubExtractor, None)
.expect("second ingest");
let after_second = svc.why("parser shipped in rust", 2, None).expect("why 2");
assert_eq!(
after_first.edges.len(),
after_second.edges.len(),
"re-ingestion must be idempotent: no duplicate edges"
);
}