130 lines
4.8 KiB
Rust
130 lines
4.8 KiB
Rust
//! 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(())
|
|
}
|
|
|
|
#[test]
|
|
fn pre_epoch_source_remains_searchable_and_verifiable() -> Result<()> {
|
|
let directory = tempfile::tempdir()?;
|
|
let path = directory.path().join("archived-notes.txt");
|
|
let content = "Historical supplier cancellation notice\n";
|
|
fs::write(&path, content)?;
|
|
fs::File::options().write(true).open(&path)?.set_times(
|
|
FileTimes::new().set_modified(std::time::UNIX_EPOCH - std::time::Duration::from_secs(1)),
|
|
)?;
|
|
let mut index = SearchIndex::open(":memory:")?;
|
|
let report = index.index_folder(directory.path())?;
|
|
assert_eq!(report.indexed, 1);
|
|
assert_eq!(report.failures, 0);
|
|
assert_eq!(index.search("archived-notes.txt", 10)?.len(), 1);
|
|
assert_eq!(index.search("cancellation", 10)?.len(), 1);
|
|
let passage = index.passage_batch(0, 1)?.remove(0);
|
|
assert_eq!(
|
|
index.file_signature(&passage.path)?,
|
|
Some((content.len() as u64, -1_000_000_000))
|
|
);
|
|
assert_eq!(
|
|
index.extracted_source_hash(&passage.path)?,
|
|
Some(format!("{:x}", Sha256::digest(content.as_bytes())))
|
|
);
|
|
assert_eq!(
|
|
index
|
|
.source_passage(&passage.path, &passage.source, 8192)?
|
|
.unwrap()
|
|
.text,
|
|
content
|
|
);
|
|
let rescan = index.index_folder(directory.path())?;
|
|
assert_eq!(rescan.unchanged, 1);
|
|
assert_eq!(rescan.indexed, 0);
|
|
Ok(())
|
|
}
|