@@ -139,9 +139,16 @@ impl<A: Adapter> Engine<A> {
139139 . as_ref ( )
140140 . is_some_and ( |old| self . adapter . same ( old, & target) . unwrap_or ( false ) ) ;
141141 let uninterrupted = epoch == self . activity ;
142- if !same || !uninterrupted || reset || !healthy {
142+ if !same || !uninterrupted || reset {
143143 self . clear ( ) ;
144144 }
145+ if !healthy {
146+ self . fallback . clear ( ) ;
147+ self . boundary = false ;
148+ if self . snapshot . as_ref ( ) . is_some_and ( |raw| raw. position == -1 ) {
149+ self . clear ( ) ;
150+ }
151+ }
145152 self . activity = epoch;
146153 if same && uninterrupted && !reset && self . tracked && healthy {
147154 if let Some ( text) = edit {
@@ -369,6 +376,7 @@ mod tests {
369376 target : usize ,
370377 protected : bool ,
371378 unsupported : bool ,
379+ editable : bool ,
372380 reads : usize ,
373381 change_on_read : bool ,
374382 }
@@ -385,6 +393,7 @@ mod tests {
385393 target : 1 ,
386394 protected : false ,
387395 unsupported : false ,
396+ editable : true ,
388397 reads : 0 ,
389398 change_on_read : false ,
390399 }
@@ -399,7 +408,7 @@ mod tests {
399408 Ok ( self . protected )
400409 }
401410 fn editable ( & mut self , _: & usize ) -> bool {
402- true
411+ self . editable
403412 }
404413 fn same ( & mut self , a : & usize , b : & usize ) -> Result < bool , Status > {
405414 Ok ( a == b)
@@ -423,6 +432,100 @@ mod tests {
423432 e
424433 }
425434 #[ test]
435+ fn native_predictions_survive_polls_without_fallback_tracking ( ) {
436+ for unavailable in 0 ..3 {
437+ let mut e = engine ( ) ;
438+ match unavailable {
439+ 0 => e. tracked = false ,
440+ 1 => e. adapter . editable = false ,
441+ _ => e. observe = || ( 0 , false ) ,
442+ }
443+ let mut service = crate :: prediction:: Service :: default ( ) ;
444+ let mut keyboard = crate :: scan_keyboard:: Keyboard :: new ( false ) ;
445+ keyboard. enable_predictions ( true ) ;
446+ let mut original = None ;
447+ for _ in 0 ..40 {
448+ let response = e. respond ( Request :: Query {
449+ generation : 0 ,
450+ edit : service. edit . take ( ) ,
451+ reset : std:: mem:: take ( & mut service. reset ) ,
452+ shift : false ,
453+ caps : false ,
454+ } ) ;
455+ let Response :: Suggestions {
456+ batch, tracking, ..
457+ } = response
458+ else {
459+ panic ! ( "expected suggestions" ) ;
460+ } ;
461+ assert ! ( !tracking) ;
462+ let batch = batch. unwrap ( ) ;
463+ let expected = original. get_or_insert_with ( || batch. clone ( ) ) ;
464+ assert_eq ! ( batch. token, expected. token) ;
465+ assert_eq ! ( batch. words, expected. words) ;
466+ service. suggestions ( & mut keyboard, Some ( batch) , tracking) ;
467+ keyboard. advance ( 250 , 1000 ) ;
468+ }
469+ let token = original. unwrap ( ) . token ;
470+ assert_eq ! ( e. accept( token, 0 ) , Some ( "ter " . into( ) ) ) ;
471+ assert ! ( e. accept( token, 0 ) . is_none( ) ) ;
472+ }
473+ }
474+ #[ test]
475+ fn genuine_context_changes_invalidate_identical_native_candidates ( ) {
476+ for change in 0 ..4 {
477+ let mut e = engine ( ) ;
478+ e. adapter . editable = false ;
479+ let original = e. query ( None , false , false , false ) . unwrap ( ) ;
480+ match change {
481+ 0 => e. adapter . target += 1 ,
482+ 1 => e. adapter . raw . position += 1 ,
483+ 2 => e. activity = u64:: MAX ,
484+ _ => { }
485+ }
486+ let replacement = e. query ( None , change == 3 , false , false ) . unwrap ( ) ;
487+ assert_eq ! ( replacement. words, original. words) ;
488+ assert_ne ! ( replacement. token, original. token) ;
489+ assert ! ( e. accept( original. token, 0 ) . is_none( ) ) ;
490+ assert_eq ! ( e. accept( replacement. token, 0 ) , Some ( "ter " . into( ) ) ) ;
491+ }
492+ }
493+ #[ test]
494+ fn native_edits_selection_and_casing_refresh_without_tracking ( ) {
495+ let mut e = engine ( ) ;
496+ e. tracked = false ;
497+ let original = e. query ( None , false , false , false ) . unwrap ( ) ;
498+ e. adapter . raw . before . push ( 't' ) ;
499+ let typed = e. query ( None , false , false , false ) . unwrap ( ) ;
500+ assert_ne ! ( typed. token, original. token) ;
501+ assert ! ( e. accept( original. token, 0 ) . is_none( ) ) ;
502+ e. adapter . raw . before . pop ( ) ;
503+ let deleted = e. query ( None , false , false , false ) . unwrap ( ) ;
504+ assert_ne ! ( deleted. token, typed. token) ;
505+ let shifted = e. query ( None , false , true , false ) . unwrap ( ) ;
506+ assert_ne ! ( shifted. token, deleted. token) ;
507+ assert ! ( e. accept( deleted. token, 0 ) . is_none( ) ) ;
508+ e. adapter . raw . has_selection = true ;
509+ assert ! ( e. query( None , false , false , false ) . is_none( ) ) ;
510+ assert ! ( e. accept( shifted. token, 0 ) . is_none( ) ) ;
511+ e. adapter . raw . has_selection = false ;
512+ let restored = e. query ( None , false , false , false ) . unwrap ( ) ;
513+ e. adapter . protected = true ;
514+ assert ! ( e. query( None , false , false , false ) . is_none( ) ) ;
515+ assert ! ( e. accept( restored. token, 0 ) . is_none( ) ) ;
516+ }
517+ #[ test]
518+ fn losing_tracking_does_not_erase_a_pending_context_reset ( ) {
519+ let mut service = crate :: prediction:: Service {
520+ reset : true ,
521+ edit : Some ( "queued" . into ( ) ) ,
522+ ..Default :: default ( )
523+ } ;
524+ service. suggestions ( & mut crate :: scan_keyboard:: Keyboard :: new ( false ) , None , false ) ;
525+ assert ! ( service. reset) ;
526+ assert ! ( service. edit. is_none( ) ) ;
527+ }
528+ #[ test]
426529 fn only_suffix_and_space_are_returned_once ( ) {
427530 let mut e = engine ( ) ;
428531 let b = e. query ( None , false , false , false ) . unwrap ( ) ;
0 commit comments