@@ -124,12 +124,35 @@ impl Locator for WindowsRegistry {
124124
125125 #[ cfg( windows) ]
126126 fn find ( & self , reporter : & dyn Reporter ) {
127- // We no longer reset `search_result` here: `find_with_cache` already
128- // returns the cached result on a hit, and `find()` is invoked on
129- // transient locators per refresh, so the cache is empty by
130- // construction the first time. Re-clearing forced every refresh to
131- // re-walk both registry hives, each of which is intercepted by
132- // Windows Defender (issue #454).
127+ // We no longer reset `search_result` here: the cache may have been
128+ // populated via `sync_refresh_state_from` between refreshes, and
129+ // `find()` is invoked on transient locators per refresh, so on the
130+ // first refresh the cache is empty by construction. Re-clearing
131+ // forced every refresh to re-walk both registry hives, each of
132+ // which is intercepted by Windows Defender (issue #454).
133+ //
134+ // On a cache hit `get_registry_pythons` is NOT called, so we must
135+ // replay the cached environments/managers to the reporter
136+ // ourselves — otherwise WindowsRegistry discoveries would silently
137+ // disappear on every refresh after the first.
138+ let cached = {
139+ let result = self
140+ . search_result
141+ . lock ( )
142+ . expect ( "search_result mutex poisoned" ) ;
143+ result. clone ( )
144+ } ;
145+ if let Some ( cached) = cached {
146+ for manager in & cached. managers {
147+ reporter. report_manager ( manager) ;
148+ }
149+ for env in & cached. environments {
150+ reporter. report_environment ( env) ;
151+ }
152+ return ;
153+ }
154+ // Cache miss: walk the registry. `get_registry_pythons` reports
155+ // inline as it discovers entries, so no separate replay is needed.
133156 let _ = self . find_with_cache ( Some ( reporter) ) ;
134157 }
135158 #[ cfg( unix) ]
@@ -328,26 +351,48 @@ mod tests {
328351 /// implementation called `self.clear()` first, which forced every
329352 /// `refresh` RPC to re-walk both registry hives — a Defender-intercepted
330353 /// hot path tracked in #454. This test pins down the new contract:
331- /// pre-populate the cache, run `find()`, and assert the original entries
332- /// survived (i.e. `find_with_cache` short-circuited on the cache hit).
354+ /// pre-populate the cache, run `find()`, and assert (a) the original
355+ /// entries survived (i.e. the cache was not cleared) and (b) the
356+ /// reporter was notified with the cached environments and managers,
357+ /// so cached results are still observable to refresh consumers.
333358 #[ cfg( windows) ]
334359 #[ test]
335360 fn test_find_reuses_cached_results_within_locator_lifetime ( ) {
336361 use pet_core:: manager:: EnvManager ;
337362 use pet_core:: python_environment:: PythonEnvironment ;
338363 use pet_core:: reporter:: Reporter ;
339364 use pet_core:: telemetry:: TelemetryEvent ;
365+ use std:: sync:: Mutex ;
340366
341- struct CountingReporter ;
342- impl Reporter for CountingReporter {
343- fn report_manager ( & self , _manager : & EnvManager ) { }
344- fn report_environment ( & self , _env : & PythonEnvironment ) { }
367+ #[ derive( Default ) ]
368+ struct RecordingReporter {
369+ environments : Mutex < Vec < String > > ,
370+ managers : Mutex < Vec < PathBuf > > ,
371+ }
372+ impl Reporter for RecordingReporter {
373+ fn report_manager ( & self , manager : & EnvManager ) {
374+ self . managers
375+ . lock ( )
376+ . unwrap ( )
377+ . push ( manager. executable . clone ( ) ) ;
378+ }
379+ fn report_environment ( & self , env : & PythonEnvironment ) {
380+ self . environments
381+ . lock ( )
382+ . unwrap ( )
383+ . push ( env. name . clone ( ) . unwrap_or_default ( ) ) ;
384+ }
345385 fn report_telemetry ( & self , _event : & TelemetryEvent ) { }
346386 }
347387
348388 let locator = create_locator ( ) ;
389+ let cached_manager = EnvManager :: new (
390+ PathBuf :: from ( "C:\\ fake\\ python.exe" ) ,
391+ pet_core:: manager:: EnvManagerType :: Conda ,
392+ None ,
393+ ) ;
349394 let cached = LocatorResult {
350- managers : vec ! [ ] ,
395+ managers : vec ! [ cached_manager . clone ( ) ] ,
351396 environments : vec ! [ PythonEnvironment {
352397 name: Some ( "cached" . to_string( ) ) ,
353398 ..Default :: default ( )
@@ -359,8 +404,10 @@ mod tests {
359404 . unwrap ( )
360405 . replace ( cached. clone ( ) ) ;
361406
362- locator. find ( & CountingReporter ) ;
407+ let reporter = RecordingReporter :: default ( ) ;
408+ locator. find ( & reporter) ;
363409
410+ // (a) The cache must still be populated and unchanged.
364411 let after = locator
365412 . search_result
366413 . lock ( )
@@ -373,6 +420,19 @@ mod tests {
373420 "find() must not clear the cache before populating" ,
374421 ) ;
375422 assert_eq ! ( after. environments[ 0 ] . name. as_deref( ) , Some ( "cached" ) ) ;
423+ // (b) The cached entries must have been replayed to the reporter
424+ // — otherwise WindowsRegistry discoveries would silently
425+ // disappear on every refresh after the first.
426+ assert_eq ! (
427+ reporter. environments. lock( ) . unwrap( ) . as_slice( ) ,
428+ & [ "cached" . to_string( ) ] ,
429+ "find() must replay cached environments to the reporter on a cache hit" ,
430+ ) ;
431+ assert_eq ! (
432+ reporter. managers. lock( ) . unwrap( ) . as_slice( ) ,
433+ & [ PathBuf :: from( "C:\\ fake\\ python.exe" ) ] ,
434+ "find() must replay cached managers to the reporter on a cache hit" ,
435+ ) ;
376436 }
377437
378438 /// Smoke test: on a fresh locator (empty cache), `find()` runs the new
0 commit comments