use anyhow::Result; use searchi_core::{IndexOptions, SearchIndex, SearchOptions}; use std::fs; #[test] fn exclusions_database_and_unchanged_metadata() -> Result<()> { let root = tempfile::tempdir()?; for directory in [".git", "node_modules", "target", "private"] { fs::create_dir(root.path().join(directory))?; fs::write(root.path().join(directory).join("hidden.txt"), "secret")?; } fs::write(root.path().join("visible.txt"), "original content")?; let mut index = SearchIndex::open(root.path().join("index.db"))?; let options = IndexOptions { excluded_directory_names: vec![ ".git".into(), "node_modules".into(), "target".into(), "private".into(), ], }; assert_eq!( index .index_folder_with_options(root.path(), &options)? .indexed, 1 ); assert_eq!(index.stats()?.files, 1); let unchanged = index.index_folder_with_options(root.path(), &options)?; assert_eq!(unchanged.unchanged, 1); assert_eq!(unchanged.indexed, 0); assert!(index.search("secret", 10)?.is_empty()); assert!(index.search("index.db", 10)?.is_empty()); fs::write( root.path().join("visible.txt"), "changed and longer content", )?; assert_eq!( index .index_folder_with_options(root.path(), &options)? .indexed, 1 ); assert!(index.search("original", 10)?.is_empty()); assert_eq!(index.search("changed", 10)?.len(), 1); // A new exclusion removes previously indexed entries from this root. let mut options = IndexOptions::default(); index.index_folder_with_options(root.path(), &options)?; assert_eq!(index.search("secret", 10)?.len(), 1); options.excluded_directory_names.push("private".into()); assert_eq!( index .index_folder_with_options(root.path(), &options)? .removed, 1 ); Ok(()) } #[test] fn filters_apply_before_limit_and_respect_folder_boundaries() -> Result<()> { let root = tempfile::tempdir()?; let docs = root.path().join("docs"); let sibling = root.path().join("docs-more"); fs::create_dir(&docs)?; fs::create_dir(&sibling)?; fs::write(docs.join("shared.md"), "shared short")?; fs::write(docs.join("shared.txt"), "shared considerably longer")?; fs::write(sibling.join("shared.md"), "shared sibling")?; let mut index = SearchIndex::open(":memory:")?; index.index_folder(root.path())?; let options = SearchOptions { folder: Some(docs), extension: Some(".MD".into()), max_size: Some(15), ..Default::default() }; let results = index.search_with_options("shared", 1, &options)?; assert_eq!(results.len(), 1); assert_eq!(results[0].filename, "shared.md"); assert!(!results[0].path.contains("docs-more")); let options = SearchOptions { min_size: Some(20), ..Default::default() }; assert_eq!(index.search_with_options("shared", 10, &options)?.len(), 1); let options = SearchOptions { modified_before_unix_ms: Some(0), ..Default::default() }; assert!(index .search_with_options("shared", 10, &options)? .is_empty()); let options = SearchOptions { min_size: Some(20), max_size: Some(10), ..Default::default() }; assert!(index.search_with_options("shared", 10, &options).is_err()); Ok(()) } #[test] fn migrates_v1_database_without_losing_search_records() -> Result<()> { let root = tempfile::tempdir()?; let db = root.path().join("legacy.db"); let connection = rusqlite::Connection::open(&db)?; connection.execute_batch( "CREATE TABLE files(id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE, root TEXT NOT NULL, filename TEXT NOT NULL, content TEXT NOT NULL); PRAGMA user_version=1;", )?; drop(connection); let mut index = SearchIndex::open(&db)?; fs::write(root.path().join("sample.txt"), "migration evidence")?; index.index_folder(root.path())?; assert_eq!(index.search("evidence", 10)?.len(), 1); Ok(()) } #[test] fn readers_open_and_search_during_uncommitted_writer() -> Result<()> { let root = tempfile::tempdir()?; let db = root.path().join("index.db"); fs::write(root.path().join("notes.txt"), "committed evidence")?; let mut index = SearchIndex::open(&db)?; index.index_folder(root.path())?; let writer = rusqlite::Connection::open(&db)?; writer.execute_batch("BEGIN IMMEDIATE; UPDATE files SET content='uncommitted replacement';")?; // Opening a current schema must not issue DDL or acquire a write lock. let reader = SearchIndex::open(&db)?; assert_eq!(reader.search("evidence", 10)?.len(), 1); assert!(reader.search("replacement", 10)?.is_empty()); writer.execute_batch("ROLLBACK;")?; Ok(()) }