use super::*;
use crate::core::corpus::CorpusStore;
fn seed_v2_fixture(path: &Path, schema_version: u32) {
use crate::core::chunker::{ChunkType, RawChunk};
let raw = |id: &str| RawChunk {
id: id.to_string(),
file: "src/lib.rs".to_string(),
start_line: 1,
end_line: 1,
content: format!("fn {id}() {{}}"),
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(),
};
let db = redb2::Database::create(path).expect("create v2 fixture");
let txn = db.begin_write().expect("v2 write txn");
{
let mut t = txn
.open_table::<&str, &[u8]>(redb2::TableDefinition::new("chunks"))
.unwrap();
let a = serde_json::to_vec(&raw("a")).unwrap();
let b = serde_json::to_vec(&raw("b")).unwrap();
t.insert("a:1:1", a.as_slice()).unwrap();
t.insert("b:2:2", b.as_slice()).unwrap();
}
{
let mut t = txn
.open_table::<&str, &[u8]>(redb2::TableDefinition::new("entities"))
.unwrap();
t.insert("src/lib.rs", b"[]".as_slice()).unwrap();
}
{
let mut t = txn
.open_table::<&str, &[u8]>(redb2::TableDefinition::new("kg_nodes"))
.unwrap();
t.insert("alpha", br#"{"chunk_id":"a:1:1","file":"a.rs"}"#.as_slice())
.unwrap();
}
{
let mut t = txn
.open_table::<&str, &[u8]>(redb2::TableDefinition::new("kg_edges"))
.unwrap();
t.insert("alpha", br#"[["CallsFunction","beta"]]"#.as_slice())
.unwrap();
}
{
let mut t = txn
.open_table::<u64, &[u8]>(redb2::TableDefinition::new("kg_communities"))
.unwrap();
t.insert(7u64, b"community-7".as_slice()).unwrap();
}
{
let mut t = txn
.open_table::<&str, u64>(redb2::TableDefinition::new("kg_symbol_community"))
.unwrap();
t.insert("alpha", 7u64).unwrap();
}
{
let mut t = txn
.open_table::<&str, &[u8]>(redb2::TableDefinition::new("_meta"))
.unwrap();
t.insert("schema_version", schema_version.to_le_bytes().as_slice())
.unwrap();
}
txn.commit().expect("commit v2 fixture");
}
#[test]
fn round_trip_v2_to_v4() {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("index.redb");
seed_v2_fixture(&dest, 4);
let outcome = migrate_redb_corpus(&dest).expect("migration must succeed");
let (total, sv, backup) = match outcome {
MigrationOutcome::Migrated {
total_rows,
schema_version,
backup,
per_table,
} => {
let chunks = per_table.iter().find(|(n, _)| *n == "chunks").unwrap().1;
assert_eq!(chunks, 2, "both chunk rows must be copied");
(total_rows, schema_version, backup)
}
MigrationOutcome::AlreadyV4 => panic!("fixture is 2.x, must migrate"),
};
assert_eq!(total, 8, "all rows across all tables must be copied");
assert_eq!(sv, 4, "schema_version must be preserved");
assert!(backup.exists(), "original 2.x bytes must be backed up");
assert!(
!staging_path(&dest).exists(),
"staging file must not linger after a successful migration"
);
let store = CorpusStore::open(&dest).expect("migrated corpus must open with redb 4.x");
assert_eq!(store.chunk_count().unwrap(), 2, "chunk count preserved");
let chunks = store.load_all_chunks().unwrap();
assert_eq!(chunks.len(), 2);
let (nodes, fwd, _rev) = store.load_kg_graph().unwrap();
assert_eq!(nodes.len(), 1, "kg node preserved");
assert_eq!(fwd.len(), 1, "kg forward edge preserved");
assert_eq!(
store.read_schema_version_sync().unwrap(),
4,
"schema_version readable via CorpusStore after migration"
);
}
#[test]
fn round_trip_from_incompatible_sibling() {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("index.redb");
let sibling = dir.path().join("index.redb.v2-incompatible");
seed_v2_fixture(&sibling, 3);
{
let _empty = CorpusStore::open(&dest).expect("empty recovery corpus");
}
assert!(opens_with_v4(&dest), "precondition: dest is empty 4.x");
let outcome = migrate_redb_corpus(&dest).expect("migration from sibling must succeed");
match outcome {
MigrationOutcome::Migrated { schema_version, .. } => {
assert_eq!(schema_version, 3, "sibling's schema_version preserved");
}
MigrationOutcome::AlreadyV4 => {
panic!("dest is empty but the sibling holds 2.x data — must migrate")
}
}
let store = CorpusStore::open(&dest).expect("migrated corpus opens");
assert_eq!(
store.chunk_count().unwrap(),
2,
"data from the sibling must now live at the canonical path"
);
}
#[test]
fn idempotent_on_v4() {
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("index.redb");
{
let store = CorpusStore::open(&dest).expect("create 4.x corpus");
store
.upsert_chunks(&[])
.expect("no-op upsert to keep store warm");
}
let outcome = migrate_redb_corpus(&dest).expect("no-op on 4.x must succeed");
assert!(
matches!(outcome, MigrationOutcome::AlreadyV4),
"an already-4.x corpus must report AlreadyV4"
);
assert!(
!dest.with_file_name("index.redb.v2-incompatible").exists(),
"no backup should be created for a no-op"
);
assert!(!staging_path(&dest).exists(), "no staging file for a no-op");
}
#[test]
#[ignore = "depends on a machine-specific *.v2-incompatible backup under the data dir"]
fn real_v2_incompatible_smoke() {
let home = match dirs::home_dir() {
Some(h) => h,
None => {
eprintln!("skip: no home dir");
return;
}
};
let base = home.join("Library/Application Support/trusty-search/indexes");
let mut found: Option<PathBuf> = None;
if let Ok(entries) = std::fs::read_dir(&base) {
for e in entries.flatten() {
let candidate = e.path().join("index.redb.v2-incompatible");
if candidate.exists() {
found = Some(candidate);
break;
}
}
}
let real = match found {
Some(p) => p,
None => {
eprintln!(
"skip: no *.v2-incompatible backup found under {} — nothing to smoke-test",
base.display()
);
return;
}
};
eprintln!(
"smoke: migrating a copy of real 2.x corpus {}",
real.display()
);
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("index.redb");
std::fs::copy(&real, &dest).expect("copy real 2.x corpus into tempdir");
let outcome = migrate_redb_corpus(&dest).expect("real 2.x corpus must migrate");
let copied = match outcome {
MigrationOutcome::Migrated {
total_rows,
schema_version,
per_table,
..
} => {
eprintln!(
"smoke: migrated {total_rows} rows, schema_version={schema_version}, \
tables={per_table:?}"
);
total_rows
}
MigrationOutcome::AlreadyV4 => panic!("the real backup is 2.x and must migrate"),
};
let store = CorpusStore::open(&dest).expect("migrated real corpus must open with redb 4.x");
let chunks = store.chunk_count().unwrap() as u64;
eprintln!("smoke: migrated corpus reports {chunks} chunks (copied {copied} total rows)");
let src_chunks = {
let db = redb2::Database::open(&real).unwrap();
let r = db.begin_read().unwrap();
match r.open_table::<&str, &[u8]>(redb2::TableDefinition::new("chunks")) {
Ok(t) => {
use redb2::ReadableTableMetadata as _;
t.len().unwrap()
}
Err(_) => 0,
}
};
assert_eq!(
chunks, src_chunks,
"every chunk row in the real 2.x corpus must survive migration"
);
}