//! Exact extraction provenance and same-metadata mutation regressions. use anyhow::Result; use searchi_core::SearchIndex; use sha2::{Digest, Sha256}; use std::{fs, fs::FileTimes}; #[test] fn same_stat_overwrite_invalidates_agent_excerpt_and_rescan_reextracts() -> Result<()> { let directory = tempfile::tempdir()?; let path = directory.path().join("source.rs"); let original = "fn old_symbol() { /* first */ }\n"; let changed = "fn new_symbol() { /* other */ }\n"; assert_eq!(original.len(), changed.len()); fs::write(&path, original)?; let mut index = SearchIndex::open(":memory:")?; index.index_folder(directory.path())?; let passage = index.passage_batch(0, 1)?.remove(0); assert_eq!( index.extracted_source_hash(&passage.path)?, Some(format!("{:x}", Sha256::digest(original.as_bytes()))) ); let modified = fs::metadata(&path)?.modified()?; fs::write(&path, changed)?; fs::File::options() .write(true) .open(&path)? .set_times(FileTimes::new().set_modified(modified))?; assert!(index .source_passage(&passage.path, &passage.source, 8192)? .is_none()); let report = index.index_folder(directory.path())?; assert_eq!(report.indexed, 1); assert_eq!(report.unchanged, 0); let fresh = index.passage_batch(0, 1)?.remove(0); assert_eq!(fresh.text, changed); assert_eq!( index.extracted_source_hash(&fresh.path)?, Some(format!("{:x}", Sha256::digest(changed.as_bytes()))) ); assert_eq!( index .source_passage(&fresh.path, &fresh.source, 8192)? .unwrap() .text, changed ); assert_eq!(index.index_folder(directory.path())?.unchanged, 1); Ok(()) } #[test] fn schema_eleven_keeps_lexical_content_but_requires_extraction_provenance() -> Result<()> { let directory = tempfile::tempdir()?; let root = directory.path().join("docs"); fs::create_dir(&root)?; fs::write(root.join("notes.txt"), "preserved migration evidence")?; let db = directory.path().join("index.db"); let passage; { let mut index = SearchIndex::open(&db)?; index.index_folder(&root)?; passage = index.passage_batch(0, 1)?.remove(0); } let connection = rusqlite::Connection::open(&db)?; connection .execute_batch("ALTER TABLE files DROP COLUMN extracted_sha256; PRAGMA user_version=11;")?; drop(connection); let mut index = SearchIndex::open(&db)?; assert_eq!(index.search("migration", 10)?.len(), 1); assert!(index.extracted_source_hash(&passage.path)?.is_none()); assert!(index .source_passage(&passage.path, &passage.source, 8192)? .is_none()); assert_eq!(index.index_folder(&root)?.indexed, 1); assert!(index.extracted_source_hash(&passage.path)?.is_some()); assert_eq!(index.index_folder(&root)?.unchanged, 1); Ok(()) } #[test] fn oversized_text_and_unsupported_files_remain_filename_only() -> Result<()> { let directory = tempfile::tempdir()?; let large = directory.path().join("large.txt"); fs::File::create(&large)?.set_len(2 * 1024 * 1024 + 1)?; fs::File::create(directory.path().join("movie.mp4"))?.set_len(100 * 1024 * 1024)?; let mut index = SearchIndex::open(":memory:")?; assert_eq!(index.index_folder(directory.path())?.indexed, 2); assert!(index.passage_batch(0, 64)?.is_empty()); assert_eq!(index.index_folder(directory.path())?.unchanged, 2); assert_eq!(index.search("large.txt", 10)?.len(), 1); Ok(()) }