use atheneum::graph::{AtheneumGraph, EdgeType, EntityType};
use serde_json::json;
fn create_fake_magellan_db(symbols: &[(&str, &str)]) -> tempfile::NamedTempFile {
let db = tempfile::NamedTempFile::new().expect("tempfile");
let conn = rusqlite::Connection::open(db.path()).expect("open temp db");
conn.execute_batch(
"CREATE TABLE graph_entities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
name TEXT NOT NULL,
file_path TEXT,
data TEXT NOT NULL
);",
)
.expect("schema");
for (name, fqn) in symbols {
let data = serde_json::json!({
"fqn": fqn,
"kind": "function",
});
conn.execute(
"INSERT INTO graph_entities (kind, name, file_path, data) VALUES ('Symbol', ?1, 'src/lib.rs', ?2)",
rusqlite::params![name, serde_json::to_string(&data).unwrap()],
)
.expect("insert symbol");
}
drop(conn);
db
}
#[test]
fn test_edge_type_explains_roundtrip() {
let e = EdgeType::Explains;
assert_eq!(e.as_str(), "explains");
assert_eq!(EdgeType::from_label("explains"), Some(EdgeType::Explains));
}
#[test]
fn test_edge_type_derived_from_roundtrip() {
let e = EdgeType::DerivedFrom;
assert_eq!(e.as_str(), "derived_from");
assert_eq!(
EdgeType::from_label("derived_from"),
Some(EdgeType::DerivedFrom)
);
}
#[test]
fn test_edge_type_explains_serde_roundtrip() {
let json = serde_json::to_string(&EdgeType::Explains).expect("serialize");
assert_eq!(json, "\"explains\"");
let back: EdgeType = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, EdgeType::Explains);
}
#[test]
fn test_edge_type_derived_from_serde_roundtrip() {
let json = serde_json::to_string(&EdgeType::DerivedFrom).expect("serialize");
assert_eq!(json, "\"derived_from\"");
let back: EdgeType = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, EdgeType::DerivedFrom);
}
#[test]
fn test_edge_type_explains_in_all() {
assert!(
EdgeType::all().contains(&EdgeType::Explains),
"Explains must appear in EdgeType::all()"
);
}
#[test]
fn test_edge_type_derived_from_in_all() {
assert!(
EdgeType::all().contains(&EdgeType::DerivedFrom),
"DerivedFrom must appear in EdgeType::all()"
);
}
#[test]
fn test_entity_type_wiki_page() {
assert_eq!(EntityType::WikiPage.as_str(), "WikiPage");
}
#[test]
fn test_ingest_wiki_page_creates_graph_entity() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/test.md",
"---\ntitle: Test\n---\nHello world\n",
Some("proj"),
)
.expect("ingest");
let entity = graph.get_entity(page_id).expect("get_entity");
assert_eq!(entity.kind, "WikiPage");
assert_eq!(entity.name, "wiki/test.md");
assert_eq!(entity.file_path, Some("wiki/test.md".to_string()));
}
#[test]
fn test_ingest_wiki_page_stores_project_id_in_data() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/test.md",
"---\ntitle: Test\n---\nHello world\n",
Some("my-project"),
)
.expect("ingest");
let entity = graph.get_entity(page_id).expect("get_entity");
assert_eq!(
entity.data.get("project_id").and_then(|v| v.as_str()),
Some("my-project")
);
}
#[test]
fn test_ingest_wiki_page_creates_wikilink_edges() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/source.md",
"---\ntitle: Source\n---\nSee [[Dest]] and [[Other]]\n",
Some("proj"),
)
.expect("ingest");
let (outgoing, _) = graph.get_neighbors(page_id).expect("neighbors");
let wikilink_edges: Vec<_> = outgoing
.iter()
.filter(|e| e.edge_type == EdgeType::Wikilink.as_str())
.collect();
assert_eq!(wikilink_edges.len(), 2, "should have 2 wikilink edges");
}
#[test]
fn test_ingest_wiki_page_idempotent() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let id1 = graph
.ingest_wiki_page("wiki/test.md", "---\ntitle: V1\n---\nv1\n", Some("proj"))
.expect("ingest1");
let id2 = graph
.ingest_wiki_page("wiki/test.md", "---\ntitle: V2\n---\nv2\n", Some("proj"))
.expect("ingest2");
assert_eq!(
id1, id2,
"re-ingesting same path should return same entity id"
);
}
#[test]
fn test_get_subgraph_filtered_by_edge_type() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/source.md",
"---\ntitle: Source\n---\nLinks to [[Dest]]\n",
Some("proj"),
)
.expect("ingest");
let agent = graph.insert_agent("test-agent", json!({})).expect("agent");
graph
.insert_edge(
page_id,
agent,
EdgeType::Mentions,
json!({"context": "test"}),
)
.expect("mentions edge");
let sg = graph
.get_subgraph_filtered(page_id, 2, &[EdgeType::Wikilink])
.expect("subgraph filtered");
assert!(
sg.edges
.iter()
.any(|e| e.edge_type == EdgeType::Wikilink.as_str()),
"should contain wikilink edge"
);
assert!(
!sg.edges
.iter()
.any(|e| e.edge_type == EdgeType::Mentions.as_str()),
"should NOT contain mentions edge"
);
}
#[test]
fn test_get_subgraph_filtered_empty_types_returns_all() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/source.md",
"---\ntitle: Source\n---\nLinks to [[Dest]]\n",
Some("proj"),
)
.expect("ingest");
let sg = graph
.get_subgraph_filtered(page_id, 2, &[])
.expect("subgraph filtered");
assert!(!sg.edges.is_empty(), "empty filter should return all edges");
}
#[test]
fn test_get_subgraph_filtered_multiple_types() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/source.md",
"---\ntitle: Source\n---\nLinks to [[Dest]]\n",
Some("proj"),
)
.expect("ingest");
let agent = graph.insert_agent("test-agent", json!({})).expect("agent");
graph
.insert_edge(page_id, agent, EdgeType::Explains, json!({}))
.expect("explains edge");
let sg = graph
.get_subgraph_filtered(page_id, 2, &[EdgeType::Wikilink, EdgeType::Explains])
.expect("subgraph filtered");
assert!(
sg.edges
.iter()
.any(|e| e.edge_type == EdgeType::Wikilink.as_str()),
"should contain wikilink edge"
);
assert!(
sg.edges
.iter()
.any(|e| e.edge_type == EdgeType::Explains.as_str()),
"should contain explains edge"
);
assert_eq!(sg.edges.len(), 2, "1 wikilink + 1 explains = 2");
}
#[test]
fn test_link_wiki_to_symbols_creates_explains_edges() {
let magellan = create_fake_magellan_db(&[
("build_router", "atheneum::build_router"),
("AtheneumGraph", "atheneum::AtheneumGraph"),
]);
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/http-handler.md",
"---\ntitle: HTTP Handler\n---\nSee [[build_router]] for details.\n",
Some("atheneum"),
)
.expect("ingest");
let linked = graph
.link_wiki_to_symbols(magellan.path(), "atheneum", Some("atheneum"))
.expect("link");
assert_eq!(linked, 1, "should link 1 wikilink to a symbol");
let (outgoing, _) = graph.get_neighbors(page_id).expect("neighbors");
let explains_edges: Vec<_> = outgoing
.iter()
.filter(|e| e.edge_type == EdgeType::Explains.as_str())
.collect();
assert_eq!(explains_edges.len(), 1, "should have 1 Explains edge");
let target_id = explains_edges[0].to_id;
let target = graph.get_entity(target_id).expect("target entity");
assert_eq!(target.name, "atheneum: build_router");
}
#[test]
fn test_link_wiki_to_symbols_no_match_is_zero() {
let magellan = create_fake_magellan_db(&[("unrelated_symbol", "foo::bar")]);
let graph = AtheneumGraph::open_in_memory().expect("open");
graph
.ingest_wiki_page(
"wiki/test.md",
"---\ntitle: Test\n---\nSee [[build_router]] for details.\n",
Some("atheneum"),
)
.expect("ingest");
let linked = graph
.link_wiki_to_symbols(magellan.path(), "atheneum", Some("atheneum"))
.expect("link");
assert_eq!(linked, 0, "no matching symbols should link 0");
}
#[test]
fn test_link_wiki_to_symbols_idempotent() {
let magellan = create_fake_magellan_db(&[("build_router", "atheneum::build_router")]);
let graph = AtheneumGraph::open_in_memory().expect("open");
graph
.ingest_wiki_page(
"wiki/test.md",
"---\ntitle: Test\n---\nSee [[build_router]]\n",
Some("atheneum"),
)
.expect("ingest");
let linked1 = graph
.link_wiki_to_symbols(magellan.path(), "atheneum", Some("atheneum"))
.expect("link1");
let linked2 = graph
.link_wiki_to_symbols(magellan.path(), "atheneum", Some("atheneum"))
.expect("link2");
assert_eq!(linked1, 1, "first run links 1 symbol");
assert_eq!(linked2, 0, "second run should not duplicate edges");
}
#[test]
fn test_estimate_tokens_entity() {
use atheneum::GraphEntity;
let entity = GraphEntity {
id: 1,
kind: "WikiPage".to_string(),
name: "wiki/test.md".to_string(),
file_path: Some("wiki/test.md".to_string()),
data: serde_json::json!({"title": "Test Page", "body": "Hello world"}),
};
let tokens = atheneum::graph::estimate_entity_tokens(&entity);
assert!(tokens > 0, "token estimate should be positive");
assert!(
tokens < 500,
"small entity should be under 500 tokens, got {}",
tokens
);
}
#[test]
fn test_estimate_tokens_is_consistent() {
use atheneum::GraphEntity;
let e1 = GraphEntity {
id: 1,
kind: "A".to_string(),
name: "x".to_string(),
file_path: None,
data: serde_json::json!({}),
};
let e2 = GraphEntity {
id: 2,
kind: "B".to_string(),
name: "y".to_string(),
file_path: None,
data: serde_json::json!({"extra": "data here"}),
};
let t1 = atheneum::graph::estimate_entity_tokens(&e1);
let t2 = atheneum::graph::estimate_entity_tokens(&e2);
assert!(
t2 > t1,
"entity with more data should have higher token estimate ({} vs {})",
t2,
t1
);
}
#[test]
fn test_truncate_subgraph_keeps_entry() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/entry.md",
"---\ntitle: Entry\n---\nLinks to [[A]] and [[B]] and [[C]]\n",
Some("proj"),
)
.expect("ingest");
let sg = graph.get_subgraph(page_id, 2).expect("subgraph");
let full_count = sg.entities.len();
assert!(full_count > 1, "should have more than just the entry");
let entry_tokens = atheneum::graph::estimate_entity_tokens(&sg.entry);
let budget = entry_tokens + 1;
let truncated = atheneum::graph::truncate_subgraph(sg, budget);
assert!(
truncated.entities.len() < full_count,
"tight budget should drop some entities"
);
assert_eq!(truncated.entry.id, page_id, "entry must always be kept");
}
#[test]
fn test_truncate_subgraph_drops_orphan_edges() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/entry.md",
"---\ntitle: Entry\n---\nLinks to [[A]] and [[B]]\n",
Some("proj"),
)
.expect("ingest");
let sg = graph.get_subgraph(page_id, 2).expect("subgraph");
let truncated = atheneum::graph::truncate_subgraph(sg, 1);
for edge in &truncated.edges {
let from_exists = truncated.entities.iter().any(|e| e.id == edge.from_id);
let to_exists = truncated.entities.iter().any(|e| e.id == edge.to_id);
assert!(
from_exists && to_exists,
"orphan edge found: from={} to={}",
edge.from_id,
edge.to_id
);
}
}
#[test]
fn test_hopgraph_query_returns_subgraph() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let page_id = graph
.ingest_wiki_page(
"wiki/router.md",
"---\ntitle: Router\n---\nThe HTTP router builds endpoints\n",
Some("atheneum"),
)
.expect("ingest");
let agent = graph.insert_agent("test", json!({})).expect("agent");
graph
.insert_edge(page_id, agent, EdgeType::Mentions, json!({}))
.expect("edge");
let views = graph
.hopgraph_query(
"router",
3,
2,
&[EdgeType::Wikilink, EdgeType::Explains],
2000,
Some("atheneum"),
)
.expect("hopgraph_query");
assert!(
!views.is_empty(),
"should find at least one result for 'router'"
);
}
#[test]
fn test_hopgraph_query_respects_token_budget() {
let graph = AtheneumGraph::open_in_memory().expect("open");
for i in 0..10 {
let body = format!(
"---\ntitle: Page {}\n---\nContent about topic {} with details\n",
i, i
);
graph
.ingest_wiki_page(&format!("wiki/page{}.md", i), &body, Some("proj"))
.expect("ingest");
}
let views = graph
.hopgraph_query("topic", 5, 2, &[], 50, Some("proj"))
.expect("hopgraph_query");
if !views.is_empty() {
let total_tokens: usize = views
.iter()
.map(|v| {
v.entities
.iter()
.map(atheneum::graph::estimate_entity_tokens)
.sum::<usize>()
})
.sum();
assert!(
total_tokens <= 150,
"token budget of 50 should produce ~{} tokens, got {}",
50,
total_tokens
);
}
}
mod embedder_tests {
use atheneum::graph::embed::{HashEmbedder, TextEmbedder};
#[test]
fn test_hash_embedder_dimension() {
let embedder = HashEmbedder::new(128);
assert_eq!(embedder.dimension(), 128);
}
#[test]
fn test_hash_embedder_produces_vector() {
let embedder = HashEmbedder::new(128);
let vec = embedder.embed("hello world").expect("embed");
assert_eq!(vec.len(), 128);
}
#[test]
fn test_hash_embedder_normalized() {
let embedder = HashEmbedder::new(128);
let vec = embedder.embed("hello world").expect("embed");
let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(
(norm - 1.0).abs() < 0.01,
"vector should be approximately unit length, norm = {}",
norm
);
}
#[test]
fn test_hash_embedder_similar_texts_close() {
let embedder = HashEmbedder::new(128);
let v1 = embedder.embed("build router function").expect("embed");
let v2 = embedder.embed("build router function").expect("embed");
let v3 = embedder
.embed("completely different topic xyz")
.expect("embed");
let sim_same = cosine_similarity(&v1, &v2);
let sim_diff = cosine_similarity(&v1, &v3);
assert!(
sim_same > 0.99,
"identical text should have similarity ~1.0, got {}",
sim_same
);
assert!(
sim_same > sim_diff,
"identical text should be more similar than different text"
);
}
#[test]
fn test_graph_default_embedder_is_hash() {
let graph = atheneum::graph::AtheneumGraph::open_in_memory().expect("open");
assert_eq!(graph.embedder_dimension(), 128);
}
#[test]
fn test_graph_search_with_hash_embedder() {
let graph = atheneum::graph::AtheneumGraph::open_in_memory().expect("open");
graph
.ingest_wiki_page(
"wiki/router.md",
"---\ntitle: Router\n---\nBuilds HTTP router with axum\n",
Some("proj"),
)
.expect("ingest");
let results = graph
.lexical_search("router", 5, Some("proj"), None, None)
.expect("search");
assert!(!results.is_empty(), "should find router page");
}
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
0.0
} else {
dot / (norm_a * norm_b)
}
}
}
mod consolidation_tests {
use atheneum::graph::{AtheneumGraph, EdgeType, EntityType};
use serde_json::json;
#[test]
fn test_consolidate_discoveries_creates_knowledge_entity() {
let graph = AtheneumGraph::open_in_memory().expect("open");
graph
.store_discovery("claude1", "Symbol", "build_router", json!({"file": "a.rs"}))
.expect("d1");
graph
.store_discovery("claude2", "Symbol", "build_router", json!({"file": "b.rs"}))
.expect("d2");
let knowledge_id = graph
.consolidate_discoveries("build_router", None)
.expect("consolidate")
.expect("should return knowledge id");
let entity = graph.get_entity(knowledge_id).expect("get");
assert_eq!(entity.kind, EntityType::Knowledge.as_str());
assert!(
entity.name.contains("build_router"),
"knowledge name should contain target"
);
}
#[test]
fn test_consolidate_creates_derived_from_edges() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let d1 = graph
.store_discovery("claude1", "Symbol", "build_router", json!({"file": "a.rs"}))
.expect("d1");
let d2 = graph
.store_discovery("claude2", "Symbol", "build_router", json!({"file": "b.rs"}))
.expect("d2");
let knowledge_id = graph
.consolidate_discoveries("build_router", None)
.expect("consolidate")
.expect("should return knowledge id");
let (outgoing, _) = graph.get_neighbors(knowledge_id).expect("neighbors");
let derived_edges: Vec<_> = outgoing
.iter()
.filter(|e| e.edge_type == EdgeType::DerivedFrom.as_str())
.collect();
assert_eq!(
derived_edges.len(),
2,
"should have DerivedFrom edges to 2 source discoveries"
);
let target_ids: Vec<_> = derived_edges.iter().map(|e| e.to_id).collect();
assert!(target_ids.contains(&d1), "should link to discovery 1");
assert!(target_ids.contains(&d2), "should link to discovery 2");
}
#[test]
fn test_consolidate_merges_metadata() {
let graph = AtheneumGraph::open_in_memory().expect("open");
graph
.store_discovery(
"claude1",
"Symbol",
"build_router",
json!({"file": "a.rs", "lines": 100}),
)
.expect("d1");
graph
.store_discovery(
"claude2",
"Symbol",
"build_router",
json!({"file": "b.rs", "lines": 200}),
)
.expect("d2");
let knowledge_id = graph
.consolidate_discoveries("build_router", None)
.expect("consolidate")
.expect("should return knowledge id");
let entity = graph.get_entity(knowledge_id).expect("get");
let agents = entity
.data
.get("source_agents")
.and_then(|v| v.as_array())
.expect("source_agents");
assert_eq!(agents.len(), 2, "should list both agents");
let discovery_count = entity
.data
.get("discovery_count")
.and_then(|v| v.as_i64())
.expect("discovery_count");
assert_eq!(discovery_count, 2);
}
#[test]
fn test_consolidate_idempotent() {
let graph = AtheneumGraph::open_in_memory().expect("open");
graph
.store_discovery("claude1", "Symbol", "build_router", json!({}))
.expect("d1");
graph
.store_discovery("claude2", "Symbol", "build_router", json!({}))
.expect("d2");
let k1 = graph
.consolidate_discoveries("build_router", None)
.expect("c1")
.expect("id1");
let k2 = graph
.consolidate_discoveries("build_router", None)
.expect("c2")
.expect("id2");
assert_eq!(
k1, k2,
"re-consolidating should return same knowledge entity"
);
}
#[test]
fn test_consolidate_single_discovery_still_creates_knowledge() {
let graph = AtheneumGraph::open_in_memory().expect("open");
graph
.store_discovery(
"claude1",
"Symbol",
"unique_target",
json!({"note": "only one"}),
)
.expect("d1");
let knowledge_id = graph
.consolidate_discoveries("unique_target", None)
.expect("consolidate")
.expect("should return knowledge id");
let entity = graph.get_entity(knowledge_id).expect("get");
assert_eq!(entity.kind, EntityType::Knowledge.as_str());
}
#[test]
fn test_consolidate_no_discoveries_returns_none() {
let graph = AtheneumGraph::open_in_memory().expect("open");
let result = graph
.consolidate_discoveries("nonexistent_target", None)
.expect("ok");
assert!(result.is_none(), "no discoveries should return None");
}
#[test]
fn test_consolidation_pass() {
let graph = AtheneumGraph::open_in_memory().expect("open");
graph
.store_discovery("claude1", "Symbol", "target_a", json!({}))
.expect("d1");
graph
.store_discovery("claude2", "Symbol", "target_a", json!({}))
.expect("d2");
graph
.store_discovery("claude1", "Symbol", "target_b", json!({}))
.expect("d3");
let report = graph.consolidation_pass(None).expect("pass");
assert!(report.len() >= 2, "should consolidate at least 2 targets");
for (target, knowledge_id) in &report {
let entity = graph.get_entity(*knowledge_id).expect("get");
assert_eq!(entity.kind, EntityType::Knowledge.as_str());
assert!(entity.data.get("target").and_then(|v| v.as_str()) == Some(target.as_str()));
}
}
#[test]
fn test_build_search_index_covers_all_entity_kinds() {
let graph = AtheneumGraph::open_in_memory().expect("open");
graph
.insert_agent("search-test-agent", json!({}))
.expect("agent");
graph
.store_discovery(
"claude",
"Bug",
"target_x",
json!({
"summary": "parameter ordering bug in query_sessions"
}),
)
.expect("discovery");
graph
.ingest_wiki_page("search-test.md", "# Search Test\nSee [[related]].\n", None)
.expect("wiki");
graph
.record_session(atheneum::graph::SessionParams {
session_id: "sess-search-1".into(),
agent_name: "search-test-agent".into(),
project: "search-project".into(),
tool: "opencode".into(),
trigger: "cli".into(),
model: Some("test-model".into()),
git_branch: Some("main".into()),
git_head: None,
parent_session_id: None,
relations: vec![],
})
.expect("session");
graph.build_search_index().expect("build index");
let hits = graph
.lexical_search("parameter ordering", 10, None, None, None)
.expect("search");
assert!(!hits.is_empty(), "should find discovery via lexical search");
let session_hits = graph
.lexical_search("search-project session", 10, None, None, None)
.expect("session search");
assert!(
!session_hits.is_empty(),
"build_search_index should index Session entities, not just Discovery/WikiPage"
);
}
#[test]
fn test_navigate_finds_session_entities() {
let graph = AtheneumGraph::open_in_memory().expect("open");
graph
.insert_agent("nav-test-agent", json!({}))
.expect("agent");
graph
.record_session(atheneum::graph::SessionParams {
session_id: "sess-nav-1".into(),
agent_name: "nav-test-agent".into(),
project: "nav-project".into(),
tool: "opencode".into(),
trigger: "cli".into(),
model: Some("test-model".into()),
git_branch: Some("feat/search".into()),
git_head: None,
parent_session_id: None,
relations: vec![],
})
.expect("session");
graph.build_search_index().expect("build index");
let views = graph
.navigate("nav-project session", 5, 2, None, None, None)
.expect("navigate");
assert!(
!views.is_empty(),
"navigate should find session entities via search index"
);
}
#[test]
fn test_hnsw_vectors_persist_across_reopen() {
let db_file = tempfile::NamedTempFile::new().expect("tempfile");
let db_path = db_file.path().to_path_buf();
{
let graph = AtheneumGraph::open(&db_path).expect("open first time");
graph
.store_discovery(
"claude",
"Bug",
"persistent_target",
json!({"summary": "vectors must persist across reopen"}),
)
.expect("discovery");
let hits = graph
.lexical_search("persistent vectors", 5, None, None, None)
.expect("search first session");
assert!(!hits.is_empty(), "should find discovery in first session");
}
{
let graph = AtheneumGraph::open(&db_path).expect("reopen");
let hits = graph
.lexical_search("persistent vectors", 5, None, None, None)
.expect("search second session");
assert!(
!hits.is_empty(),
"HNSW vectors must persist across database reopen"
);
}
}
}