@@ -469,3 +469,115 @@ describe('e2e: callable metadata — cost varies with invocation args', () => {
469469 expect ( swapAFn ) . not . toHaveBeenCalled ( ) ;
470470 } ) ;
471471} ) ;
472+
473+ // ---------------------------------------------------------------------------
474+ // E2E: lift retry — first candidate throws, sheaf recovers to fallback
475+ // ---------------------------------------------------------------------------
476+
477+ describe ( 'e2e: lift retry on handler failure' , ( ) => {
478+ it ( 'recovers to next candidate when first throws, lift receives non-empty errors' , async ( ) => {
479+ type RouteMeta = { priority : number } ;
480+
481+ const primaryFn = vi . fn ( ( _acct : string ) : number => {
482+ throw new Error ( 'primary unavailable' ) ;
483+ } ) ;
484+ const fallbackFn = vi . fn ( ( _acct : string ) : number => 99 ) ;
485+
486+ const sections : PresheafSection < RouteMeta > [ ] = [
487+ {
488+ exo : makeSection (
489+ 'Primary' ,
490+ M . interface ( 'Primary' , {
491+ getBalance : M . call ( M . string ( ) ) . returns ( M . number ( ) ) ,
492+ } ) ,
493+ { getBalance : primaryFn } ,
494+ ) ,
495+ metadata : constant ( { priority : 0 } ) ,
496+ } ,
497+ {
498+ exo : makeSection (
499+ 'Fallback' ,
500+ M . interface ( 'Fallback' , {
501+ getBalance : M . call ( M . string ( ) ) . returns ( M . number ( ) ) ,
502+ } ) ,
503+ { getBalance : fallbackFn } ,
504+ ) ,
505+ metadata : constant ( { priority : 1 } ) ,
506+ } ,
507+ ] ;
508+
509+ // Track the error-array length the lift receives after each failed attempt.
510+ const errorCountsSeenByLift : number [ ] = [ ] ;
511+ const priorityFirst : Lift < RouteMeta > = async function * ( germs ) {
512+ const ordered = [ ...germs ] . sort (
513+ ( a , b ) => ( a . metadata ?. priority ?? 0 ) - ( b . metadata ?. priority ?? 0 ) ,
514+ ) ;
515+ for ( const germ of ordered ) {
516+ const errors : unknown [ ] = yield germ ;
517+ errorCountsSeenByLift . push ( errors . length ) ;
518+ }
519+ } ;
520+
521+ const wallet = sheafify ( { name : 'Wallet' , sections } ) . getGlobalSection ( {
522+ lift : priorityFirst ,
523+ } ) ;
524+
525+ const result = await E ( wallet ) . getBalance ( 'alice' ) ;
526+
527+ // fallback succeeded and both handlers were invoked
528+ expect ( result ) . toBe ( 99 ) ;
529+ expect ( primaryFn ) . toHaveBeenCalledWith ( 'alice' ) ;
530+ expect ( fallbackFn ) . toHaveBeenCalledWith ( 'alice' ) ;
531+
532+ // after the primary failed the lift received an errors array with one entry
533+ expect ( errorCountsSeenByLift ) . toHaveLength ( 1 ) ;
534+ expect ( errorCountsSeenByLift [ 0 ] ) . toBe ( 1 ) ;
535+ } ) ;
536+
537+ it ( 'throws accumulated errors when all candidates fail' , async ( ) => {
538+ type RouteMeta = { priority : number } ;
539+
540+ const sections : PresheafSection < RouteMeta > [ ] = [
541+ {
542+ exo : makeSection (
543+ 'A' ,
544+ M . interface ( 'A' , {
545+ getBalance : M . call ( M . string ( ) ) . returns ( M . number ( ) ) ,
546+ } ) ,
547+ {
548+ getBalance : ( _acct : string ) : number => {
549+ throw new Error ( 'A failed' ) ;
550+ } ,
551+ } ,
552+ ) ,
553+ metadata : constant ( { priority : 0 } ) ,
554+ } ,
555+ {
556+ exo : makeSection (
557+ 'B' ,
558+ M . interface ( 'B' , {
559+ getBalance : M . call ( M . string ( ) ) . returns ( M . number ( ) ) ,
560+ } ) ,
561+ {
562+ getBalance : ( _acct : string ) : number => {
563+ throw new Error ( 'B failed' ) ;
564+ } ,
565+ } ,
566+ ) ,
567+ metadata : constant ( { priority : 1 } ) ,
568+ } ,
569+ ] ;
570+
571+ const wallet = sheafify ( { name : 'Wallet' , sections } ) . getGlobalSection ( {
572+ async * lift ( germs ) {
573+ yield * [ ...germs ] . sort (
574+ ( a , b ) => ( a . metadata ?. priority ?? 0 ) - ( b . metadata ?. priority ?? 0 ) ,
575+ ) ;
576+ } ,
577+ } ) ;
578+
579+ await expect ( E ( wallet ) . getBalance ( 'alice' ) ) . rejects . toThrow (
580+ 'No viable section' ,
581+ ) ;
582+ } ) ;
583+ } ) ;
0 commit comments