@@ -454,6 +454,87 @@ describe("config observe recovery", () => {
454454 } ) ;
455455 } ) ;
456456
457+ it ( "retries recovery on next launch after a failed copyFile restore" , async ( ) => {
458+ await withSuiteHome ( async ( home ) => {
459+ const { deps, configPath, auditPath, warn } = makeDeps ( home ) ;
460+ await seedConfigBackup ( configPath , recoverableTelegramConfig ) ;
461+ const clobbered = await writeClobberedUpdateChannel ( configPath ) ;
462+
463+ const copyError = Object . assign ( new Error ( "EACCES: permission denied" ) , { code : "EACCES" } ) ;
464+ const failingFs : ObserveRecoveryDeps [ "fs" ] = {
465+ ...deps . fs ,
466+ promises : {
467+ ...deps . fs . promises ,
468+ copyFile : ( ) => Promise . reject ( copyError ) ,
469+ } ,
470+ } ;
471+ await maybeRecoverSuspiciousConfigRead ( {
472+ deps : { ...deps , fs : failingFs } ,
473+ configPath,
474+ raw : clobbered . raw ,
475+ parsed : clobbered . parsed ,
476+ } ) ;
477+
478+ expectWarnContaining ( warn , "Config auto-restore from backup failed:" ) ;
479+ const firstEvents = await readObserveEvents ( auditPath ) ;
480+ expect ( firstEvents ) . toHaveLength ( 1 ) ;
481+ expect ( firstEvents [ 0 ] ?. restoredFromBackup ) . toBe ( false ) ;
482+
483+ const retryResult = await maybeRecoverSuspiciousConfigRead ( {
484+ deps,
485+ configPath,
486+ raw : clobbered . raw ,
487+ parsed : clobbered . parsed ,
488+ } ) ;
489+
490+ expect ( ( retryResult . parsed as { gateway ?: { mode ?: string } } ) . gateway ?. mode ) . toBe ( "local" ) ;
491+ await expect ( fsp . readFile ( configPath , "utf-8" ) ) . resolves . not . toBe ( clobbered . raw ) ;
492+ const retryEvents = await readObserveEvents ( auditPath ) ;
493+ expect ( retryEvents ) . toHaveLength ( 2 ) ;
494+ expect ( retryEvents [ 1 ] ?. restoredFromBackup ) . toBe ( true ) ;
495+ } ) ;
496+ } ) ;
497+
498+ it ( "sync recovery retries on next launch after a failed copyFileSync restore" , async ( ) => {
499+ await withSuiteHome ( async ( home ) => {
500+ const { deps, configPath, auditPath, warn } = makeDeps ( home ) ;
501+ await seedConfigBackup ( configPath , recoverableTelegramConfig ) ;
502+ const clobbered = await writeClobberedUpdateChannel ( configPath ) ;
503+
504+ const copyError = Object . assign ( new Error ( "EACCES: permission denied" ) , { code : "EACCES" } ) ;
505+ const failingFs : ObserveRecoveryDeps [ "fs" ] = {
506+ ...deps . fs ,
507+ copyFileSync : ( ) => {
508+ throw copyError ;
509+ } ,
510+ } ;
511+ maybeRecoverSuspiciousConfigReadSync ( {
512+ deps : { ...deps , fs : failingFs } ,
513+ configPath,
514+ raw : clobbered . raw ,
515+ parsed : clobbered . parsed ,
516+ } ) ;
517+
518+ expectWarnContaining ( warn , "Config auto-restore from backup failed:" ) ;
519+ const firstEvents = await readObserveEvents ( auditPath ) ;
520+ expect ( firstEvents ) . toHaveLength ( 1 ) ;
521+ expect ( firstEvents [ 0 ] ?. restoredFromBackup ) . toBe ( false ) ;
522+
523+ const retryResult = maybeRecoverSuspiciousConfigReadSync ( {
524+ deps,
525+ configPath,
526+ raw : clobbered . raw ,
527+ parsed : clobbered . parsed ,
528+ } ) ;
529+
530+ expect ( ( retryResult . parsed as { gateway ?: { mode ?: string } } ) . gateway ?. mode ) . toBe ( "local" ) ;
531+ await expect ( fsp . readFile ( configPath , "utf-8" ) ) . resolves . not . toBe ( clobbered . raw ) ;
532+ const retryEvents = await readObserveEvents ( auditPath ) ;
533+ expect ( retryEvents ) . toHaveLength ( 2 ) ;
534+ expect ( retryEvents [ 1 ] ?. restoredFromBackup ) . toBe ( true ) ;
535+ } ) ;
536+ } ) ;
537+
457538 it ( "dedupes repeated suspicious hashes" , async ( ) => {
458539 await withSuiteHome ( async ( home ) => {
459540 const { deps, configPath, auditPath } = makeDeps ( home ) ;
0 commit comments