#![cfg(feature = "memory-core")]
use chrono::Utc;
use std::collections::HashSet;
use tempfile::tempdir;
use trusty_common::memory_core::community::{find_communities, partition};
use trusty_common::memory_core::store::kg::{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,
}
}
async fn build_kg(edges: &[(&str, &str, &str)]) -> (tempfile::TempDir, KnowledgeGraph) {
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
for (s, p, o) in edges {
kg.assert(t(s, p, o)).await.unwrap();
}
(dir, kg)
}
#[tokio::test]
async fn find_communities_detects_two_clusters() {
let (_dir, kg) = build_kg(&[
("A", "rel", "B"),
("B", "rel", "C"),
("C", "rel", "A"),
("X", "rel", "Y"),
("Y", "rel", "Z"),
("Z", "rel", "X"),
("C", "bridge", "X"),
])
.await;
let communities = partition(&kg);
assert!(
communities.len() >= 2,
"expected at least 2 communities, got {}: {:?}",
communities.len(),
communities
);
let comm_a = communities
.iter()
.find(|c| c.iter().any(|e| e == "A"))
.expect("A must belong to some community");
let comm_a_set: HashSet<&str> = comm_a.iter().map(String::as_str).collect();
assert!(
comm_a_set.contains("B") && comm_a_set.contains("C"),
"A's community should include B and C, got {:?}",
comm_a
);
let comm_x = communities
.iter()
.find(|c| c.iter().any(|e| e == "X"))
.expect("X must belong to some community");
let comm_x_set: HashSet<&str> = comm_x.iter().map(String::as_str).collect();
assert!(
comm_x_set.contains("Y") && comm_x_set.contains("Z"),
"X's community should include Y and Z, got {:?}",
comm_x
);
}
#[tokio::test]
async fn sparse_community_is_classified_as_gap() {
let mut edges: Vec<(String, String, String)> = Vec::new();
for i in 0..12 {
edges.push(("hub".to_string(), "rel".to_string(), format!("leaf_{i}")));
}
let dir = tempdir().unwrap();
let kg = KnowledgeGraph::open(&dir.path().join("kg.db")).unwrap();
for (s, p, o) in &edges {
kg.assert(t(s, p, o)).await.unwrap();
}
let gaps = find_communities(&kg);
assert!(
!gaps.is_empty(),
"expected at least one gap on a sparse hub-and-leaves graph, got 0"
);
let has_sparse_gap = gaps.iter().any(|g| g.internal_density < 0.2);
assert!(
has_sparse_gap,
"expected at least one gap with internal_density < 0.2, got densities: {:?}",
gaps.iter()
.map(|g| (g.entities.len(), g.internal_density))
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn dense_community_not_a_gap() {
let (_dir, kg) = build_kg(&[
("A", "r", "B"),
("A", "r", "C"),
("A", "r", "D"),
("B", "r", "C"),
("B", "r", "D"),
("C", "r", "D"),
])
.await;
let gaps = find_communities(&kg);
let four_clique_is_gap = gaps.iter().any(|g| {
let names: HashSet<&str> = g.entities.iter().map(String::as_str).collect();
names.contains("A") && names.contains("B") && names.contains("C") && names.contains("D")
});
assert!(
!four_clique_is_gap,
"K4 clique (density 1.0) must not be flagged as a gap; gaps = {:?}",
gaps
);
let communities = partition(&kg);
let clique = communities
.iter()
.find(|c| c.iter().any(|e| e == "A"))
.expect("A must belong to some community");
if clique.len() == 4 {
}
}
#[tokio::test]
async fn partition_covers_all_nodes() {
let (_dir, kg) = build_kg(&[
("alice", "knows", "bob"),
("bob", "knows", "carol"),
("dave", "knows", "eve"),
("frank", "knows", "grace"),
])
.await;
let communities = partition(&kg);
let mut seen: HashSet<String> = HashSet::new();
let mut total = 0usize;
for c in &communities {
for e in c {
total += 1;
assert!(
seen.insert(e.clone()),
"node {} appears in multiple communities: {:?}",
e,
communities
);
}
}
let expected: HashSet<&str> = ["alice", "bob", "carol", "dave", "eve", "frank", "grace"]
.into_iter()
.collect();
let actual: HashSet<&str> = seen.iter().map(String::as_str).collect();
assert_eq!(
actual, expected,
"partition must cover every node exactly once"
);
assert_eq!(total, expected.len(), "no node may be missing");
}
#[tokio::test]
async fn knowledge_gaps_on_sparse_graph() {
let (_dir, kg) = build_kg(&[
("hub", "rel", "leaf1"),
("hub", "rel", "leaf2"),
("hub", "rel", "leaf3"),
("hub", "rel", "leaf4"),
("hub", "rel", "leaf5"),
("hub", "rel", "leaf6"),
])
.await;
let gaps = kg.knowledge_gaps();
assert!(
!gaps.is_empty(),
"expected at least one gap on a sparse hub-and-leaves graph"
);
}
#[tokio::test]
async fn suggested_exploration_is_non_empty() {
let (_dir, kg) = build_kg(&[
("hub", "rel", "leaf1"),
("hub", "rel", "leaf2"),
("hub", "rel", "leaf3"),
("hub", "rel", "leaf4"),
("hub", "rel", "leaf5"),
])
.await;
let gaps = find_communities(&kg);
assert!(!gaps.is_empty(), "expected at least one gap to exist");
for g in &gaps {
assert!(
!g.suggested_exploration.is_empty(),
"suggested_exploration must be non-empty for every gap"
);
}
}