2026-10-12 17:44:56 +02:00
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 (())
}
2026-10-12 23:54:26 +02:00
#[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 (())
}