use super::*;
#[test]
fn sanitize_strips_unsafe_chars() {
assert_eq!(sanitize_id("good-name_1.0"), "good-name_1.0");
assert_eq!(sanitize_id("../escape"), ".._escape");
assert_eq!(sanitize_id("with spaces/slash"), "with_spaces_slash");
}
#[test]
fn registry_file_serde_roundtrip() {
let file = IndexRegistryFile {
indexes: vec![
PersistedIndex {
id: "a".into(),
root_path: PathBuf::from("/tmp/a"),
..Default::default()
},
PersistedIndex {
id: "b".into(),
root_path: PathBuf::from("/tmp/b"),
..Default::default()
},
],
};
let s = toml::to_string_pretty(&file).unwrap();
let parsed: IndexRegistryFile = toml::from_str(&s).unwrap();
assert_eq!(parsed.indexes, file.indexes);
}
#[test]
fn remove_index_persists_to_toml() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
upsert_index_registry_entry_at(
&path,
PersistedIndex {
id: "keep".into(),
root_path: PathBuf::from("/tmp/keep"),
..Default::default()
},
)
.unwrap();
upsert_index_registry_entry_at(
&path,
PersistedIndex {
id: "drop".into(),
root_path: PathBuf::from("/tmp/drop"),
..Default::default()
},
)
.unwrap();
assert_eq!(load_index_registry_at(&path).unwrap().len(), 2);
remove_index_registry_entry_at(&path, "drop").unwrap();
let restored = load_index_registry_at(&path).unwrap();
assert_eq!(restored.len(), 1);
assert_eq!(restored[0].id, "keep");
assert!(restored.iter().all(|e| e.id != "drop"));
remove_index_registry_entry_at(&path, "drop").unwrap();
assert_eq!(load_index_registry_at(&path).unwrap().len(), 1);
}
#[test]
fn upsert_index_dedupes_on_id() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
upsert_index_registry_entry_at(
&path,
PersistedIndex {
id: "proj".into(),
root_path: PathBuf::from("/old"),
..Default::default()
},
)
.unwrap();
upsert_index_registry_entry_at(
&path,
PersistedIndex {
id: "proj".into(),
root_path: PathBuf::from("/new"),
..Default::default()
},
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1, "duplicate [[index]] block written");
assert_eq!(entries[0].root_path, PathBuf::from("/new"));
}
#[test]
fn respect_gitignore_defaults_true_and_round_trips() {
assert!(PersistedIndex::default().respect_gitignore);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
std::fs::write(
&path,
r#"
[[index]]
id = "legacy"
root_path = "/tmp/legacy"
"#,
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(
entries[0].respect_gitignore,
"missing field must default to true (issue #100 back-compat)"
);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
save_index_registry_at(
&path,
&[PersistedIndex {
id: "vendored".into(),
root_path: PathBuf::from("/tmp/v"),
respect_gitignore: false,
..Default::default()
}],
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(!entries[0].respect_gitignore);
}
#[test]
fn include_docs_defaults_true_and_round_trips() {
assert!(PersistedIndex::default().include_docs);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
std::fs::write(
&path,
r#"
[[index]]
id = "legacy"
root_path = "/tmp/legacy"
"#,
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(
entries[0].include_docs,
"missing field must default to true (issue #118 migration)"
);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
save_index_registry_at(
&path,
&[PersistedIndex {
id: "docs_off".into(),
root_path: PathBuf::from("/tmp/v"),
include_docs: false,
..Default::default()
}],
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(!entries[0].include_docs);
}
#[test]
fn lexical_only_round_trips() {
assert!(!PersistedIndex::default().lexical_only);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
std::fs::write(
&path,
r#"
[[index]]
id = "legacy"
root_path = "/tmp/legacy"
"#,
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(
!entries[0].lexical_only,
"missing field must default to false (issue #109 back-compat)"
);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
save_index_registry_at(
&path,
&[PersistedIndex {
id: "lex_only".into(),
root_path: PathBuf::from("/tmp/v"),
lexical_only: true,
..Default::default()
}],
)
.unwrap();
let s = std::fs::read_to_string(&path).unwrap();
assert!(
s.contains("lexical_only"),
"explicit true must be serialised — TOML was: {s}"
);
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].lexical_only);
}
#[test]
fn skip_kg_round_trips() {
assert!(!PersistedIndex::default().skip_kg);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
std::fs::write(
&path,
r#"
[[index]]
id = "legacy"
root_path = "/tmp/legacy"
"#,
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(
!entries[0].skip_kg,
"missing field must default to false (issue #313 back-compat)"
);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
save_index_registry_at(
&path,
&[PersistedIndex {
id: "no_kg".into(),
root_path: PathBuf::from("/tmp/v"),
skip_kg: true,
..Default::default()
}],
)
.unwrap();
let s = std::fs::read_to_string(&path).unwrap();
assert!(
s.contains("skip_kg"),
"explicit true must be serialised — TOML was: {s}"
);
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].skip_kg);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
save_index_registry_at(
&path,
&[PersistedIndex {
id: "both_flags".into(),
root_path: PathBuf::from("/tmp/v"),
lexical_only: true,
skip_kg: true,
..Default::default()
}],
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].lexical_only, "lexical_only preserved");
assert!(entries[0].skip_kg, "skip_kg preserved");
}
#[test]
fn colocated_flag_round_trips() {
assert!(!PersistedIndex::default().colocated);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
std::fs::write(
&path,
r#"
[[index]]
id = "legacy"
root_path = "/tmp/legacy_col"
"#,
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(
!entries[0].colocated,
"missing field must default to false (issue #403 back-compat)"
);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
let root_dir = tempfile::tempdir().unwrap();
save_index_registry_at(
&path,
&[PersistedIndex {
id: "colocated_idx".into(),
root_path: root_dir.path().to_path_buf(),
colocated: true,
..Default::default()
}],
)
.unwrap();
let s = std::fs::read_to_string(&path).unwrap();
assert!(
s.contains("colocated"),
"explicit true must be serialised — TOML was: {s}"
);
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].colocated);
let hnsw = super::hnsw_path_for_entry(&entries[0]).unwrap();
assert!(
hnsw.starts_with(root_dir.path()),
"colocated hnsw path must be inside root; got {hnsw:?}"
);
let redb = super::corpus_redb_path_for_entry(&entries[0]).unwrap();
assert!(
redb.starts_with(root_dir.path()),
"colocated redb path must be inside root; got {redb:?}"
);
}
#[test]
fn data_file_hygiene_round_trips() {
let def = PersistedIndex::default();
assert!(def.extra_skip_dirs.contains(&"data".to_string()));
assert_eq!(def.extra_skip_dirs.len(), 6);
assert_eq!(def.data_file_max_bytes, Some(65_536));
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
std::fs::write(
&path,
r#"
[[index]]
id = "legacy"
root_path = "/tmp/legacy"
"#,
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert!(
entries[0].extra_skip_dirs.contains(&"reports".to_string()),
"missing field must default to the targeted set: {:?}",
entries[0].extra_skip_dirs
);
assert_eq!(
entries[0].data_file_max_bytes,
Some(65_536),
"missing field must default to Some(64 KiB)"
);
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
save_index_registry_at(
&path,
&[PersistedIndex {
id: "custom".into(),
root_path: PathBuf::from("/tmp/v"),
extra_skip_dirs: vec!["archive".to_string()],
data_file_max_bytes: Some(8192),
..Default::default()
}],
)
.unwrap();
let entries = load_index_registry_at(&path).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].extra_skip_dirs, vec!["archive".to_string()]);
assert_eq!(entries[0].data_file_max_bytes, Some(8192));
assert_eq!(resolve_data_file_max_bytes(None), 65_536);
assert_eq!(resolve_data_file_max_bytes(Some(4096)), 4096);
}
#[test]
fn registry_upsert_idempotent_unit() {
let mut entries = vec![PersistedIndex {
id: "a".into(),
root_path: PathBuf::from("/old"),
..Default::default()
}];
let new = PersistedIndex {
id: "a".into(),
root_path: PathBuf::from("/new"),
..Default::default()
};
if let Some(existing) = entries.iter_mut().find(|e| e.id == new.id) {
existing.root_path = new.root_path.clone();
} else {
entries.push(new);
}
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].root_path, PathBuf::from("/new"));
}
#[test]
#[serial_test::serial]
fn data_dir_respects_trusty_data_dir_env_var() {
let tmp = tempfile::tempdir().unwrap();
let unique = tmp.path().join("persistence_data_dir_test");
std::fs::create_dir_all(&unique).unwrap();
unsafe { std::env::set_var("TRUSTY_DATA_DIR", &unique) };
let result = data_dir();
unsafe { std::env::remove_var("TRUSTY_DATA_DIR") };
let dir = result.expect("data_dir with TRUSTY_DATA_DIR must succeed");
assert_eq!(dir, unique, "data_dir() should return the override path");
assert!(
dir.exists(),
"data_dir() should ensure the directory exists"
);
}