@@ -44,6 +44,7 @@ use url::Url;
4444use walkdir:: WalkDir ;
4545
4646use std:: sync:: { Arc , Mutex } ;
47+ use std:: sync:: OnceLock ;
4748
4849use std:: collections:: HashMap ;
4950
@@ -65,6 +66,17 @@ pub struct FileChange {
6566 pub previous_hash : Option < String > ,
6667}
6768
69+ static WATCH_TEST_NOTIFIER : OnceLock < Mutex < Option < tokio:: sync:: mpsc:: UnboundedSender < PathBuf > > > > =
70+ OnceLock :: new ( ) ;
71+
72+ pub fn set_watch_test_notifier ( sender : tokio:: sync:: mpsc:: UnboundedSender < PathBuf > ) {
73+ let mut guard = WATCH_TEST_NOTIFIER
74+ . get_or_init ( || Mutex :: new ( None ) )
75+ . lock ( )
76+ . unwrap ( ) ;
77+ * guard = Some ( sender) ;
78+ }
79+
6880#[ derive( Clone , Copy , Debug ) ]
6981enum SurrealEmbeddingColumn {
7082 Embedding384 ,
@@ -2979,6 +2991,10 @@ impl ProjectIndexer {
29792991 }
29802992 }
29812993
2994+ pub async fn surreal_storage ( & self ) -> Arc < TokioMutex < SurrealDbStorage > > {
2995+ Arc :: clone ( & self . surreal )
2996+ }
2997+
29822998 #[ cfg( feature = "embeddings" ) ]
29832999 async fn log_surreal_chunk_count ( & self , expected : usize ) {
29843000 let db = {
@@ -3013,6 +3029,26 @@ impl ProjectIndexer {
30133029 }
30143030 }
30153031
3032+ #[ cfg( test) ]
3033+ pub async fn test_fetch_file_metadata (
3034+ & self ,
3035+ file_path : & str ,
3036+ ) -> Result < Option < serde_json:: Value > > {
3037+ let db = {
3038+ let storage = self . surreal . lock ( ) . await ;
3039+ storage. db ( )
3040+ } ;
3041+
3042+ let mut resp = db
3043+ . query (
3044+ "SELECT file_path, last_indexed_at, node_count FROM file_metadata WHERE file_path = $file_path" ,
3045+ )
3046+ . bind ( ( "file_path" , file_path) )
3047+ . await ?;
3048+ let rows: Vec < serde_json:: Value > = resp. take ( 0 ) ?;
3049+ Ok ( rows. into_iter ( ) . next ( ) )
3050+ }
3051+
30163052 async fn log_surreal_edge_count ( & self , expected : usize ) {
30173053 let db = {
30183054 let storage = self . surreal . lock ( ) . await ;
@@ -3771,33 +3807,82 @@ impl ProjectIndexer {
37713807 watcher. watch ( & path, RecursiveMode :: Recursive ) ?;
37723808 info ! ( "Watching for changes in: {:?}" , path) ;
37733809
3774- use std:: collections:: HashMap ;
3775- use std:: time:: { Duration , Instant } ;
3776- let mut last_events: HashMap < PathBuf , Instant > = HashMap :: new ( ) ;
3810+ let mut last_events: std:: collections:: HashMap < PathBuf , std:: time:: Instant > =
3811+ std:: collections:: HashMap :: new ( ) ;
37773812
37783813 while let Some ( event) = rx. recv ( ) . await {
3779- match event. kind {
3780- EventKind :: Modify ( ModifyKind :: Data ( _) ) | EventKind :: Create ( _) => {
3781- for path in event. paths {
3782- if self . should_index ( & path) {
3783- let now = Instant :: now ( ) ;
3784- let entry = last_events. entry ( path. clone ( ) ) . or_insert ( now) ;
3785- if now. duration_since ( * entry) . as_millis ( ) as u64 >= debounce_ms {
3786- * entry = now;
3787- info ! ( "File changed: {:?}, reindexing (debounced)..." , path) ;
3814+ self . handle_file_event ( event, & mut last_events, debounce_ms) . await ;
3815+ }
3816+ Ok ( ( ) )
3817+ }
3818+ }
3819+
3820+ impl ProjectIndexer {
3821+ async fn handle_file_event (
3822+ & self ,
3823+ event : notify:: Event ,
3824+ last_events : & mut std:: collections:: HashMap < PathBuf , std:: time:: Instant > ,
3825+ debounce_ms : u64 ,
3826+ ) {
3827+ use notify:: event:: { EventKind , ModifyKind } ;
3828+ use std:: time:: Instant ;
3829+
3830+ match event. kind {
3831+ EventKind :: Modify ( ModifyKind :: Data ( _) ) | EventKind :: Create ( _) => {
3832+ for path in event. paths {
3833+ if self . should_index ( & path) {
3834+ let now = Instant :: now ( ) ;
3835+ match last_events. entry ( path. clone ( ) ) {
3836+ std:: collections:: hash_map:: Entry :: Vacant ( v) => {
3837+ v. insert ( now) ;
3838+ info ! ( "File changed: {:?}, reindexing..." , path) ;
37883839 if let Err ( e) = self . index_single_file ( & path) . await {
37893840 warn ! ( "Incremental reindex failed for {:?}: {}" , path, e) ;
37903841 }
3791- } else {
3792- debug ! ( "Debounced change for {:?}" , path) ;
3842+ if let Some ( tx) = WATCH_TEST_NOTIFIER
3843+ . get_or_init ( || Mutex :: new ( None ) )
3844+ . lock ( )
3845+ . unwrap ( )
3846+ . as_ref ( )
3847+ {
3848+ let _ = tx. send ( path. clone ( ) ) ;
3849+ }
3850+ }
3851+ std:: collections:: hash_map:: Entry :: Occupied ( mut entry) => {
3852+ if now. duration_since ( * entry. get ( ) ) . as_millis ( ) as u64 >= debounce_ms
3853+ {
3854+ * entry. get_mut ( ) = now;
3855+ info ! ( "File changed: {:?}, reindexing (debounced)..." , path) ;
3856+ if let Err ( e) = self . index_single_file ( & path) . await {
3857+ warn ! ( "Incremental reindex failed for {:?}: {}" , path, e) ;
3858+ }
3859+ if let Some ( tx) = WATCH_TEST_NOTIFIER
3860+ . get_or_init ( || Mutex :: new ( None ) )
3861+ . lock ( )
3862+ . unwrap ( )
3863+ . as_ref ( )
3864+ {
3865+ let _ = tx. send ( path. clone ( ) ) ;
3866+ }
3867+ } else {
3868+ debug ! ( "Debounced change for {:?}" , path) ;
3869+ }
37933870 }
37943871 }
37953872 }
37963873 }
3797- _ => { }
37983874 }
3875+ _ => { }
37993876 }
3800- Ok ( ( ) )
3877+ }
3878+
3879+ pub async fn simulate_file_event (
3880+ & self ,
3881+ event : notify:: Event ,
3882+ last_events : & mut std:: collections:: HashMap < PathBuf , std:: time:: Instant > ,
3883+ debounce_ms : u64 ,
3884+ ) {
3885+ self . handle_file_event ( event, last_events, debounce_ms) . await ;
38013886 }
38023887}
38033888
0 commit comments