#![cfg(feature = "memory-core")]
use chrono::Utc;
use tempfile::tempdir;
use trusty_common::memory_core::store::kg::{KgEdge, KnowledgeGraph, Triple};
fn t(subject: &str, predicate: &str, object: &str) -> Triple {
Triple {
subject: subject.into(),
predicate: predicate.into(),
object: object.into(),
valid_from: Utc::now(),
valid_to: None,
confidence: 1.0,
provenance: None,
}
}
#[tokio::test]
async fn hydration_populates_graph() {
let dir = tempdir().unwrap();
let path = dir.path().join("kg.db");
{
let kg = KnowledgeGraph::open(&path).unwrap();
for (s, p, o) in [
("alice", "knows", "bob"),
("bob", "knows", "carol"),
("carol", "knows", "dave"),
("alice", "manages", "eve"),
("eve", "reports_to", "alice"),
] {
kg.assert(t(s, p, o)).await.unwrap();
}
}
let kg = KnowledgeGraph::open(&path).unwrap();
let total_degree: usize = ["alice", "bob", "carol", "dave", "eve"]
.iter()
.map(|n| kg.neighbors(n).unwrap().len())
.sum();
assert_eq!(total_degree, 2 * 5, "expected 5 edges hydrated");
}
#[tokio::test]
async fn assert_adds_edge() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("alice", "knows", "bob")).await.unwrap();
let alice_neighbors: Vec<(String, KgEdge)> = kg.neighbors("alice").unwrap();
assert_eq!(alice_neighbors.len(), 1, "alice has one outgoing edge");
assert_eq!(alice_neighbors[0].0, "bob");
assert_eq!(alice_neighbors[0].1.predicate, "knows");
let bob_neighbors = kg.neighbors("bob").unwrap();
assert_eq!(bob_neighbors.len(), 1, "bob has one incoming edge");
assert_eq!(bob_neighbors[0].0, "alice");
}
#[tokio::test]
async fn retract_removes_edge() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("alice", "knows", "bob")).await.unwrap();
assert_eq!(kg.neighbors("alice").unwrap().len(), 1);
let closed = kg.retract("alice", "knows").await.unwrap();
assert_eq!(closed, 1);
assert!(
kg.neighbors("alice").unwrap().is_empty(),
"retract should remove the edge from alice"
);
assert!(
kg.neighbors("bob").unwrap().is_empty(),
"retract should remove the edge from bob too"
);
kg.assert(t("alice", "likes", "bob")).await.unwrap();
let after = kg.neighbors("alice").unwrap();
assert_eq!(after.len(), 1);
assert_eq!(after[0].0, "bob");
assert_eq!(after[0].1.predicate, "likes");
}
#[tokio::test]
async fn neighbors_returns_connected() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("a", "links", "b")).await.unwrap();
kg.assert(t("b", "links", "c")).await.unwrap();
let mut b_neighbors: Vec<String> = kg
.neighbors("b")
.unwrap()
.into_iter()
.map(|(n, _)| n)
.collect();
b_neighbors.sort();
assert_eq!(b_neighbors, vec!["a".to_string(), "c".to_string()]);
let a_neighbors = kg.neighbors("a").unwrap();
assert_eq!(a_neighbors.len(), 1);
assert_eq!(a_neighbors[0].0, "b");
assert!(kg.neighbors("nope").unwrap().is_empty());
}
#[tokio::test]
async fn shortest_path_finds_route() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("A", "step", "B")).await.unwrap();
kg.assert(t("B", "step", "C")).await.unwrap();
let path = kg.shortest_path("A", "C").unwrap();
assert_eq!(
path,
Some(vec!["A".to_string(), "B".to_string(), "C".to_string()])
);
let self_path = kg.shortest_path("A", "A").unwrap();
assert_eq!(self_path, Some(vec!["A".to_string()]));
assert_eq!(kg.shortest_path("A", "missing").unwrap(), None);
assert_eq!(kg.shortest_path("missing", "C").unwrap(), None);
kg.assert(t("X", "knows", "Y")).await.unwrap();
assert_eq!(kg.shortest_path("A", "X").unwrap(), None);
}
#[tokio::test]
async fn bfs_reachable_within_hops() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("A", "to", "B")).await.unwrap();
kg.assert(t("B", "to", "C")).await.unwrap();
kg.assert(t("C", "to", "D")).await.unwrap();
let mut hits = kg.reachable("A", 2).unwrap();
hits.sort();
assert_eq!(
hits,
vec!["B".to_string(), "C".to_string()],
"BFS within 2 hops must include B and C but not D"
);
assert!(kg.reachable("A", 0).unwrap().is_empty());
let mut all = kg.reachable("A", 3).unwrap();
all.sort();
assert_eq!(all, vec!["B".to_string(), "C".to_string(), "D".to_string()]);
assert!(kg.reachable("missing", 5).unwrap().is_empty());
}
#[tokio::test]
async fn reverse_lookup_returns_incoming() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("A", "to", "B")).await.unwrap();
kg.assert(t("B", "to", "C")).await.unwrap();
let into_c = kg.incoming("C").unwrap();
assert_eq!(into_c.len(), 1, "C has exactly one incoming edge");
assert_eq!(into_c[0].0, "B");
assert_eq!(into_c[0].1.predicate, "to");
assert!(kg.incoming("A").unwrap().is_empty());
assert!(kg.incoming("nope").unwrap().is_empty());
}
#[tokio::test]
async fn connected_components_count() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("A", "to", "B")).await.unwrap();
kg.assert(t("X", "to", "Y")).await.unwrap();
assert_eq!(kg.connected_components().unwrap(), 2);
kg.assert(t("B", "to", "X")).await.unwrap();
assert_eq!(kg.connected_components().unwrap(), 1);
}
#[tokio::test]
async fn astar_path_finds_route() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("A", "to", "B")).await.unwrap();
kg.assert(t("B", "to", "C")).await.unwrap();
assert_eq!(
kg.astar_path("A", "C").unwrap(),
Some(vec!["A".to_string(), "B".to_string(), "C".to_string()])
);
assert_eq!(
kg.astar_path("A", "A").unwrap(),
Some(vec!["A".to_string()])
);
assert_eq!(kg.astar_path("A", "missing").unwrap(), None);
assert_eq!(kg.astar_path("missing", "C").unwrap(), None);
kg.assert(t("X", "to", "Y")).await.unwrap();
assert_eq!(kg.astar_path("A", "X").unwrap(), None);
}
#[tokio::test]
async fn list_subjects_matches_redb() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("alice", "knows", "bob")).await.unwrap();
kg.assert(t("alice", "likes", "rust")).await.unwrap();
kg.assert(t("bob", "knows", "alice")).await.unwrap();
kg.assert(t("carol", "knows", "dave")).await.unwrap();
let closed = kg.retract("carol", "knows").await.unwrap();
assert_eq!(closed, 1);
let subjects = kg.list_subjects(50).unwrap();
assert_eq!(
subjects,
vec!["alice".to_string(), "bob".to_string()],
"carol has no active triples and must not appear"
);
}
#[tokio::test]
async fn list_active_matches_redb() {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
kg.assert(t("alice", "knows", "bob")).await.unwrap();
kg.assert(t("alice", "likes", "rust")).await.unwrap();
kg.assert(t("bob", "knows", "alice")).await.unwrap();
let mut alice_rows: Vec<(String, String)> = kg
.query_active("alice")
.await
.unwrap()
.into_iter()
.map(|t| (t.predicate, t.object))
.collect();
alice_rows.sort();
assert_eq!(
alice_rows,
vec![
("knows".to_string(), "bob".to_string()),
("likes".to_string(), "rust".to_string()),
]
);
let bob_rows = kg.query_active("bob").await.unwrap();
assert_eq!(bob_rows.len(), 1);
assert_eq!(bob_rows[0].predicate, "knows");
assert_eq!(bob_rows[0].object, "alice");
}