//! Coverage/publication-control tests use empty ANN graphs, never model vectors. //! Actual semantic retrieval is separately covered by early_text_google_cpu. use super::*; use std::fs; struct RejectAllInference; impl EmbeddingProvider for RejectAllInference { fn fingerprint(&self) -> &str { GEMMA_FINGERPRINT } fn dimensions(&self) -> usize { GEMMA_DIMENSIONS } fn split_windows(&mut self, _: &str, _: &str) -> Result> { anyhow::bail!("Unexpected tokenization") } fn embed_documents(&mut self, _: &[&str], _: &str) -> Result>> { anyhow::bail!("Unexpected document inference") } fn embed_query(&mut self, _: &str) -> Result> { anyhow::bail!("No fabricated query vectors") } } fn fixture() -> Result<(tempfile::TempDir, std::path::PathBuf, SearchIndex, Store)> { let temp = tempfile::tempdir()?; let root = temp.path().join("corpus"); fs::create_dir(&root)?; fs::write( root.join("supplier.txt"), "Supplier cancellation requires written notice.", )?; // Filename/coverage control only, not an image inference fixture. fs::write(root.join("photo.bmp"), b"not an image")?; let db = temp.path().join("index.db"); let mut lexical = SearchIndex::open(&db)?; lexical.index_folder(&root)?; let store = Store::open(&db, GEMMA_DIMENSIONS)?; Ok((temp, db, lexical, store)) } fn empty_generation(store: &mut Store, policy: &str) -> Result { let _lock = store.build_lock()?; let generation = store.begin(policy)?; store.activate(generation)?; Ok(generation) } #[test] fn coverage_status_distinguishes_media_pending_and_survives_mutation() -> Result<()> { let (temp, db, mut lexical, mut store) = fixture()?; let before = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?; assert_eq!(before.pending, 1); assert_eq!(before.coverage, None); assert_eq!(HybridSearch::embedding_status(&db)?.pending, 2); let policy = format!("{}:text-only", generation_fingerprint(GEMMA_FINGERPRINT)); empty_generation(&mut store, &policy)?; let text = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?; assert_eq!(text.coverage, Some(SemanticCoverage::TextOnly)); assert!(!text.stale); assert_eq!( text.pending, 0, "Media must not trigger endless text-only rebuilds" ); assert!(text.text_indexing_allowed); let full = HybridSearch::embedding_status(&db)?; assert!(full.stale); assert_eq!(full.pending, 2); fs::write( temp.path().join("corpus/supplier.txt"), "A revised supplier cancellation policy.", )?; lexical.index_folder(temp.path().join("corpus"))?; let stale = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?; assert!(stale.stale); assert_eq!(stale.pending, 1); fs::remove_file(temp.path().join("corpus/supplier.txt"))?; lexical.index_folder(temp.path().join("corpus"))?; assert_eq!( HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?.pending, 0 ); assert_eq!(HybridSearch::embedding_status(&db)?.pending, 1); Ok(()) } #[test] fn guarded_text_setup_preserves_full_publication_even_after_stale_ui_snapshot() -> Result<()> { let (_temp, db, _lexical, mut publisher) = fixture()?; let mut early = HybridSearch::with_provider(&db, Box::new(RejectAllInference))?; assert!( HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)? .text_indexing_allowed ); let lock = publisher.build_lock()?; // A competing full publisher owns the same lock. Early setup must not // perform source discovery, inference or stage deletion while it is held. assert!(early .index_text_embeddings_preserving_media() .unwrap_err() .to_string() .contains("Another semantic rebuild")); let generation = publisher.begin(&generation_fingerprint(GEMMA_FINGERPRINT))?; publisher.activate(generation)?; drop(lock); assert!(early.index_text_embeddings_preserving_media()?.is_none()); let status = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?; assert_eq!(status.generation, Some(generation)); assert_eq!(status.coverage, Some(SemanticCoverage::FullMedia)); assert!(!status.text_indexing_allowed); assert!(!status.stale); Ok(()) } #[test] fn custom_full_policy_remains_protected_while_default_status_is_stale() -> Result<()> { let (_temp, db, _lexical, mut store) = fixture()?; let policy = format!( "{}:custom-240-2000-120-10000-2000-1800-14400000-17179869184", generation_fingerprint(GEMMA_FINGERPRINT) ); let generation = empty_generation(&mut store, &policy)?; let mut early = HybridSearch::with_provider(&db, Box::new(RejectAllInference))?; assert!(early.index_text_embeddings_preserving_media()?.is_none()); let text = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?; assert_eq!(text.coverage, Some(SemanticCoverage::FullMedia)); assert_eq!(text.generation, Some(generation)); assert!(!text.stale); assert!(!text.text_indexing_allowed); assert!( HybridSearch::embedding_status(&db)?.stale, "Default full status keeps its exact-policy contract" ); Ok(()) } #[test] fn active_policy_transition_rejects_other_models_windows_and_dimensions() -> Result<()> { let (_temp, db, _lexical, mut publisher) = fixture()?; let graphs = PreparedGraphs::default(); publisher.share_prepared_graphs(&graphs)?; let expected = generation_fingerprint(GEMMA_FINGERPRINT); let generation = empty_generation(&mut publisher, &format!("{expected}:text-only"))?; let mut reader = Store::open(&db, GEMMA_DIMENSIONS)?; reader.share_prepared_graphs(&graphs)?; // Empty graph controls policy acceptance and prepared-graph handoff without // inventing vectors or claiming search-quality evidence. let mut trace = TraceCollector::new(MetricsMode::Debug); reader.search_origin_traced( &vec![0.0; GEMMA_DIMENSIONS], &expected, 5, &SearchOptions::default(), None, &mut trace, None, )?; let spans = trace.finish(0).trace.unwrap(); assert!(spans .iter() .any(|s| s.name == "semantic.ann_load" && s.counters.get("cache_hit") == Some(&1))); let connection = rusqlite::Connection::open(&db)?; for policy in [ "different-model", &expected.replace("byte64k-overlap512", "byte64k-overlap1024"), ] { connection.execute( "UPDATE semantic_generations SET fingerprint=?1 WHERE id=?2", rusqlite::params![policy, generation], )?; let mut trace = TraceCollector::new(MetricsMode::Debug); reader.search_origin_traced( &vec![0.0; GEMMA_DIMENSIONS], &expected, 5, &SearchOptions::default(), None, &mut trace, None, )?; assert!(!trace .finish(0) .trace .unwrap() .iter() .any(|s| s.name == "semantic.ann_load")); assert_eq!( HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?.coverage, None ); } connection.execute( "UPDATE semantic_generations SET fingerprint=?1,dimensions=512 WHERE id=?2", rusqlite::params![expected, generation], )?; let mut trace = TraceCollector::new(MetricsMode::Debug); reader.search_origin_traced( &vec![0.0; GEMMA_DIMENSIONS], &expected, 5, &SearchOptions::default(), None, &mut trace, None, )?; assert!(!trace .finish(0) .trace .unwrap() .iter() .any(|s| s.name == "semantic.ann_load")); let incompatible = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?; assert_eq!(incompatible.coverage, None); assert!( incompatible.text_indexing_allowed, "Wrong dimensions must not claim compatible full coverage" ); connection.execute( "UPDATE semantic_generations SET dimensions=768,ann_blob=NULL WHERE id=?1", [generation], )?; let unprepared = HybridSearch::embedding_status_for_coverage(&db, SemanticCoverage::TextOnly)?; assert_eq!(unprepared.coverage, None); assert!( !unprepared.text_indexing_allowed, "Missing ANN must not permit dropping otherwise compatible full media sources" ); Ok(()) } #[test] fn text_task_component_policy_preserves_model_and_window_identity() { let base = generation_fingerprint(GEMMA_FINGERPRINT); assert_eq!(policy_base(&format!("{base}:text-only")), base); let custom = format!("{base}:custom-240-2000-120-10000-2000-1800-14400000-17179869184"); assert_eq!(policy_base(&custom), base); assert!(compatible_query_policy( &format!("{base}:text-only"), &custom )); assert!(compatible_query_policy(&custom, &base)); for suffix in [":text-only-v2", ":custom-unknown", ":custom-1-2-3-4"] { let unknown = format!("{base}{suffix}"); assert_eq!(policy_base(&unknown), unknown); assert!(!compatible_query_policy(&base, &unknown)); } assert!(!compatible_query_policy( &base, &base.replace("byte64k-overlap512", "byte64k-overlap1024") )); assert_eq!( serde_json::to_string(&SemanticCoverage::TextOnly).unwrap(), "\"text_only\"" ); assert_eq!( serde_json::to_string(&SemanticCoverage::FullMedia).unwrap(), "\"full_media\"" ); }