Files
searchi/crates/searchi-service/src/coverage_tests.rs
T

260 lines
9.7 KiB
Rust
Raw Normal View History

//! 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<Vec<TextWindow>> {
anyhow::bail!("Unexpected tokenization")
}
fn embed_documents(&mut self, _: &[&str], _: &str) -> Result<Vec<Vec<f32>>> {
anyhow::bail!("Unexpected document inference")
}
fn embed_query(&mut self, _: &str) -> Result<Vec<f32>> {
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<i64> {
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\""
);
}