use recall_graph::types::*;
use recall_graph::GraphMemory;
use tempfile::TempDir;
async fn setup() -> (GraphMemory, TempDir) {
let dir = TempDir::new().unwrap();
let gm = GraphMemory::open(dir.path()).await.unwrap();
(gm, dir)
}
fn rust_entity() -> NewEntity {
NewEntity {
name: "Rust".to_string(),
entity_type: EntityType::Tool,
abstract_text: "Systems programming language focused on safety and performance".to_string(),
overview: Some("Rust is a multi-paradigm systems programming language.".to_string()),
content: None,
attributes: None,
source: Some("test".to_string()),
}
}
fn voice_echo_entity() -> NewEntity {
NewEntity {
name: "voice-echo".to_string(),
entity_type: EntityType::Project,
abstract_text: "Voice assistant pipeline built in Rust using Twilio and Claude".to_string(),
overview: None,
content: None,
attributes: None,
source: Some("test".to_string()),
}
}
#[tokio::test]
async fn add_and_get_entity() {
let (gm, _dir) = setup().await;
let entity = gm.add_entity(rust_entity()).await.unwrap();
assert_eq!(entity.name, "Rust");
assert_eq!(entity.entity_type, EntityType::Tool);
assert!(entity.embedding.is_some());
assert!(entity.mutable);
let found = gm.get_entity("Rust").await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().name, "Rust");
let found_by_id = gm.get_entity_by_id(&entity.id_string()).await.unwrap();
assert!(found_by_id.is_some());
}
#[tokio::test]
async fn entity_not_found() {
let (gm, _dir) = setup().await;
let found = gm.get_entity("nonexistent").await.unwrap();
assert!(found.is_none());
}
#[tokio::test]
async fn update_entity() {
let (gm, _dir) = setup().await;
let entity = gm.add_entity(rust_entity()).await.unwrap();
let updated = gm
.update_entity(
&entity.id_string(),
EntityUpdate {
overview: Some("Updated overview for Rust.".to_string()),
..Default::default()
},
)
.await
.unwrap();
assert_eq!(updated.overview, "Updated overview for Rust.");
}
#[tokio::test]
async fn delete_entity() {
let (gm, _dir) = setup().await;
let entity = gm.add_entity(rust_entity()).await.unwrap();
gm.delete_entity(&entity.id_string()).await.unwrap();
let found = gm.get_entity("Rust").await.unwrap();
assert!(found.is_none());
}
#[tokio::test]
async fn list_entities() {
let (gm, _dir) = setup().await;
gm.add_entity(rust_entity()).await.unwrap();
gm.add_entity(voice_echo_entity()).await.unwrap();
let all = gm.list_entities(None).await.unwrap();
assert_eq!(all.len(), 2);
let tools = gm.list_entities(Some("tool")).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "Rust");
let projects = gm.list_entities(Some("project")).await.unwrap();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].name, "voice-echo");
}
#[tokio::test]
async fn add_and_get_relationship() {
let (gm, _dir) = setup().await;
gm.add_entity(rust_entity()).await.unwrap();
gm.add_entity(voice_echo_entity()).await.unwrap();
let rel = gm
.add_relationship(NewRelationship {
from_entity: "voice-echo".to_string(),
to_entity: "Rust".to_string(),
rel_type: "WRITTEN_IN".to_string(),
description: Some("voice-echo is written in Rust".to_string()),
confidence: None,
source: Some("test".to_string()),
})
.await
.unwrap();
assert_eq!(rel.rel_type, "WRITTEN_IN");
assert!(rel.valid_until.is_none());
let rels = gm
.get_relationships("voice-echo", Direction::Outgoing)
.await
.unwrap();
assert_eq!(rels.len(), 1);
assert_eq!(rels[0].rel_type, "WRITTEN_IN");
}
#[tokio::test]
async fn semantic_search() {
let (gm, _dir) = setup().await;
gm.add_entity(rust_entity()).await.unwrap();
gm.add_entity(voice_echo_entity()).await.unwrap();
let results = gm.search("programming language", 5).await.unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].entity.name, "Rust");
}
#[tokio::test]
async fn traverse_graph() {
let (gm, _dir) = setup().await;
gm.add_entity(rust_entity()).await.unwrap();
gm.add_entity(voice_echo_entity()).await.unwrap();
gm.add_relationship(NewRelationship {
from_entity: "voice-echo".to_string(),
to_entity: "Rust".to_string(),
rel_type: "WRITTEN_IN".to_string(),
description: None,
confidence: None,
source: None,
})
.await
.unwrap();
let tree = gm.traverse("voice-echo", 1).await.unwrap();
assert_eq!(tree.entity.name, "voice-echo");
assert_eq!(tree.edges.len(), 1);
assert_eq!(tree.edges[0].rel_type, "WRITTEN_IN");
assert_eq!(tree.edges[0].target.entity.name, "Rust");
}
#[tokio::test]
async fn graph_stats() {
let (gm, _dir) = setup().await;
let stats = gm.stats().await.unwrap();
assert_eq!(stats.entity_count, 0);
assert_eq!(stats.relationship_count, 0);
gm.add_entity(rust_entity()).await.unwrap();
gm.add_entity(voice_echo_entity()).await.unwrap();
let stats = gm.stats().await.unwrap();
assert_eq!(stats.entity_count, 2);
}
#[tokio::test]
async fn immutable_entity_type() {
let (gm, _dir) = setup().await;
let entity = gm
.add_entity(NewEntity {
name: "Architecture Decision".to_string(),
entity_type: EntityType::Decision,
abstract_text: "Chose SurrealDB for graph storage".to_string(),
overview: None,
content: None,
attributes: None,
source: None,
})
.await
.unwrap();
assert!(!entity.mutable); }
#[tokio::test]
async fn supersede_relationship() {
let (gm, _dir) = setup().await;
gm.add_entity(rust_entity()).await.unwrap();
gm.add_entity(voice_echo_entity()).await.unwrap();
let old_rel = gm
.add_relationship(NewRelationship {
from_entity: "voice-echo".to_string(),
to_entity: "Rust".to_string(),
rel_type: "USES".to_string(),
description: Some("old relationship".to_string()),
confidence: Some(0.5),
source: None,
})
.await
.unwrap();
let new_rel = gm
.supersede_relationship(
&old_rel.id_string(),
NewRelationship {
from_entity: "voice-echo".to_string(),
to_entity: "Rust".to_string(),
rel_type: "WRITTEN_IN".to_string(),
description: Some("superseded relationship".to_string()),
confidence: Some(1.0),
source: None,
},
)
.await
.unwrap();
assert_eq!(new_rel.rel_type, "WRITTEN_IN");
assert!(new_rel.valid_until.is_none());
}
#[tokio::test]
async fn search_with_type_filter() {
let (gm, _dir) = setup().await;
gm.add_entity(rust_entity()).await.unwrap();
gm.add_entity(voice_echo_entity()).await.unwrap();
let results = gm
.search_with_options(
"programming language",
&SearchOptions {
limit: 5,
entity_type: Some("tool".to_string()),
keyword: None,
},
)
.await
.unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].entity.name, "Rust");
for r in &results {
assert_eq!(r.entity.entity_type, EntityType::Tool);
}
let project_results = gm
.search_with_options(
"programming language",
&SearchOptions {
limit: 5,
entity_type: Some("project".to_string()),
keyword: None,
},
)
.await
.unwrap();
for r in &project_results {
assert_ne!(r.entity.name, "Rust");
}
}
#[tokio::test]
async fn search_episodes() {
let (gm, _dir) = setup().await;
gm.add_episode(NewEpisode {
session_id: "sess-001".to_string(),
abstract_text: "Discussed Rust memory safety and ownership model".to_string(),
overview: Some("Deep dive into borrow checker".to_string()),
content: None,
log_number: Some(1),
})
.await
.unwrap();
gm.add_episode(NewEpisode {
session_id: "sess-002".to_string(),
abstract_text: "Set up Docker containers for the web app deployment".to_string(),
overview: None,
content: None,
log_number: Some(2),
})
.await
.unwrap();
let results = gm.search_episodes("Rust ownership", 5).await.unwrap();
assert!(!results.is_empty());
assert_eq!(results[0].episode.session_id, "sess-001");
}
#[tokio::test]
async fn hybrid_query() {
let (gm, _dir) = setup().await;
gm.add_entity(rust_entity()).await.unwrap();
gm.add_entity(voice_echo_entity()).await.unwrap();
gm.add_entity(NewEntity {
name: "SurrealDB".to_string(),
entity_type: EntityType::Tool,
abstract_text: "Multi-model database with embedded and distributed modes".to_string(),
overview: None,
content: None,
attributes: None,
source: Some("test".to_string()),
})
.await
.unwrap();
gm.add_relationship(NewRelationship {
from_entity: "voice-echo".to_string(),
to_entity: "Rust".to_string(),
rel_type: "WRITTEN_IN".to_string(),
description: None,
confidence: None,
source: None,
})
.await
.unwrap();
gm.add_episode(NewEpisode {
session_id: "sess-003".to_string(),
abstract_text: "Built voice assistant pipeline with Rust and Twilio integration"
.to_string(),
overview: None,
content: None,
log_number: Some(3),
})
.await
.unwrap();
let result = gm
.query(
"voice assistant",
&QueryOptions {
limit: 10,
entity_type: None,
keyword: None,
graph_depth: 1,
include_episodes: true,
},
)
.await
.unwrap();
assert!(!result.entities.is_empty());
let entity_names: Vec<&str> = result
.entities
.iter()
.map(|e| e.entity.name.as_str())
.collect();
assert!(entity_names.contains(&"voice-echo"));
assert!(!result.episodes.is_empty());
let has_graph_source = result
.entities
.iter()
.any(|e| matches!(e.source, MatchSource::Graph { .. }));
let has_rust = entity_names.contains(&"Rust");
assert!(has_rust || has_graph_source);
}
#[tokio::test]
async fn traversal_with_type_filter() {
let (gm, _dir) = setup().await;
gm.add_entity(rust_entity()).await.unwrap();
gm.add_entity(voice_echo_entity()).await.unwrap();
gm.add_entity(NewEntity {
name: "SurrealDB".to_string(),
entity_type: EntityType::Tool,
abstract_text: "Multi-model database".to_string(),
overview: None,
content: None,
attributes: None,
source: None,
})
.await
.unwrap();
gm.add_relationship(NewRelationship {
from_entity: "voice-echo".to_string(),
to_entity: "Rust".to_string(),
rel_type: "WRITTEN_IN".to_string(),
description: None,
confidence: None,
source: None,
})
.await
.unwrap();
gm.add_relationship(NewRelationship {
from_entity: "voice-echo".to_string(),
to_entity: "SurrealDB".to_string(),
rel_type: "USES".to_string(),
description: None,
confidence: None,
source: None,
})
.await
.unwrap();
let tree = gm.traverse("voice-echo", 1).await.unwrap();
assert_eq!(tree.edges.len(), 2);
let filtered = gm
.traverse_filtered("voice-echo", 1, Some("tool"))
.await
.unwrap();
assert_eq!(filtered.edges.len(), 2);
let filtered_proj = gm
.traverse_filtered("voice-echo", 1, Some("project"))
.await
.unwrap();
assert_eq!(filtered_proj.edges.len(), 0);
}