Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
Core CI / core (ubuntu-latest) (push) Canceled after 2m23s
243 lines
8.2 KiB
Rust
243 lines
8.2 KiB
Rust
//! Cache-control tests count provider calls; they do not measure model relevance.
|
|
//! Production still loads Google and root runs separate real CPU benchmarks.
|
|
use anyhow::Result;
|
|
use searchi_core::{SearchIndex, SearchOptions};
|
|
use searchi_semantic::TextWindow;
|
|
use searchi_service::{EmbeddingProvider, HybridSearch};
|
|
use std::{
|
|
collections::BTreeMap,
|
|
sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Arc, Mutex,
|
|
},
|
|
};
|
|
|
|
#[derive(Default)]
|
|
struct Calls {
|
|
queries: Mutex<Vec<String>>,
|
|
fail: AtomicBool,
|
|
invalid: AtomicBool,
|
|
new_fingerprint: AtomicBool,
|
|
}
|
|
struct CountingProvider(Arc<Calls>);
|
|
impl EmbeddingProvider for CountingProvider {
|
|
fn fingerprint(&self) -> &str {
|
|
if self.0.new_fingerprint.load(Ordering::Relaxed) {
|
|
"cache-control-v2"
|
|
} else {
|
|
"cache-control-v1"
|
|
}
|
|
}
|
|
fn dimensions(&self) -> usize {
|
|
768
|
|
}
|
|
fn split_windows(&mut self, _: &str, _: &str) -> Result<Vec<TextWindow>> {
|
|
anyhow::bail!("Not used by cache-control tests")
|
|
}
|
|
fn embed_documents(&mut self, _: &[&str], _: &str) -> Result<Vec<Vec<f32>>> {
|
|
anyhow::bail!("Not used by cache-control tests")
|
|
}
|
|
fn embed_query(&mut self, query: &str) -> Result<Vec<f32>> {
|
|
self.0.queries.lock().unwrap().push(query.into());
|
|
if self.0.fail.load(Ordering::Relaxed) {
|
|
anyhow::bail!("Injected provider failure");
|
|
}
|
|
let mut vector = vec![0.0; 768];
|
|
if !self.0.invalid.load(Ordering::Relaxed) {
|
|
vector[0] = 1.0;
|
|
}
|
|
Ok(vector)
|
|
}
|
|
fn embed_code_query(&mut self, query: &str) -> Result<Vec<f32>> {
|
|
self.embed_query(&format!("code-task:{query}"))
|
|
}
|
|
fn query_diagnostics(&self) -> BTreeMap<String, u64> {
|
|
BTreeMap::from([("worker_inference_us".into(), 123)])
|
|
}
|
|
}
|
|
fn fixture() -> Result<(tempfile::TempDir, HybridSearch, Arc<Calls>)> {
|
|
let directory = tempfile::tempdir()?;
|
|
let database = directory.path().join("index.db");
|
|
let calls = Arc::new(Calls::default());
|
|
let service = HybridSearch::with_provider(database, Box::new(CountingProvider(calls.clone())))?;
|
|
Ok((directory, service, calls))
|
|
}
|
|
fn count(calls: &Calls) -> usize {
|
|
calls.queries.lock().unwrap().len()
|
|
}
|
|
|
|
#[test]
|
|
fn query_task_scopes_cache_and_code_debug_never_replays_old_worker_counters() -> Result<()> {
|
|
let (_directory, mut service, calls) = fixture()?;
|
|
let normal = SearchOptions::default();
|
|
let code = SearchOptions {
|
|
code_only: true,
|
|
..Default::default()
|
|
};
|
|
service.search("parse JSON", 5, &normal)?;
|
|
service.search("parse JSON", 5, &code)?;
|
|
assert_eq!(
|
|
*calls.queries.lock().unwrap(),
|
|
vec!["parse JSON", "code-task:parse JSON"]
|
|
);
|
|
let cached = service.search_measured("parse JSON", 5, &code, true)?;
|
|
assert_eq!(count(&calls), 2);
|
|
let embedding = cached
|
|
.metrics
|
|
.trace
|
|
.unwrap()
|
|
.into_iter()
|
|
.find(|s| s.name == "semantic.query_embedding")
|
|
.unwrap();
|
|
assert_eq!(embedding.counters["code_task"], 1);
|
|
assert_eq!(embedding.counters["query_cache_hit"], 1);
|
|
assert!(!embedding.counters.contains_key("worker_inference_us"));
|
|
service.search("parse JSON", 5, &normal)?;
|
|
assert_eq!(count(&calls), 2);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn exact_trimmed_queries_reuse_vectors_but_case_and_model_fingerprint_do_not() -> Result<()> {
|
|
let (_directory, mut service, calls) = fixture()?;
|
|
let options = SearchOptions::default();
|
|
service.search(" supplier ", 5, &options)?;
|
|
service.search("supplier", 5, &options)?;
|
|
assert_eq!(*calls.queries.lock().unwrap(), vec!["supplier"]);
|
|
service.search("Supplier", 5, &options)?;
|
|
assert_eq!(count(&calls), 2);
|
|
calls.new_fingerprint.store(true, Ordering::Relaxed);
|
|
service.search("supplier", 5, &options)?;
|
|
assert_eq!(count(&calls), 3);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn filters_and_changed_index_are_recomputed_even_when_vector_is_reused() -> Result<()> {
|
|
let (directory, mut service, calls) = fixture()?;
|
|
std::fs::write(directory.path().join("first.txt"), "supplier notice")?;
|
|
std::fs::write(directory.path().join("second.md"), "supplier notice")?;
|
|
let mut index = SearchIndex::open(directory.path().join("index.db"))?;
|
|
index.index_folder(directory.path())?;
|
|
let txt = SearchOptions {
|
|
extension: Some("txt".into()),
|
|
..Default::default()
|
|
};
|
|
let md = SearchOptions {
|
|
extension: Some("md".into()),
|
|
..Default::default()
|
|
};
|
|
assert_eq!(
|
|
service.search("supplier", 5, &txt)?[0].filename,
|
|
"first.txt"
|
|
);
|
|
assert_eq!(service.search("supplier", 5, &md)?[0].filename, "second.md");
|
|
assert_eq!(count(&calls), 1);
|
|
std::fs::remove_file(directory.path().join("first.txt"))?;
|
|
index.index_folder(directory.path())?;
|
|
assert!(service.search("supplier", 5, &txt)?.is_empty());
|
|
assert_eq!(count(&calls), 1);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn cache_hit_trace_never_replays_prior_worker_diagnostics() -> Result<()> {
|
|
let (_directory, mut service, _calls) = fixture()?;
|
|
let miss = service.search_measured("supplier", 5, &SearchOptions::default(), true)?;
|
|
let hit = service.search_measured("supplier", 5, &SearchOptions::default(), true)?;
|
|
let miss = miss
|
|
.metrics
|
|
.trace
|
|
.unwrap()
|
|
.into_iter()
|
|
.find(|span| span.name == "semantic.query_embedding")
|
|
.unwrap();
|
|
let hit = hit
|
|
.metrics
|
|
.trace
|
|
.unwrap()
|
|
.into_iter()
|
|
.find(|span| span.name == "semantic.query_embedding")
|
|
.unwrap();
|
|
assert_eq!(miss.counters["query_cache_miss"], 1);
|
|
assert_eq!(miss.counters["worker_inference_us"], 123);
|
|
assert_eq!(hit.counters["query_cache_hit"], 1);
|
|
assert_eq!(hit.counters["query_cache_miss"], 0);
|
|
assert!(!hit.counters.contains_key("worker_inference_us"));
|
|
assert_eq!(hit.counters["failed"], 0);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn failures_and_invalid_vectors_are_not_cached() -> Result<()> {
|
|
let (_directory, mut service, calls) = fixture()?;
|
|
assert!(service
|
|
.search_semantic(&"界".repeat(2000), 5, &SearchOptions::default())
|
|
.is_err());
|
|
assert_eq!(
|
|
count(&calls),
|
|
0,
|
|
"Oversized query must not reach the provider or cache"
|
|
);
|
|
calls.fail.store(true, Ordering::Relaxed);
|
|
for _ in 0..2 {
|
|
assert!(service
|
|
.search("supplier", 5, &SearchOptions::default())
|
|
.is_err());
|
|
}
|
|
calls.fail.store(false, Ordering::Relaxed);
|
|
calls.invalid.store(true, Ordering::Relaxed);
|
|
for _ in 0..2 {
|
|
assert!(service
|
|
.search("supplier", 5, &SearchOptions::default())
|
|
.is_err());
|
|
}
|
|
calls.invalid.store(false, Ordering::Relaxed);
|
|
service.search("supplier", 5, &SearchOptions::default())?;
|
|
service.search("supplier", 5, &SearchOptions::default())?;
|
|
assert_eq!(count(&calls), 5);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn cache_evicts_least_recently_used_query_at_32_entries() -> Result<()> {
|
|
let (_directory, mut service, calls) = fixture()?;
|
|
for number in 0..32 {
|
|
service.search(&format!("query{number}"), 5, &SearchOptions::default())?;
|
|
}
|
|
service.search("query0", 5, &SearchOptions::default())?;
|
|
service.search("query32", 5, &SearchOptions::default())?;
|
|
service.search("query0", 5, &SearchOptions::default())?;
|
|
assert_eq!(count(&calls), 33);
|
|
service.search("query1", 5, &SearchOptions::default())?;
|
|
assert_eq!(count(&calls), 34);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn warmup_bypasses_cache_and_never_seeds_an_unseen_measured_query() -> Result<()> {
|
|
let (_directory, mut service, calls) = fixture()?;
|
|
service.warmup()?;
|
|
assert_eq!(count(&calls), 1);
|
|
let first = service.search_measured(
|
|
"warm up local file search",
|
|
5,
|
|
&SearchOptions::default(),
|
|
true,
|
|
)?;
|
|
assert_eq!(count(&calls), 2);
|
|
let span = first
|
|
.metrics
|
|
.trace
|
|
.unwrap()
|
|
.into_iter()
|
|
.find(|span| span.name == "semantic.query_embedding")
|
|
.unwrap();
|
|
assert_eq!(span.counters["query_cache_miss"], 1);
|
|
service.warmup()?;
|
|
assert_eq!(count(&calls), 3);
|
|
service.search("warm up local file search", 5, &SearchOptions::default())?;
|
|
assert_eq!(count(&calls), 3);
|
|
Ok(())
|
|
}
|