use fathomdb_engine::{Engine, EngineError, PreparedWrite, ReadView, TraversalDirection};
use fathomdb_schema::SQLITE_SUFFIX;
use tempfile::TempDir;
fn db_path(dir: &TempDir, name: &str) -> std::path::PathBuf {
dir.path().join(format!("{name}{SQLITE_SUFFIX}"))
}
fn node(kind: &str, body: &str, logical_id: &str) -> PreparedWrite {
PreparedWrite::Node {
kind: kind.to_string(),
body: body.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some(logical_id.to_string()),
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
}
}
fn edge(from: &str, to: &str, logical_id: &str) -> PreparedWrite {
PreparedWrite::Edge {
kind: "link".to_string(),
from: from.to_string(),
to: to.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some(logical_id.to_string()),
body: None,
t_valid: None,
t_invalid: None,
confidence: None,
extractor_model_id: None,
temporal_fallback: None,
}
}
fn edge_with_t_invalid(from: &str, to: &str, logical_id: &str, t_invalid: i64) -> PreparedWrite {
PreparedWrite::Edge {
kind: "link".to_string(),
from: from.to_string(),
to: to.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some(logical_id.to_string()),
body: None,
t_valid: None,
t_invalid: Some(t_invalid),
confidence: None,
extractor_model_id: None,
temporal_fallback: None,
}
}
#[test]
fn graph_neighbors_depth1_returns_adjacent() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "depth1")).expect("open");
opened
.engine
.write(&[
node("doc", "Root A", "A"),
node("doc", "Node B", "B"),
node("doc", "Node C", "C"),
edge("A", "B", "E-AB"),
edge("A", "C", "E-AC"),
])
.expect("write");
let neighbors = opened
.engine
.graph_neighbors("A", 1, TraversalDirection::Outgoing, &ReadView::default())
.expect("graph_neighbors");
let mut ids: Vec<_> = neighbors.iter().map(|n| n.logical_id.as_str()).collect();
ids.sort_unstable();
assert_eq!(ids, vec!["B", "C"], "depth=1 outgoing must return both direct neighbors");
assert!(
!neighbors.iter().any(|n| n.logical_id == "A"),
"root node must not appear in its own neighbor set"
);
opened.engine.close().unwrap();
}
#[test]
fn graph_neighbors_depth2_returns_two_hops() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "depth2")).expect("open");
opened
.engine
.write(&[
node("doc", "Root A", "A"),
node("doc", "Node B", "B"),
node("doc", "Node C", "C"),
edge("A", "B", "E-AB"),
edge("B", "C", "E-BC"),
])
.expect("write");
let neighbors = opened
.engine
.graph_neighbors("A", 2, TraversalDirection::Outgoing, &ReadView::default())
.expect("graph_neighbors");
let mut ids: Vec<_> = neighbors.iter().map(|n| n.logical_id.as_str()).collect();
ids.sort_unstable();
assert_eq!(ids, vec!["B", "C"], "depth=2 must include both hop-1 (B) and hop-2 (C)");
opened.engine.close().unwrap();
}
#[test]
fn graph_neighbors_depth3_limit() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "depth3")).expect("open");
opened
.engine
.write(&[
node("doc", "Root A", "A"),
node("doc", "Node B", "B"),
node("doc", "Node C", "C"),
node("doc", "Node D", "D"),
edge("A", "B", "E-AB"),
edge("B", "C", "E-BC"),
edge("C", "D", "E-CD"),
])
.expect("write");
let neighbors = opened
.engine
.graph_neighbors("A", 3, TraversalDirection::Outgoing, &ReadView::default())
.expect("graph_neighbors");
let mut ids: Vec<_> = neighbors.iter().map(|n| n.logical_id.as_str()).collect();
ids.sort_unstable();
assert_eq!(ids, vec!["B", "C", "D"], "depth=3 must include all three hops");
opened.engine.close().unwrap();
}
#[test]
fn graph_neighbors_depth_gt3_rejected() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "depth_gt3")).expect("open");
opened.engine.write(&[node("doc", "Root A", "A")]).expect("write");
let result =
opened.engine.graph_neighbors("A", 4, TraversalDirection::Outgoing, &ReadView::default());
match result {
Err(EngineError::InvalidArgument { .. }) => {
}
other => panic!("depth=4 must return EngineError::InvalidArgument, got: {other:?}"),
}
opened.engine.close().unwrap();
}
#[test]
fn graph_neighbors_cycle_guard() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "cycle")).expect("open");
opened
.engine
.write(&[
node("doc", "Root A", "A"),
node("doc", "Node B", "B"),
edge("A", "B", "E-AB"),
edge("B", "A", "E-BA"),
])
.expect("write");
let neighbors = opened
.engine
.graph_neighbors("A", 3, TraversalDirection::Both, &ReadView::default())
.expect("graph_neighbors must not loop on cycles");
let has_b = neighbors.iter().any(|n| n.logical_id == "B");
let has_a = neighbors.iter().any(|n| n.logical_id == "A");
assert!(has_b, "B must be reachable from A via A→B");
assert!(!has_a, "root A must not appear in the neighbor set (cycle guard prevents revisiting)");
opened.engine.close().unwrap();
}
#[test]
fn graph_neighbors_cap50_enforced() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "cap50")).expect("open");
let mut batch = vec![node("doc", "Root", "ROOT")];
for i in 0..60u32 {
let id = format!("N{i}");
let eid = format!("E{i}");
batch.push(node("doc", &format!("Node {i}"), &id));
batch.push(edge("ROOT", &id, &eid));
}
opened.engine.write(&batch).expect("write");
let neighbors = opened
.engine
.graph_neighbors("ROOT", 1, TraversalDirection::Outgoing, &ReadView::default())
.expect("graph_neighbors");
assert!(neighbors.len() <= 50, "hard cap 50 must be enforced; got {} results", neighbors.len());
opened.engine.close().unwrap();
}
#[test]
fn graph_neighbors_valid_time_filter_drops_invalidated() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "valid_time")).expect("open");
let past = 946_684_800; opened
.engine
.write(&[
node("doc", "Root A", "A"),
node("doc", "Node B", "B"),
node("doc", "Node C", "C"),
edge_with_t_invalid("A", "B", "E-AB", past),
edge("A", "C", "E-AC"),
])
.expect("write");
let neighbors = opened
.engine
.graph_neighbors("A", 1, TraversalDirection::Outgoing, &ReadView::default())
.expect("graph_neighbors");
let ids: Vec<_> = neighbors.iter().map(|n| n.logical_id.as_str()).collect();
assert!(
!ids.contains(&"B"),
"node B must NOT be reachable via an invalidated edge (t_invalid in the past)"
);
assert!(ids.contains(&"C"), "node C must still be reachable via the valid edge");
opened.engine.close().unwrap();
}
#[test]
fn explain_plan_uses_indexes() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "explain")).expect("open");
opened
.engine
.write(&[
node("doc", "Root", "ROOT"),
node("doc", "Child", "CHILD"),
edge("ROOT", "CHILD", "E1"),
])
.expect("write");
let plan = opened
.engine
.explain_graph_neighbors_for_test("ROOT", 1, TraversalDirection::Outgoing)
.expect("explain_graph_neighbors_for_test");
let has_index =
plan.iter().any(|line| line.contains("canonical_edges") && line.contains("USING INDEX"));
let has_scan = plan.iter().any(|line| {
line.contains("SCAN canonical_edges") && !line.contains("USING INDEX")
});
assert!(has_index, "BFS CTE must use an index on canonical_edges;\nplan:\n{}", plan.join("\n"));
assert!(!has_scan, "BFS CTE must NOT full-scan canonical_edges;\nplan:\n{}", plan.join("\n"));
opened.engine.close().unwrap();
}
#[test]
fn search_expand_returns_neighbors() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "expand")).expect("open");
opened
.engine
.write(&[
node("doc", "xyzunique expand quark alpha", "A"),
node("doc", "neighbor node B body", "B"),
edge("A", "B", "E-AB"),
])
.expect("write");
let result =
opened.engine.search_expand("xyzunique expand quark", None, 1).expect("search_expand");
let expanded_ids: Vec<_> =
result.expanded.iter().map(|(n, _hop)| n.logical_id.as_str()).collect();
assert!(
expanded_ids.contains(&"B"),
"node B must appear in expanded; expanded={expanded_ids:?}\nresult.search_hits={:?}",
result.search_hits.iter().map(|h| h.id.clone()).collect::<Vec<_>>()
);
opened.engine.close().unwrap();
}
#[test]
fn search_expand_deduplicates() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "dedup")).expect("open");
opened
.engine
.write(&[
node("doc", "dedup shimmer unique test node alpha", "A"),
node("doc", "dedup shimmer unique test node beta", "B"),
edge("A", "B", "E-AB"),
])
.expect("write");
let result =
opened.engine.search_expand("dedup shimmer unique", None, 1).expect("search_expand");
let hit_ids: Vec<_> = result.search_hits.iter().map(|h| h.id.clone()).collect();
let expanded_logical_ids: Vec<_> =
result.expanded.iter().map(|(n, _hop)| n.logical_id.as_str()).collect();
assert!(
!expanded_logical_ids.contains(&"B"),
"B is a search hit and must not appear in expanded; expanded={expanded_logical_ids:?}"
);
assert!(!hit_ids.is_empty(), "search must return at least one hit for 'dedup shimmer unique'");
opened.engine.close().unwrap();
}
#[test]
fn expired_edge_is_excluded_from_traversal() {
let dir = TempDir::new().unwrap();
let opened =
Engine::open(dir.path().join(format!("fix1{SQLITE_SUFFIX}"))).expect("engine open");
let a_id = "fix1-A";
let b_id = "fix1-B";
opened
.engine
.write(&[
node("test", "{}", a_id),
node("test", "{}", b_id),
PreparedWrite::Edge {
logical_id: None,
from: a_id.to_string(),
to: b_id.to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
kind: "expired_link".to_string(),
t_invalid: Some(1_577_836_800), body: None,
t_valid: None,
confidence: None,
extractor_model_id: None,
temporal_fallback: None,
},
])
.expect("write");
let result = opened
.engine
.graph_neighbors(a_id, 1, TraversalDirection::Outgoing, &ReadView::default())
.expect("graph_neighbors");
assert!(result.is_empty(), "expired edge must be excluded from traversal; got {result:?}");
opened.engine.close().unwrap();
}
#[test]
fn search_expand_depth0_populates_all_logical_ids() {
let dir = TempDir::new().unwrap();
let opened =
Engine::open(dir.path().join(format!("depth0{SQLITE_SUFFIX}"))).expect("engine open");
opened
.engine
.write(&[node("note", r#"{"text":"quilted vermillion zephyr unique depth0"}"#, "depth0-X")])
.expect("write");
let result = opened
.engine
.search_expand("quilted vermillion zephyr unique depth0", None, 0)
.expect("search_expand depth=0");
assert!(
!result.all_logical_ids.is_empty(),
"depth=0 search_expand must populate all_logical_ids from search hits; got empty"
);
assert!(result.expanded.is_empty(), "depth=0 must produce no expanded nodes");
opened.engine.close().unwrap();
}
#[test]
fn graph_neighbors_depth0_rejected() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "depth0rej")).expect("open");
opened.engine.write(&[node("doc", "{}", "A")]).expect("write");
let result =
opened.engine.graph_neighbors("A", 0, TraversalDirection::Outgoing, &ReadView::default());
assert!(
matches!(result, Err(EngineError::InvalidArgument { .. })),
"graph_neighbors depth=0 must return InvalidArgument; got {result:?}"
);
opened.engine.close().unwrap();
}
#[test]
fn search_expand_reports_nearest_hop_count() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "nearest_hop")).expect("open");
opened
.engine
.write(&[
node("note", "nearest hop shimmer alpha unique1", "A"),
node("note", "nearest hop glimmer beta unique2", "B"),
node("note", "{}", "C"),
node("note", "{}", "D"),
node("note", "{}", "X"),
edge("A", "X", "E-AX"), edge("B", "C", "E-BC"),
edge("C", "D", "E-CD"),
edge("D", "X", "E-DX"), ])
.expect("write");
let result = opened
.engine
.search_expand("shimmer alpha unique1 glimmer beta unique2", None, 3)
.expect("search_expand");
let x_entry = result.expanded.iter().find(|(n, _)| n.logical_id == "X");
if let Some((_, hop)) = x_entry {
assert_eq!(*hop, 1, "X is 1 hop from A; nearest hop must be 1, not {hop}");
}
opened.engine.close().unwrap();
}
#[test]
fn graph_neighbors_inactive_intermediate_not_traversed() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "dangling")).expect("open");
opened
.engine
.write(&[
node("doc", "Root A", "A"),
node("doc", "Node C", "C"),
edge("A", "MISSING", "E-AM"),
edge("MISSING", "C", "E-MC"),
])
.expect("write");
let results = opened
.engine
.graph_neighbors(
"A",
2,
fathomdb_engine::TraversalDirection::Outgoing,
&ReadView::default(),
)
.expect("graph_neighbors");
let ids: Vec<&str> = results.iter().map(|n| n.logical_id.as_str()).collect();
assert!(
!ids.contains(&"C"),
"C is reachable only through MISSING (inactive) — must not appear; got {ids:?}"
);
opened.engine.close().unwrap();
}
#[test]
fn search_expand_anon_node_hit_does_not_crash() {
let dir = TempDir::new().unwrap();
let opened = Engine::open(db_path(&dir, "anon_hit")).expect("open");
opened
.engine
.write(&[
PreparedWrite::Node {
kind: "doc".to_string(),
body: "anon shimmer unique probe alpha node".to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: None, state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
},
node("doc", "named shimmer unique probe beta node", "NAMED"),
])
.expect("write");
let result = opened
.engine
.search_expand("shimmer unique probe", None, 1)
.expect("search_expand must not crash on anonymous hit");
let all_ids = &result.all_logical_ids;
assert!(
all_ids.contains(&"NAMED".to_string()) || !result.search_hits.is_empty(),
"expected at least one result; got hits={}, expanded={}",
result.search_hits.len(),
result.expanded.len()
);
opened.engine.close().unwrap();
}
#[test]
fn write_rejects_logical_id_containing_record_separator() {
let dir = TempDir::new().unwrap();
let engine = Engine::open(db_path(&dir, "rs_reject")).expect("open failed").engine;
let bad_id = "A\x1eB"; let result = engine.write(&[PreparedWrite::Node {
kind: "doc".to_string(),
body: "body".to_string(),
source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: Some(bad_id.to_string()),
state: fathomdb_engine::InitialState::Active,
reason: None,
valid_from: None,
valid_until: None,
}]);
assert!(
matches!(result, Err(EngineError::WriteValidation)),
"logical_id containing 0x1E must be rejected; got {result:?}"
);
engine.close().unwrap();
}
#[test]
fn write_rejects_edge_endpoint_containing_record_separator() {
let dir = TempDir::new().unwrap();
let engine = Engine::open(db_path(&dir, "rs_edge_reject")).expect("open failed").engine;
let result = engine.write(&[PreparedWrite::Edge {
kind: "link".to_string(),
from: "A".to_string(),
to: "B\x1eC".to_string(), source_id: fathomdb_engine::SourceId::new("test:fixture").expect("test source id"),
logical_id: None,
body: None,
t_valid: None,
t_invalid: None,
confidence: None,
extractor_model_id: None,
temporal_fallback: None,
}]);
assert!(
matches!(result, Err(EngineError::WriteValidation)),
"edge to containing 0x1E must be rejected; got {result:?}"
);
engine.close().unwrap();
}