use super::store_impl::CorpusStore;
use super::tables::redb_cache_size_bytes;
use super::tables::DEFAULT_REDB_CACHE_MB;
use super::types::PersistedKgNode;
use crate::core::chunker::{ChunkType, RawChunk};
fn raw(id: &str, content: &str) -> RawChunk {
RawChunk {
id: id.to_string(),
file: "src/lib.rs".to_string(),
start_line: 1,
end_line: 1,
content: content.to_string(),
function_name: None,
language: Some("rust".to_string()),
chunk_type: ChunkType::Code,
calls: Vec::new(),
inherits_from: Vec::new(),
chunk_depth: 0,
parent_chunk_id: None,
child_chunk_ids: Vec::new(),
nlp_keywords: Vec::new(),
nlp_code_refs: Vec::new(),
virtual_terms: Vec::new(),
}
}
#[test]
fn redb_cache_size_default_and_env_override() {
let prior = std::env::var("TRUSTY_REDB_CACHE_MB").ok();
unsafe { std::env::remove_var("TRUSTY_REDB_CACHE_MB") };
assert_eq!(redb_cache_size_bytes(), DEFAULT_REDB_CACHE_MB * 1024 * 1024);
unsafe { std::env::set_var("TRUSTY_REDB_CACHE_MB", "1024") };
assert_eq!(redb_cache_size_bytes(), 1024 * 1024 * 1024);
unsafe { std::env::set_var("TRUSTY_REDB_CACHE_MB", "0") };
assert_eq!(redb_cache_size_bytes(), DEFAULT_REDB_CACHE_MB * 1024 * 1024);
unsafe { std::env::set_var("TRUSTY_REDB_CACHE_MB", "not-a-number") };
assert_eq!(redb_cache_size_bytes(), DEFAULT_REDB_CACHE_MB * 1024 * 1024);
unsafe {
match prior {
Some(v) => std::env::set_var("TRUSTY_REDB_CACHE_MB", v),
None => std::env::remove_var("TRUSTY_REDB_CACHE_MB"),
}
}
}
#[test]
fn roundtrip() {
let dir = tempfile::tempdir().unwrap();
let store = CorpusStore::open(&dir.path().join("index.redb")).unwrap();
let chunks = vec![raw("a:1:1", "fn a() {}"), raw("b:1:1", "fn b() {}")];
store.upsert_chunks(&chunks).unwrap();
store
.upsert_entities(&[("src/lib.rs".to_string(), Vec::new())])
.unwrap();
assert_eq!(store.chunk_count().unwrap(), 2);
drop(store);
let store = CorpusStore::open(&dir.path().join("index.redb")).unwrap();
let mut loaded = store.load_all_chunks().unwrap();
loaded.sort_by(|x, y| x.id.cmp(&y.id));
assert_eq!(loaded.len(), 2);
assert_eq!(loaded[0].id, "a:1:1");
assert_eq!(loaded[0].content, "fn a() {}");
let entities = store.load_all_entities().unwrap();
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].0, "src/lib.rs");
}
#[test]
fn batch_upsert_is_atomic_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.redb");
{
let store = CorpusStore::open(&path).unwrap();
store
.upsert_batch(
&[raw("a:1:1", "fn a() {}"), raw("b:1:1", "fn b() {}")],
&[("src/lib.rs".to_string(), Vec::new())],
)
.unwrap();
assert_eq!(store.chunk_count().unwrap(), 2);
}
let store = CorpusStore::open(&path).unwrap();
let mut loaded = store.load_all_chunks().unwrap();
loaded.sort_by(|x, y| x.id.cmp(&y.id));
assert_eq!(loaded.len(), 2);
assert_eq!(loaded[0].id, "a:1:1");
let entities = store.load_all_entities().unwrap();
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].0, "src/lib.rs");
store
.upsert_batch(&[raw("c:1:1", "fn c() {}")], &[])
.unwrap();
assert_eq!(store.chunk_count().unwrap(), 3);
store
.upsert_batch(&[], &[("src/other.rs".to_string(), Vec::new())])
.unwrap();
assert_eq!(store.load_all_entities().unwrap().len(), 2);
store.upsert_batch(&[], &[]).unwrap();
assert_eq!(store.chunk_count().unwrap(), 3);
}
#[test]
fn get_chunks_batch_reads_subset() {
let dir = tempfile::tempdir().unwrap();
let store = CorpusStore::open(&dir.path().join("index.redb")).unwrap();
store
.upsert_chunks(&[
raw("a:1:1", "fn a() {}"),
raw("b:1:1", "fn b() {}"),
raw("c:1:1", "fn c() {}"),
])
.unwrap();
let got = store
.get_chunks(&["c:1:1", "missing:0:0", "a:1:1"])
.unwrap();
assert_eq!(got.len(), 2, "unknown id must be skipped, not error");
assert_eq!(got[0].id, "c:1:1", "input order must be preserved");
assert_eq!(got[0].content, "fn c() {}");
assert_eq!(got[1].id, "a:1:1");
assert!(store.get_chunks(&[]).unwrap().is_empty());
assert!(store.get_chunks(&["nope:0:0"]).unwrap().is_empty());
}
#[test]
fn chunks_after_cursor_seeks_and_pages_in_key_order() {
let dir = tempfile::tempdir().unwrap();
let store = CorpusStore::open(&dir.path().join("index.redb")).unwrap();
store
.upsert_chunks(&[
raw("c:1:1", "fn c() {}"),
raw("a:1:1", "fn a() {}"),
raw("e:1:1", "fn e() {}"),
raw("b:1:1", "fn b() {}"),
raw("d:1:1", "fn d() {}"),
])
.unwrap();
let page1 = store.chunks_after(None, 2).unwrap();
assert_eq!(page1.len(), 2);
assert_eq!(page1[0].id, "a:1:1");
assert_eq!(page1[1].id, "b:1:1");
let page2 = store.chunks_after(Some("b:1:1"), 2).unwrap();
assert_eq!(page2.len(), 2);
assert_eq!(page2[0].id, "c:1:1");
assert_eq!(page2[1].id, "d:1:1");
let page3 = store.chunks_after(Some("d:1:1"), 2).unwrap();
assert_eq!(page3.len(), 1);
assert_eq!(page3[0].id, "e:1:1");
assert!(store.chunks_after(Some("e:1:1"), 2).unwrap().is_empty());
let after_gap = store.chunks_after(Some("a:9:9"), 10).unwrap();
assert_eq!(after_gap[0].id, "b:1:1", "seek lands on next-greater id");
assert!(store.chunks_after(None, 0).unwrap().is_empty());
}
#[test]
fn missing_db_is_empty() {
let dir = tempfile::tempdir().unwrap();
let store = CorpusStore::open(&dir.path().join("fresh.redb")).unwrap();
assert_eq!(store.chunk_count().unwrap(), 0);
assert!(store.load_all_chunks().unwrap().is_empty());
assert!(store.load_all_entities().unwrap().is_empty());
}
#[test]
fn test_meta_indexed_root_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let store = CorpusStore::open(&dir.path().join("index.redb")).unwrap();
assert_eq!(store.read_indexed_root_sync().unwrap(), None);
let root = std::path::PathBuf::from("/Users/me/code/project");
store.write_indexed_root_sync(&root).unwrap();
assert_eq!(store.read_indexed_root_sync().unwrap(), Some(root.clone()));
let moved = std::path::PathBuf::from("/mnt/serving/project");
store.write_indexed_root_sync(&moved).unwrap();
assert_eq!(store.read_indexed_root_sync().unwrap(), Some(moved));
}
#[test]
fn delete_removes_chunk() {
let dir = tempfile::tempdir().unwrap();
let store = CorpusStore::open(&dir.path().join("index.redb")).unwrap();
store
.upsert_chunks(&[raw("a:1:1", "x"), raw("b:1:1", "y")])
.unwrap();
store.delete_chunks(&["a:1:1".to_string()]).unwrap();
assert_eq!(store.chunk_count().unwrap(), 1);
let loaded = store.load_all_chunks().unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].id, "b:1:1");
store.delete_chunks(&["nope:0:0".to_string()]).unwrap();
assert_eq!(store.chunk_count().unwrap(), 1);
}
#[test]
fn empty_batches_are_noops() {
let dir = tempfile::tempdir().unwrap();
let store = CorpusStore::open(&dir.path().join("index.redb")).unwrap();
store.upsert_chunks(&[]).unwrap();
store.upsert_entities(&[]).unwrap();
store.delete_chunks(&[]).unwrap();
assert_eq!(store.chunk_count().unwrap(), 0);
}
#[test]
fn delete_entities_removes_file_row() {
let dir = tempfile::tempdir().unwrap();
let store = CorpusStore::open(&dir.path().join("index.redb")).unwrap();
store
.upsert_entities(&[
("src/a.rs".to_string(), Vec::new()),
("src/b.rs".to_string(), Vec::new()),
])
.unwrap();
assert_eq!(store.load_all_entities().unwrap().len(), 2);
store.delete_entities("src/a.rs").unwrap();
let remaining = store.load_all_entities().unwrap();
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].0, "src/b.rs");
store.delete_entities("src/never.rs").unwrap();
assert_eq!(store.load_all_entities().unwrap().len(), 1);
}
#[test]
fn path_accessor_returns_open_path() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("index.redb");
let store = CorpusStore::open(&p).unwrap();
assert_eq!(store.path(), p.as_path());
}
#[test]
fn hash_cache_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.redb");
{
let store = CorpusStore::open(&path).unwrap();
assert!(store.load_file_hashes().unwrap().is_empty());
store
.upsert_file_hashes(&[("src/a.rs", "aabbcc"), ("src/b.rs", "ddeeff")])
.unwrap();
let mut loaded = store.load_file_hashes().unwrap();
loaded.sort_by(|x, y| x.0.cmp(&y.0));
assert_eq!(loaded.len(), 2);
assert_eq!(loaded[0], ("src/a.rs".to_string(), "aabbcc".to_string()));
assert_eq!(loaded[1], ("src/b.rs".to_string(), "ddeeff".to_string()));
store.upsert_file_hashes(&[("src/a.rs", "aabbcc")]).unwrap();
assert_eq!(store.load_file_hashes().unwrap().len(), 2);
store.upsert_file_hashes(&[("src/a.rs", "112233")]).unwrap();
let mut loaded2 = store.load_file_hashes().unwrap();
loaded2.sort_by(|x, y| x.0.cmp(&y.0));
assert_eq!(loaded2[0].1, "112233");
}
let store = CorpusStore::open(&path).unwrap();
let mut loaded = store.load_file_hashes().unwrap();
loaded.sort_by(|x, y| x.0.cmp(&y.0));
assert_eq!(loaded.len(), 2);
assert_eq!(loaded[0].0, "src/a.rs");
assert_eq!(loaded[0].1, "112233");
}
#[test]
fn hash_cache_clear() {
let dir = tempfile::tempdir().unwrap();
let store = CorpusStore::open(&dir.path().join("index.redb")).unwrap();
store
.upsert_file_hashes(&[("src/a.rs", "aa"), ("src/b.rs", "bb")])
.unwrap();
assert_eq!(store.load_file_hashes().unwrap().len(), 2);
store.clear_file_hashes().unwrap();
assert!(store.load_file_hashes().unwrap().is_empty());
store.clear_file_hashes().unwrap();
store.upsert_file_hashes(&[]).unwrap();
assert!(store.load_file_hashes().unwrap().is_empty());
}
#[test]
fn open_fresh_truncates_stale_staging_file() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("index.redb.tmp");
{
let store = CorpusStore::open(&p).unwrap();
store.upsert_chunks(&[raw("stale:1:1", "old")]).unwrap();
assert_eq!(store.chunk_count().unwrap(), 1);
}
assert!(p.exists());
let fresh = CorpusStore::open_fresh(&p).unwrap();
assert_eq!(fresh.chunk_count().unwrap(), 0);
assert_eq!(fresh.path(), p.as_path());
let fresh2 = CorpusStore::open_fresh(&dir.path().join("never.redb.tmp")).unwrap();
assert_eq!(fresh2.chunk_count().unwrap(), 0);
}
#[test]
fn save_load_kg_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.redb");
let nodes = vec![
(
"alpha".to_string(),
PersistedKgNode {
chunk_id: "a:1:1".into(),
file: "a.rs".into(),
},
),
(
"beta".to_string(),
PersistedKgNode {
chunk_id: "b:1:1".into(),
file: "b.rs".into(),
},
),
];
let adj_fwd = vec![(
"alpha".to_string(),
vec![("CallsFunction".to_string(), "beta".to_string())],
)];
let adj_rev = vec![(
"beta".to_string(),
vec![("CallsFunction".to_string(), "alpha".to_string())],
)];
{
let store = CorpusStore::open(&path).unwrap();
store
.save_kg_graph(&nodes, &adj_fwd, &adj_rev)
.expect("save kg");
assert_eq!(store.kg_node_count().unwrap(), 2);
}
let store = CorpusStore::open(&path).unwrap();
let (loaded_nodes, loaded_fwd, loaded_rev) = store.load_kg_graph().unwrap();
assert_eq!(loaded_nodes.len(), 2);
assert_eq!(loaded_fwd, adj_fwd);
assert_eq!(loaded_rev, adj_rev);
store.save_kg_graph(&[], &[], &[]).unwrap();
assert_eq!(store.kg_node_count().unwrap(), 0);
let (n, f, r) = store.load_kg_graph().unwrap();
assert!(n.is_empty() && f.is_empty() && r.is_empty());
}
#[test]
fn copy_all_from_seeds_staging_corpus() {
let dir = tempfile::tempdir().unwrap();
let src_path = dir.path().join("index.redb");
let src = CorpusStore::open(&src_path).unwrap();
src.upsert_chunks(&[
{
let mut c = raw("stable:1:1", "fn stable() {}");
c.file = "stable.rs".to_string();
c
},
{
let mut c = raw("other:1:1", "fn other() {}");
c.file = "other.rs".to_string();
c
},
])
.unwrap();
src.upsert_entities(&[
("stable.rs".to_string(), Vec::new()),
("other.rs".to_string(), Vec::new()),
])
.unwrap();
src.upsert_file_hashes(&[("stable.rs", "aabbcc"), ("other.rs", "ddeeff")])
.unwrap();
let root = dir.path().to_path_buf();
src.write_indexed_root_sync(&root).unwrap();
let staging_path = dir.path().join("index.redb.tmp");
let staging = CorpusStore::open_fresh(&staging_path).unwrap();
assert_eq!(
staging.chunk_count().unwrap(),
0,
"staging must start empty"
);
staging.copy_all_from(&src).unwrap();
assert_eq!(staging.chunk_count().unwrap(), 2);
let mut chunks = staging.load_all_chunks().unwrap();
chunks.sort_by(|a, b| a.id.cmp(&b.id));
assert_eq!(chunks[0].id, "other:1:1");
assert_eq!(chunks[1].id, "stable:1:1");
let entities = staging.load_all_entities().unwrap();
assert_eq!(entities.len(), 2);
let mut hashes = staging.load_file_hashes().unwrap();
hashes.sort_by(|a, b| a.0.cmp(&b.0));
assert_eq!(hashes.len(), 2);
assert_eq!(hashes[0], ("other.rs".to_string(), "ddeeff".to_string()));
assert_eq!(hashes[1], ("stable.rs".to_string(), "aabbcc".to_string()));
assert_eq!(staging.read_indexed_root_sync().unwrap(), Some(root));
staging.copy_all_from(&src).unwrap();
assert_eq!(staging.chunk_count().unwrap(), 2);
let empty_src_path = dir.path().join("empty.redb");
let empty_src = CorpusStore::open(&empty_src_path).unwrap();
staging.copy_all_from(&empty_src).unwrap();
assert_eq!(
staging.chunk_count().unwrap(),
2,
"copy from empty source must not erase staging rows"
);
}