203 lines
8.3 KiB
Rust
203 lines
8.3 KiB
Rust
use anyhow::Result;
|
|
use searchi_core::SearchIndex;
|
|
use searchi_service::{HybridSearch, SearchOptions};
|
|
use std::fs;
|
|
|
|
#[test]
|
|
#[ignore = "Requires explicitly provisioned Google model and CPU worker"]
|
|
fn real_google_search_persists_and_suppresses_changed_deleted_sources() -> Result<()> {
|
|
assert_eq!(
|
|
std::env::var("SEARCHI_FORCE_CPU").unwrap(),
|
|
"1",
|
|
"VM tests require CPU only"
|
|
);
|
|
let model = std::env::var_os("SEARCHI_GEMMA_MODEL_DIR").expect("Set SEARCHI_GEMMA_MODEL_DIR");
|
|
let worker = std::env::var_os("SEARCHI_GEMMA_WORKER").expect("Set SEARCHI_GEMMA_WORKER");
|
|
let temp = tempfile::tempdir()?;
|
|
let root = temp.path().join("documents");
|
|
fs::create_dir(&root)?;
|
|
let db = temp.path().join("index.db");
|
|
fs::write(
|
|
root.join("agreement.txt"),
|
|
"The supplier partnership may be ended with sixty days written notice.",
|
|
)?;
|
|
fs::write(
|
|
root.join("garden.txt"),
|
|
"Tomatoes need regular watering and lots of sunlight.",
|
|
)?;
|
|
let mut lexical = SearchIndex::open(&db)?;
|
|
lexical.index_folder(&root)?;
|
|
let mut hybrid = HybridSearch::open(&db, &model, &worker, 1)?;
|
|
let report = hybrid.index_embeddings()?;
|
|
assert_eq!(report.passages, 2);
|
|
let hits = hybrid.search(
|
|
"Wie beenden wir die Zusammenarbeit mit dem Lieferanten?",
|
|
10,
|
|
&SearchOptions::default(),
|
|
)?;
|
|
assert_eq!(hits[0].filename, "agreement.txt");
|
|
assert!(hits[0].source.is_some());
|
|
assert!(hits[0].snippet.contains("sixty days"));
|
|
let exact = hybrid.search("garden.txt", 10, &SearchOptions::default())?;
|
|
assert_eq!(exact[0].match_kind, "exact_filename");
|
|
let restricted = hybrid.search(
|
|
"Wie beenden wir die Zusammenarbeit mit dem Lieferanten?",
|
|
10,
|
|
&SearchOptions {
|
|
extension: Some("pdf".into()),
|
|
..Default::default()
|
|
},
|
|
)?;
|
|
assert!(restricted.is_empty());
|
|
drop(hybrid);
|
|
let mut hybrid = HybridSearch::open(&db, &model, &worker, 1)?;
|
|
assert_eq!(
|
|
hybrid.search(
|
|
"How can we stop working with the vendor?",
|
|
10,
|
|
&SearchOptions::default()
|
|
)?[0]
|
|
.filename,
|
|
"agreement.txt"
|
|
);
|
|
fs::write(root.join("agreement.txt"),"Entirely unrelated replacement content about repainting a kitchen and changing cupboard handles.")?;
|
|
lexical.index_folder(&root)?;
|
|
assert!(hybrid
|
|
.search_semantic(
|
|
"How can we stop working with the vendor?",
|
|
10,
|
|
&SearchOptions::default()
|
|
)?
|
|
.iter()
|
|
.all(|hit| hit.filename != "agreement.txt"));
|
|
fs::remove_file(root.join("garden.txt"))?;
|
|
lexical.index_folder(&root)?;
|
|
assert!(hybrid
|
|
.search_semantic("How do we water vegetables?", 10, &SearchOptions::default())?
|
|
.iter()
|
|
.all(|hit| hit.filename != "garden.txt"));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "Requires explicitly provisioned Google model and CPU worker"]
|
|
fn real_google_failed_policy_rebuild_preserves_live_reader_and_retry() -> Result<()> {
|
|
use fs2::FileExt;
|
|
use rusqlite::Connection;
|
|
use searchi_service::{FfmpegTools, MediaLimits};
|
|
use std::fs::OpenOptions;
|
|
|
|
assert_eq!(std::env::var("SEARCHI_FORCE_CPU")?, "1");
|
|
let model = std::env::var_os("SEARCHI_GEMMA_MODEL_DIR").expect("Set SEARCHI_GEMMA_MODEL_DIR");
|
|
let worker = std::env::var_os("SEARCHI_GEMMA_WORKER").expect("Set SEARCHI_GEMMA_WORKER");
|
|
let temp = tempfile::tempdir()?;
|
|
let root = temp.path().join("documents");
|
|
fs::create_dir(&root)?;
|
|
let db = temp.path().join("index.db");
|
|
fs::write(
|
|
root.join("agreement.txt"),
|
|
"The supplier partnership may be ended with sixty days written notice.",
|
|
)?;
|
|
let mut lexical = SearchIndex::open(&db)?;
|
|
lexical.index_folder(&root)?;
|
|
let mut service = HybridSearch::open(&db, model, worker, 1)?;
|
|
service.index_embeddings()?;
|
|
let connection = Connection::open(&db)?;
|
|
let active = |connection: &Connection| -> Result<(i64, String)> {
|
|
Ok(connection.query_row(
|
|
"SELECT g.id,g.fingerprint FROM semantic_generations g JOIN semantic_meta m ON m.active_generation=g.id",
|
|
[],
|
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
)?)
|
|
};
|
|
let assert_reader = |service: &mut HybridSearch| -> Result<()> {
|
|
let hits = service.search_semantic(
|
|
"How can we stop working with the vendor?",
|
|
10,
|
|
&SearchOptions::default(),
|
|
)?;
|
|
assert_eq!(hits.len(), 1);
|
|
assert_eq!(hits[0].filename, "agreement.txt");
|
|
assert!(hits[0].snippet.contains("sixty days"));
|
|
Ok(())
|
|
};
|
|
assert_reader(&mut service)?;
|
|
let original = active(&connection)?;
|
|
// No media files are present: exercise the production media-policy rebuild
|
|
// with genuine document inference, without invoking any decoder fixture.
|
|
let tools = FfmpegTools {
|
|
ffmpeg: std::env::current_exe()?,
|
|
ffprobe: std::env::current_exe()?,
|
|
};
|
|
let limits = MediaLimits::default();
|
|
let lock_path = format!("{}.semantic.lock", db.canonicalize()?.to_string_lossy());
|
|
let lock = OpenOptions::new()
|
|
.read(true)
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(false)
|
|
.open(lock_path)?;
|
|
lock.try_lock_exclusive()?;
|
|
let failure = service
|
|
.index_embeddings_with_media(&tools, &limits)
|
|
.unwrap_err();
|
|
assert!(format!("{failure:#}").contains("Another semantic rebuild"));
|
|
FileExt::unlock(&lock)?;
|
|
assert_eq!(active(&connection)?, original);
|
|
assert_reader(&mut service)?;
|
|
|
|
connection.execute_batch("CREATE TRIGGER reject_generation_begin BEFORE INSERT ON semantic_generations BEGIN SELECT RAISE(ABORT,'rejected generation begin'); END;")?;
|
|
assert!(service
|
|
.index_embeddings_with_media(&tools, &limits)
|
|
.is_err());
|
|
assert_eq!(active(&connection)?, original);
|
|
assert_reader(&mut service)?;
|
|
connection.execute_batch("DROP TRIGGER reject_generation_begin;
|
|
CREATE TRIGGER reject_generation_activation BEFORE UPDATE OF active_generation ON semantic_meta BEGIN SELECT RAISE(ABORT,'rejected generation activation'); END;")?;
|
|
let failure = service
|
|
.index_embeddings_with_media(&tools, &limits)
|
|
.unwrap_err();
|
|
assert!(format!("{failure:#}").contains("rejected generation activation"));
|
|
assert_eq!(active(&connection)?, original);
|
|
assert_reader(&mut service)?;
|
|
let generations: i64 =
|
|
connection.query_row("SELECT count(*) FROM semantic_generations", [], |row| {
|
|
row.get(0)
|
|
})?;
|
|
assert_eq!(generations, 1, "Failed activation must discard staging");
|
|
|
|
connection.execute_batch("CREATE TRIGGER reject_generation_cleanup BEFORE DELETE ON semantic_generations WHEN old.id != (SELECT active_generation FROM semantic_meta WHERE singleton=1) BEGIN SELECT RAISE(ABORT,'rejected generation cleanup'); END;")?;
|
|
let failure = service
|
|
.index_embeddings_with_media(&tools, &limits)
|
|
.unwrap_err();
|
|
let message = format!("{failure:#}");
|
|
assert!(message.contains("rejected generation activation"));
|
|
assert!(message.contains("rejected generation cleanup"));
|
|
assert_eq!(active(&connection)?, original);
|
|
assert_reader(&mut service)?;
|
|
connection.execute_batch(
|
|
"DROP TRIGGER reject_generation_activation; DROP TRIGGER reject_generation_cleanup;",
|
|
)?;
|
|
let retry = service.index_embeddings_with_media(&tools, &limits)?;
|
|
assert_eq!(retry.passages, 1);
|
|
assert_reader(&mut service)?;
|
|
let media_active = active(&connection)?;
|
|
assert_ne!(media_active.1, original.1);
|
|
|
|
// Exercise the reverse media-to-text policy change through the same helper.
|
|
lock.try_lock_exclusive()?;
|
|
assert!(service.index_embeddings().is_err());
|
|
FileExt::unlock(&lock)?;
|
|
assert_eq!(active(&connection)?, media_active);
|
|
assert_reader(&mut service)?;
|
|
connection.execute_batch("CREATE TRIGGER reject_generation_activation BEFORE UPDATE OF active_generation ON semantic_meta BEGIN SELECT RAISE(ABORT,'rejected generation activation'); END;")?;
|
|
assert!(service.index_embeddings().is_err());
|
|
assert_eq!(active(&connection)?, media_active);
|
|
assert_reader(&mut service)?;
|
|
connection.execute_batch("DROP TRIGGER reject_generation_activation;")?;
|
|
service.index_embeddings()?;
|
|
assert_eq!(active(&connection)?.1, original.1);
|
|
assert_reader(&mut service)?;
|
|
Ok(())
|
|
}
|