261 lines
9.7 KiB
Rust
261 lines
9.7 KiB
Rust
//! 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\""
|
|
);
|
|
}
|