use sqry_core::config::{CacheConfig, IndexingConfig};
use sqry_core::project::Project;
use sqry_core::project::persistence::{
ProjectPersistence, build_persisted_state, compute_config_fingerprint,
};
use sqry_core::project::types::ProjectId;
use std::collections::HashMap;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_persist_and_reload_happy_path() {
let tmp = TempDir::new().unwrap();
let index_root = tmp.path().to_path_buf();
fs::create_dir_all(index_root.join(".git")).unwrap();
let project1 = Project::new(index_root.clone()).unwrap();
project1.initialize().unwrap();
assert!(project1.repo_count() >= 1, "Repo should be detected");
let repo_count = project1.repo_count();
project1.persist_if_configured();
let state_path = index_root
.join(".sqry-cache")
.join("project-state")
.join(format!("proj_{:016x}.json", project1.id.as_u64()));
assert!(
state_path.exists(),
"Persisted state file should exist at {state_path:?}"
);
drop(project1);
let project2 = Project::new(index_root.clone()).unwrap();
project2.initialize().unwrap();
assert_eq!(
project2.repo_count(),
repo_count,
"Repo count should match after preload"
);
}
#[test]
fn test_opt_out_no_files_created() {
let tmp = TempDir::new().unwrap();
let index_root = tmp.path().to_path_buf();
let _persistence = ProjectPersistence::new(&index_root, ".sqry-cache");
let _project_id = ProjectId::from_index_root(&index_root);
let cache = CacheConfig {
directory: ".sqry-cache".to_string(),
persistent: false,
};
assert!(!cache.persistent);
let state_dir = index_root.join(".sqry-cache").join("project-state");
assert!(
!state_dir.exists(),
"State directory should not exist before any persist"
);
}
#[test]
#[cfg(unix)]
fn test_read_only_path_warns_no_panic() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let index_root = tmp.path().to_path_buf();
let cache_dir = index_root.join(".sqry-cache");
fs::create_dir_all(&cache_dir).unwrap();
let mut perms = fs::metadata(&cache_dir).unwrap().permissions();
perms.set_mode(0o500); fs::set_permissions(&cache_dir, perms).unwrap();
let persistence = ProjectPersistence::new(&index_root, ".sqry-cache");
let project_id = ProjectId::from_index_root(&index_root);
let state = build_persisted_state(
project_id,
&index_root,
12345,
&HashMap::new(),
&HashMap::new(),
);
let result = persistence.write_metadata(&state);
assert!(result.is_err(), "Write should fail on read-only directory");
let mut perms = fs::metadata(&cache_dir).unwrap().permissions();
perms.set_mode(0o700);
fs::set_permissions(&cache_dir, perms).unwrap();
}
#[test]
fn test_checksum_mismatch_falls_back() {
let tmp = TempDir::new().unwrap();
let index_root = tmp.path().to_path_buf();
let persistence = ProjectPersistence::new(&index_root, ".sqry-cache");
let project_id = ProjectId::from_index_root(&index_root);
let state = build_persisted_state(
project_id,
&index_root,
12345,
&HashMap::new(),
&HashMap::new(),
);
persistence.write_metadata(&state).unwrap();
let state_path = index_root
.join(".sqry-cache")
.join("project-state")
.join(format!("proj_{:016x}.json", project_id.as_u64()));
let mut contents = fs::read_to_string(&state_path).unwrap();
contents = contents.replace("\"checksum\":", "\"checksum\": \"corrupted");
fs::write(&state_path, contents).unwrap();
let result = persistence.read_metadata(project_id);
assert!(
result.is_err() || result.unwrap().is_none(),
"Corrupted state should fail to load"
);
}
#[test]
fn test_concurrent_persist_separate_roots() {
use std::thread;
let handles: Vec<_> = (0..4)
.map(|i| {
thread::spawn(move || {
let tmp = TempDir::new().unwrap();
let index_root = tmp.path().to_path_buf();
fs::create_dir_all(index_root.join(".git")).unwrap();
let project = Project::new(index_root.clone()).unwrap();
project.initialize().unwrap();
project.persist_if_configured();
let state_path = index_root.join(".sqry-cache").join("project-state");
assert!(
state_path.exists(),
"Thread {i}: State directory should exist"
);
})
})
.collect();
for (i, handle) in handles.into_iter().enumerate() {
handle
.join()
.unwrap_or_else(|_| panic!("Thread {i} panicked"));
}
}
#[test]
fn test_custom_cache_directory_relative() {
let tmp = TempDir::new().unwrap();
let index_root = tmp.path().to_path_buf();
let persistence = ProjectPersistence::new(&index_root, "custom-cache");
let project_id = ProjectId::from_index_root(&index_root);
let state = build_persisted_state(
project_id,
&index_root,
12345,
&HashMap::new(),
&HashMap::new(),
);
persistence.write_metadata(&state).unwrap();
let state_path = index_root
.join("custom-cache")
.join("project-state")
.join(format!("proj_{:016x}.json", project_id.as_u64()));
assert!(
state_path.exists(),
"State should be in custom cache directory"
);
}
#[test]
fn test_absolute_cache_directory_rejected() {
let tmp = TempDir::new().unwrap();
let index_root = tmp.path().to_path_buf();
let cache_tmp = TempDir::new().unwrap();
let cache_dir = cache_tmp.path().to_string_lossy().to_string();
let persistence = ProjectPersistence::new(&index_root, &cache_dir);
let project_id = ProjectId::from_index_root(&index_root);
let state = build_persisted_state(
project_id,
&index_root,
12345,
&HashMap::new(),
&HashMap::new(),
);
persistence.write_metadata(&state).unwrap();
let escaped_path = cache_tmp
.path()
.join("project-state")
.join(format!("proj_{:016x}.json", project_id.as_u64()));
assert!(
!escaped_path.exists(),
"Absolute path should be rejected; state should NOT escape project"
);
let default_path = index_root
.join(".sqry-cache")
.join("project-state")
.join(format!("proj_{:016x}.json", project_id.as_u64()));
assert!(
default_path.exists(),
"State should be in default cache directory when absolute path rejected"
);
}
#[test]
fn test_config_fingerprint_invalidates_on_change() {
let cache1 = CacheConfig::default();
let indexing1 = IndexingConfig::default();
let cache2 = CacheConfig {
directory: ".other-cache".to_string(),
..Default::default()
};
let indexing2 = IndexingConfig::default();
let fp1 = compute_config_fingerprint(&cache1, &indexing1);
let fp2 = compute_config_fingerprint(&cache2, &indexing2);
assert_ne!(fp1, fp2, "Fingerprint should change when config changes");
}
#[test]
fn test_file_table_fidelity_integration() {
use sqry_core::project::types::{FileEntry, StringId};
use std::sync::Arc;
use std::time::SystemTime;
let tmp = TempDir::new().unwrap();
let index_root = tmp.path().to_path_buf();
fs::create_dir_all(index_root.join(".git")).unwrap();
fs::create_dir_all(index_root.join("src")).unwrap();
fs::write(index_root.join("src/main.rs"), "fn main() {}").unwrap();
fs::write(index_root.join("src/lib.rs"), "pub mod foo;").unwrap();
let project = Project::new(index_root.clone()).unwrap();
project.initialize().unwrap();
let repo_index = project.repo_index();
assert!(!repo_index.is_empty(), "Repo should be detected");
let repo_id = *repo_index.values().next().unwrap();
let path1: StringId = Arc::from("src/main.rs");
let path2: StringId = Arc::from("src/lib.rs");
let entry1 = FileEntry::with_metadata(
Arc::clone(&path1),
repo_id,
Some(0xdead_beef_cafe_babe),
Some(SystemTime::now()),
Some(Arc::from("rust")),
);
let entry2 = FileEntry::with_metadata(
Arc::clone(&path2),
repo_id,
Some(0x1234_5678_9abc_def0),
Some(SystemTime::now()),
Some(Arc::from("rust")),
);
project.register_file(entry1);
project.register_file(entry2);
project.persist_if_configured();
drop(project);
let project2 = Project::new(index_root.clone()).unwrap();
project2.initialize().unwrap();
assert!(
project2.file_count() >= 2,
"Should have at least 2 files after preload"
);
if let Some(file) = project2.get_file("src/main.rs") {
assert!(
file.repo_id.is_some(),
"RepoId should be restored (not NONE)"
);
assert_eq!(
file.content_hash,
Some(0xdead_beef_cafe_babe),
"content_hash should be preserved"
);
assert_eq!(
file.language_id.as_deref(),
Some("rust"),
"language_id should be preserved"
);
}
if let Some(file) = project2.get_file("src/lib.rs") {
assert!(
file.repo_id.is_some(),
"RepoId should be restored (not NONE)"
);
assert_eq!(
file.content_hash,
Some(0x1234_5678_9abc_def0),
"content_hash should be preserved"
);
}
}
#[test]
fn test_repo_id_fidelity() {
use sqry_core::project::types::{FileEntry, StringId};
use std::sync::Arc;
let tmp = TempDir::new().unwrap();
let index_root = tmp.path().canonicalize().unwrap();
fs::create_dir_all(index_root.join(".git")).unwrap();
fs::create_dir_all(index_root.join("src")).unwrap();
fs::write(index_root.join("src/main.rs"), "fn main() {}").unwrap();
fs::write(index_root.join("src/lib.rs"), "pub mod foo;").unwrap();
let project = Project::new(index_root.clone()).unwrap();
project.initialize().unwrap();
let repo_index = project.repo_index();
assert!(!repo_index.is_empty(), "Should detect at least 1 repo");
let main_repo_id = *repo_index.get(&index_root).expect("main repo should exist");
let path1: StringId = Arc::from("src/main.rs");
let path2: StringId = Arc::from("src/lib.rs");
let entry1 = FileEntry::new(Arc::clone(&path1), main_repo_id);
let entry2 = FileEntry::new(Arc::clone(&path2), main_repo_id);
project.register_file(entry1);
project.register_file(entry2);
project.persist_if_configured();
drop(project);
let project2 = Project::new(index_root.clone()).unwrap();
project2.initialize().unwrap();
let repo_index2 = project2.repo_index();
assert_eq!(
repo_index.len(),
repo_index2.len(),
"Repo count should match after preload"
);
for (git_root, original_id) in &repo_index {
let restored_id = repo_index2.get(git_root);
assert!(
restored_id.is_some(),
"Repo {git_root:?} should exist after preload"
);
assert_eq!(
restored_id.unwrap(),
original_id,
"RepoId should match for {git_root:?}"
);
}
if let Some(main_file) = project2.get_file("src/main.rs") {
assert!(
main_file.repo_id.is_some(),
"main.rs RepoId should not be NONE"
);
assert_eq!(
main_file.repo_id, main_repo_id,
"main.rs should belong to main repo"
);
}
if let Some(lib_file) = project2.get_file("src/lib.rs") {
assert!(
lib_file.repo_id.is_some(),
"lib.rs RepoId should not be NONE"
);
assert_eq!(
lib_file.repo_id, main_repo_id,
"lib.rs should belong to main repo"
);
}
}
#[test]
fn test_path_traversal_protection() {
let outer_tmp = TempDir::new().unwrap();
let index_root = outer_tmp.path().join("workspace");
fs::create_dir_all(&index_root).unwrap();
fs::create_dir_all(index_root.join(".git")).unwrap();
let project = Project::new(index_root.clone()).unwrap();
project.initialize().unwrap();
project.persist_if_configured();
let state_dir = index_root.join(".sqry-cache").join("project-state");
assert!(
state_dir.exists(),
"State directory should be under index_root"
);
let parent = index_root.parent().unwrap();
let escaped_path = parent.join(".sqry-cache");
assert!(
!escaped_path.exists(),
"State should not escape to parent directory: {escaped_path:?}"
);
}