@@ -23,12 +23,15 @@ import {
2323 ffAnchor ,
2424 ffReverseAnchor ,
2525 sfAnchor ,
26- sfReverseAnchor
26+ sfReverseAnchor ,
27+ MAX_WORKING_SPAN_DAYS
2728} from '@hcengineering/gantt'
2829import type { Issue , IssueRelation } from '@hcengineering/tracker'
2930import type { Ref } from '@hcengineering/core'
3031import type { DragState , DragTarget , PrimaryEdit } from '../types'
3132
33+ const DAY_MS = 86_400_000
34+
3235function issue ( id : string , start ?: number , due ?: number ) : Issue {
3336 return {
3437 _id : id as Ref < Issue > ,
@@ -512,6 +515,8 @@ describe('simulateCascade — working-days mode', () => {
512515 if ( res . kind !== 'cascade' ) return
513516 // fsAnchor(Fri May 22, 1 wd, Mo-Fr) = Mon May 25.
514517 expect ( res . shifts [ 0 ] . newStart ) . toBe ( Date . UTC ( 2026 , 4 , 25 ) )
518+ // B keeps its five working days: Mon May 25 .. Fri May 29.
519+ expect ( res . shifts [ 0 ] . newDue ) . toBe ( Date . UTC ( 2026 , 4 , 29 ) )
515520 } )
516521
517522 it ( 'FS lag=2 with Mo-Fr cfg: 2 working days after Friday = Wednesday next week' , ( ) => {
@@ -524,6 +529,8 @@ describe('simulateCascade — working-days mode', () => {
524529 if ( res . kind !== 'cascade' ) return
525530 // fsAnchor(Fri May 22, (1 + 2) wd, Mo-Fr) = Wed May 27.
526531 expect ( res . shifts [ 0 ] . newStart ) . toBe ( Date . UTC ( 2026 , 4 , 27 ) )
532+ // B keeps its five working days: Wed May 27 .. Tue Jun 2.
533+ expect ( res . shifts [ 0 ] . newDue ) . toBe ( Date . UTC ( 2026 , 5 , 2 ) )
527534 } )
528535
529536 it ( 'legacy (cfg=undefined): FS push uses calendar days with the +1-day rule' , ( ) => {
@@ -564,6 +571,8 @@ describe('simulateCascade — working-days mode', () => {
564571 // fsReverseAnchor(Mon May 25, 0, cfg) = previous Fri May 22 → A.newDue = May 22.
565572 expect ( res . shifts [ 0 ] . issue . _id ) . toBe ( 'A' )
566573 expect ( res . shifts [ 0 ] . newDue ) . toBe ( Date . UTC ( 2026 , 4 , 22 ) )
574+ // A keeps its six working days (Mon Jun 1 .. Mon Jun 8): Fri May 15 .. Fri May 22.
575+ expect ( res . shifts [ 0 ] . newStart ) . toBe ( Date . UTC ( 2026 , 4 , 15 ) )
567576 } )
568577
569578 it ( 'FS gap floor uses working days: drag A Fri→Mon (1 wd) pushes B by 1 working day, not 3 calendar days' , ( ) => {
@@ -656,23 +665,27 @@ describe('simulateCascade — working-days mode', () => {
656665 } )
657666
658667 it ( 'Test 6: multi-hop FS chain — floor dominates the second hop and propagates through the cascade' , ( ) => {
659- // A: 1-day Thu 14. B: Fri 15 .. Mon 18 (weekend spanner, zero slack: fsAnchor(Thu 14) = Fri 15).
660- // C: 1-day Wed 20 (1 wd slack: fsAnchor(Mon 18) = Tue 19). Primary: A → Sat 16 (pinned non-working).
668+ // A: 1-day Thu 14. B: Fri 15 .. Mon 18 (weekend spanner, two working days, zero slack:
669+ // fsAnchor(Thu 14) = Fri 15). C: 1-day Wed 20 (1 wd slack: fsAnchor(Mon 18) = Tue 19).
670+ // Primary: A → Mon 18.
661671 const A = issue ( 'A' , Date . UTC ( 2026 , 4 , 14 ) , Date . UTC ( 2026 , 4 , 14 ) )
662672 const B = issue ( 'B' , Date . UTC ( 2026 , 4 , 15 ) , Date . UTC ( 2026 , 4 , 18 ) )
663673 const C = issue ( 'C' , Date . UTC ( 2026 , 4 , 20 ) , Date . UTC ( 2026 , 4 , 20 ) )
664674 const relations = [ rel ( 'A' , 'B' , 'finish-to-start' , 0 ) , rel ( 'B' , 'C' , 'finish-to-start' , 0 ) ]
665- const primary : PrimaryEdit [ ] = [ { issue : A , newStart : Date . UTC ( 2026 , 4 , 16 ) , newDue : Date . UTC ( 2026 , 4 , 16 ) } ]
675+ const primary : PrimaryEdit [ ] = [ { issue : A , newStart : Date . UTC ( 2026 , 4 , 18 ) , newDue : Date . UTC ( 2026 , 4 , 18 ) } ]
666676 const res = simulateCascade ( primary , [ A , B , C ] , relations , ( ) => true , { workingDays : cfgMonFri } )
667677 expect ( res . kind ) . toBe ( 'cascade' )
668678 if ( res . kind !== 'cascade' ) return
669679 const byId = new Map ( res . shifts . map ( ( s ) => [ s . issue . _id , s ] ) )
670- // Hop 1 (A→B): snap = floor = Mon 18 (anchor case) → B keeps its 3-day span → Mon 18 .. Thu 21.
671- expect ( byId . get ( 'B' as Ref < Issue > ) ?. newStart ) . toBe ( Date . UTC ( 2026 , 4 , 18 ) )
672- expect ( byId . get ( 'B' as Ref < Issue > ) ?. newDue ) . toBe ( Date . UTC ( 2026 , 4 , 21 ) )
673- // Hop 2 (B→C): floor addWorkingDays(Wed 20, 3) = Mon 25 > snap fsAnchor(Thu 21) = Fri 22 → floor wins.
674- expect ( byId . get ( 'C' as Ref < Issue > ) ?. newStart ) . toBe ( Date . UTC ( 2026 , 4 , 25 ) )
675- expect ( byId . get ( 'C' as Ref < Issue > ) ?. newDue ) . toBe ( Date . UTC ( 2026 , 4 , 25 ) )
680+ // Hop 1 (A→B): floor addWorkingDays(Fri 15, workingDayDelta(Thu 14, Mon 18) = 2) = Tue 19
681+ // = snap fsAnchor(Mon 18) → B keeps its two working days → Tue 19 .. Wed 20.
682+ expect ( byId . get ( 'B' as Ref < Issue > ) ?. newStart ) . toBe ( Date . UTC ( 2026 , 4 , 19 ) )
683+ expect ( byId . get ( 'B' as Ref < Issue > ) ?. newDue ) . toBe ( Date . UTC ( 2026 , 4 , 20 ) )
684+ // Hop 2 (B→C): floor addWorkingDays(Wed 20, workingDayDelta(Mon 18, Wed 20) = 2) = Fri 22
685+ // > snap fsAnchor(Wed 20) = Thu 21 → floor wins.
686+ expect ( byId . get ( 'C' as Ref < Issue > ) ?. newStart ) . toBe ( Date . UTC ( 2026 , 4 , 22 ) )
687+ expect ( byId . get ( 'C' as Ref < Issue > ) ?. newDue ) . toBe ( Date . UTC ( 2026 , 4 , 22 ) )
688+ expect ( res . shifts . some ( ( s ) => s . reason === 'pull-predecessor' ) ) . toBe ( false )
676689 expect ( isWorkingDay ( byId . get ( 'C' as Ref < Issue > ) ?. newStart as number , cfgMonFri ) ) . toBe ( true )
677690 } )
678691
@@ -754,7 +767,7 @@ describe('simulateCascade — working-days mode: reverse pass must not undo a sa
754767 const cfg = { weekdayMask : 0b0011111 , holidays : [ Date . UTC ( 2026 , 9 , 22 ) ] }
755768 const oct = ( d : number ) : number => Date . UTC ( 2026 , 9 , d )
756769
757- it ( 'chain push A→B→C where B ends on a Saturday: B keeps Fri 16–Sat 17, no pull-predecessor ' , ( ) => {
770+ it ( 'chain push A→B→C: B keeps its two working days ( Fri 16–Mon 19), C follows on Tue 20–Wed 21 ' , ( ) => {
758771 // A Mon 5–Fri 9, B Mon 12–Tue 13, C Wed 14–Thu 15 (FS chain).
759772 const A = issue ( 'A' , oct ( 5 ) , oct ( 9 ) )
760773 const B = issue ( 'B' , oct ( 12 ) , oct ( 13 ) )
@@ -766,10 +779,11 @@ describe('simulateCascade — working-days mode: reverse pass must not undo a sa
766779 expect ( res . kind ) . toBe ( 'cascade' )
767780 if ( res . kind !== 'cascade' ) return
768781 const byId = new Map ( res . shifts . map ( ( s ) => [ s . issue . _id as string , s ] ) )
769- // fsAnchor(Thu 15) = Fri 16 → B Fri 16–Sat 17.
770- expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : oct ( 16 ) , newDue : oct ( 17 ) , reason : 'push-successor' } )
771- // fsAnchor(Sat 17) = Mon 19 → C Mon 19–Tue 20.
772- expect ( byId . get ( 'C' ) ) . toMatchObject ( { newStart : oct ( 19 ) , newDue : oct ( 20 ) , reason : 'push-successor' } )
782+ // fsAnchor(Thu 15) = Fri 16 → B keeps two working days: Fri 16–Mon 19.
783+ expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : oct ( 16 ) , newDue : oct ( 19 ) , reason : 'push-successor' } )
784+ // fsAnchor(Mon 19) = Tue 20 = floor addWorkingDays(Wed 14, workingDayDelta(Tue 13, Mon 19) = 4)
785+ // → C Tue 20–Wed 21; fsAnchor(B.due Mon 19) = Tue 20 = C.start, so no pull.
786+ expect ( byId . get ( 'C' ) ) . toMatchObject ( { newStart : oct ( 20 ) , newDue : oct ( 21 ) , reason : 'push-successor' } )
773787 expect ( res . shifts . some ( ( s ) => s . reason === 'pull-predecessor' ) ) . toBe ( false )
774788 } )
775789
@@ -783,16 +797,193 @@ describe('simulateCascade — working-days mode: reverse pass must not undo a sa
783797 } )
784798
785799 it ( "successor dragged onto the predecessor's Friday (real violation) → predecessor pulled to Wed 14–Thu 15" , ( ) => {
786- const B = issue ( 'B' , oct ( 16 ) , oct ( 17 ) )
800+ // B Thu 15–Fri 16 (two working days).
801+ const B = issue ( 'B' , oct ( 15 ) , oct ( 16 ) )
787802 const C = issue ( 'C' , oct ( 21 ) , oct ( 23 ) )
788803 const primary : PrimaryEdit [ ] = [ { issue : C , newStart : oct ( 16 ) , newDue : oct ( 19 ) } ]
789804 const res = simulateCascade ( primary , [ B , C ] , [ rel ( 'B' , 'C' ) ] , ( ) => true , { workingDays : cfg } )
790805 expect ( res . kind ) . toBe ( 'cascade' )
791806 if ( res . kind !== 'cascade' ) return
792- // fsReverseAnchor(Fri 16) = Thu 15 → B shifted back by 2 calendar days .
807+ // fsReverseAnchor(Fri 16) = Thu 15 → B keeps two working days: Wed 14–Thu 15 .
793808 expect ( res . shifts ) . toHaveLength ( 1 )
794809 expect ( res . shifts [ 0 ] ) . toMatchObject ( { newStart : oct ( 14 ) , newDue : oct ( 15 ) , reason : 'pull-predecessor' } )
795810 } )
811+
812+ it ( 'a pulled predecessor stored on Fri 16–Sat 17 becomes a one-working-day bar (Thu 15)' , ( ) => {
813+ const B = issue ( 'B' , oct ( 16 ) , oct ( 17 ) )
814+ const C = issue ( 'C' , oct ( 21 ) , oct ( 23 ) )
815+ const primary : PrimaryEdit [ ] = [ { issue : C , newStart : oct ( 16 ) , newDue : oct ( 19 ) } ]
816+ const res = simulateCascade ( primary , [ B , C ] , [ rel ( 'B' , 'C' ) ] , ( ) => true , { workingDays : cfg } )
817+ expect ( res . kind ) . toBe ( 'cascade' )
818+ if ( res . kind !== 'cascade' ) return
819+ // workingDaySpan(Fri 16, Sat 17) = 1 → B Thu 15–Thu 15.
820+ expect ( res . shifts ) . toHaveLength ( 1 )
821+ expect ( res . shifts [ 0 ] ) . toMatchObject ( { newStart : oct ( 15 ) , newDue : oct ( 15 ) , reason : 'pull-predecessor' } )
822+ } )
823+ } )
824+
825+ describe ( 'simulateCascade — working-days mode: shifted issues keep their working-day span' , ( ) => {
826+ const cfgMonFri = { weekdayMask : 0b0011111 , holidays : [ ] }
827+ const may = ( d : number ) : number => Date . UTC ( 2026 , 4 , d )
828+ const jun = ( d : number ) : number => Date . UTC ( 2026 , 5 , d )
829+
830+ function run (
831+ issues : Issue [ ] ,
832+ relations : IssueRelation [ ] ,
833+ primary : PrimaryEdit [ ] ,
834+ workingDays : { weekdayMask : number , holidays : number [ ] } | null = cfgMonFri
835+ ) : Map < string , { newStart : number , newDue : number , reason : string } > {
836+ const res = simulateCascade (
837+ primary ,
838+ issues ,
839+ relations ,
840+ ( ) => true ,
841+ workingDays === null ? undefined : { workingDays }
842+ )
843+ expect ( res . kind ) . toBe ( 'cascade' )
844+ if ( res . kind !== 'cascade' ) return new Map ( )
845+ return new Map ( res . shifts . map ( ( s ) => [ s . issue . _id as string , s ] ) )
846+ }
847+
848+ it ( 'push keeps a five-working-day successor at five working days across the weekend' , ( ) => {
849+ const A = issue ( 'A' , may ( 18 ) , may ( 22 ) )
850+ const B = issue ( 'B' , may ( 25 ) , may ( 29 ) )
851+ const byId = run ( [ A , B ] , [ rel ( 'A' , 'B' ) ] , [ { issue : A , newStart : may ( 20 ) , newDue : may ( 26 ) } ] )
852+ expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : may ( 27 ) , newDue : jun ( 2 ) , reason : 'push-successor' } )
853+ expect ( isWorkingDay ( byId . get ( 'B' ) ?. newDue as number , cfgMonFri ) ) . toBe ( true )
854+ } )
855+
856+ it ( 'FF push across a weekend anchors the due date and keeps the span' , ( ) => {
857+ const A = issue ( 'A' , may ( 18 ) , may ( 20 ) )
858+ const B = issue ( 'B' , may ( 18 ) , may ( 20 ) )
859+ const byId = run ( [ A , B ] , [ rel ( 'A' , 'B' , 'finish-to-finish' ) ] , [ { issue : A , newStart : may ( 18 ) , newDue : may ( 26 ) } ] )
860+ expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : may ( 22 ) , newDue : may ( 26 ) , reason : 'push-successor' } )
861+ } )
862+
863+ it ( 'FS pull across a weekend keeps the predecessor span' , ( ) => {
864+ // A Thu 14 – Mon 18 = three working days.
865+ const A = issue ( 'A' , may ( 14 ) , may ( 18 ) )
866+ const B = issue ( 'B' , may ( 19 ) , may ( 20 ) )
867+ const res = simulateCascade (
868+ [ { issue : B , newStart : may ( 11 ) , newDue : may ( 12 ) } ] ,
869+ [ A , B ] ,
870+ [ rel ( 'A' , 'B' ) ] ,
871+ ( ) => true ,
872+ { workingDays : cfgMonFri }
873+ )
874+ expect ( res . kind ) . toBe ( 'cascade' )
875+ if ( res . kind !== 'cascade' ) return
876+ expect ( res . shifts ) . toHaveLength ( 1 )
877+ expect ( res . shifts [ 0 ] ) . toMatchObject ( { newStart : may ( 6 ) , newDue : may ( 8 ) , reason : 'pull-predecessor' } )
878+ } )
879+
880+ it ( 'a pushed bar steps over a holiday' , ( ) => {
881+ const cfgHol = { weekdayMask : 0b0011111 , holidays : [ may ( 21 ) ] }
882+ const A = issue ( 'A' , may ( 15 ) , may ( 15 ) )
883+ const B = issue ( 'B' , may ( 18 ) , may ( 19 ) )
884+ const byId = run ( [ A , B ] , [ rel ( 'A' , 'B' ) ] , [ { issue : A , newStart : may ( 19 ) , newDue : may ( 19 ) } ] , cfgHol )
885+ expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : may ( 20 ) , newDue : may ( 22 ) , reason : 'push-successor' } )
886+ } )
887+
888+ it ( 'a bar stored on non-working days becomes a one-working-day bar when pushed' , ( ) => {
889+ const A = issue ( 'A' , may ( 15 ) , may ( 15 ) )
890+ const B = issue ( 'B' , may ( 16 ) , may ( 17 ) )
891+ const byId = run ( [ A , B ] , [ rel ( 'A' , 'B' ) ] , [ { issue : A , newStart : may ( 18 ) , newDue : may ( 18 ) } ] )
892+ expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : may ( 19 ) , newDue : may ( 19 ) , reason : 'push-successor' } )
893+ } )
894+
895+ it ( 'an inverted bar becomes at least a one-day bar when pushed' , ( ) => {
896+ const A = issue ( 'A' , may ( 18 ) , may ( 18 ) )
897+ const B = issue ( 'B' , may ( 19 ) , may ( 18 ) )
898+ const byId = run ( [ A , B ] , [ rel ( 'A' , 'B' ) ] , [ { issue : A , newStart : may ( 19 ) , newDue : may ( 19 ) } ] )
899+ expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : may ( 20 ) , newDue : may ( 20 ) , reason : 'push-successor' } )
900+ } )
901+
902+ it ( 'a bar beyond the working-span cap keeps its calendar-day length when pushed' , ( ) => {
903+ const A = issue ( 'A' , may ( 18 ) , may ( 18 ) )
904+ const bStart = may ( 19 )
905+ const bDue = bStart + MAX_WORKING_SPAN_DAYS * DAY_MS
906+ const B = issue ( 'B' , bStart , bDue )
907+ const byId = run ( [ A , B ] , [ rel ( 'A' , 'B' ) ] , [ { issue : A , newStart : may ( 21 ) , newDue : may ( 21 ) } ] )
908+ expect ( byId . get ( 'B' ) ) . toMatchObject ( {
909+ newStart : may ( 22 ) ,
910+ newDue : bDue + 3 * DAY_MS ,
911+ reason : 'push-successor'
912+ } )
913+ expect ( ( byId . get ( 'B' ) ?. newDue as number ) - ( byId . get ( 'B' ) ?. newStart as number ) ) . toBe ( bDue - bStart )
914+ } )
915+
916+ it ( 'SS push across a weekend keeps the successor span' , ( ) => {
917+ const A = issue ( 'A' , may ( 18 ) , may ( 20 ) )
918+ const B = issue ( 'B' , may ( 18 ) , may ( 19 ) )
919+ const byId = run ( [ A , B ] , [ rel ( 'A' , 'B' , 'start-to-start' ) ] , [ { issue : A , newStart : may ( 22 ) , newDue : may ( 26 ) } ] )
920+ expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : may ( 22 ) , newDue : may ( 25 ) , reason : 'push-successor' } )
921+ } )
922+
923+ it ( 'SF push across a weekend anchors the due date and keeps the span' , ( ) => {
924+ const A = issue ( 'A' , may ( 18 ) , may ( 20 ) )
925+ const B = issue ( 'B' , may ( 11 ) , may ( 12 ) )
926+ const byId = run ( [ A , B ] , [ rel ( 'A' , 'B' , 'start-to-finish' ) ] , [ { issue : A , newStart : may ( 25 ) , newDue : may ( 27 ) } ] )
927+ expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : may ( 22 ) , newDue : may ( 25 ) , reason : 'push-successor' } )
928+ } )
929+
930+ function pullOnly (
931+ A : Issue ,
932+ B : Issue ,
933+ kind : 'start-to-start' | 'finish-to-finish' | 'start-to-finish' ,
934+ primary : PrimaryEdit ,
935+ workingDays : { weekdayMask : number , holidays : number [ ] } | null = cfgMonFri
936+ ) : { newStart : number , newDue : number , reason : string } | undefined {
937+ const res = simulateCascade (
938+ [ primary ] ,
939+ [ A , B ] ,
940+ [ rel ( 'A' , 'B' , kind ) ] ,
941+ ( ) => true ,
942+ workingDays === null ? undefined : { workingDays }
943+ )
944+ expect ( res . kind ) . toBe ( 'cascade' )
945+ if ( res . kind !== 'cascade' ) return undefined
946+ expect ( res . shifts ) . toHaveLength ( 1 )
947+ return res . shifts [ 0 ]
948+ }
949+
950+ it ( 'SS pull across a weekend keeps the predecessor span' , ( ) => {
951+ // A Thu 21 – Mon 25 = three working days.
952+ const A = issue ( 'A' , may ( 21 ) , may ( 25 ) )
953+ const B = issue ( 'B' , may ( 21 ) , may ( 22 ) )
954+ const s = pullOnly ( A , B , 'start-to-start' , { issue : B , newStart : may ( 18 ) , newDue : may ( 19 ) } )
955+ expect ( s ) . toMatchObject ( { newStart : may ( 18 ) , newDue : may ( 20 ) , reason : 'pull-predecessor' } )
956+ } )
957+
958+ it ( 'SF pull across a weekend keeps the predecessor span' , ( ) => {
959+ // A Fri 22 – Tue 26 = three working days.
960+ const A = issue ( 'A' , may ( 22 ) , may ( 26 ) )
961+ const B = issue ( 'B' , may ( 20 ) , may ( 22 ) )
962+ const s = pullOnly ( A , B , 'start-to-finish' , { issue : B , newStart : may ( 18 ) , newDue : may ( 20 ) } )
963+ expect ( s ) . toMatchObject ( { newStart : may ( 20 ) , newDue : may ( 22 ) , reason : 'pull-predecessor' } )
964+ } )
965+
966+ it ( 'FF pull across a weekend keeps the predecessor span' , ( ) => {
967+ // A Thu 21 – Mon 25 = three working days.
968+ const A = issue ( 'A' , may ( 21 ) , may ( 25 ) )
969+ const B = issue ( 'B' , may ( 25 ) , may ( 27 ) )
970+ const s = pullOnly ( A , B , 'finish-to-finish' , { issue : B , newStart : may ( 18 ) , newDue : may ( 20 ) } )
971+ expect ( s ) . toMatchObject ( { newStart : may ( 18 ) , newDue : may ( 20 ) , reason : 'pull-predecessor' } )
972+ } )
973+
974+ it ( 'legacy mode keeps the calendar-day length on push' , ( ) => {
975+ const A = issue ( 'A' , may ( 18 ) , may ( 22 ) )
976+ const B = issue ( 'B' , may ( 25 ) , may ( 29 ) )
977+ const byId = run ( [ A , B ] , [ rel ( 'A' , 'B' ) ] , [ { issue : A , newStart : may ( 20 ) , newDue : may ( 26 ) } ] , null )
978+ expect ( byId . get ( 'B' ) ) . toMatchObject ( { newStart : may ( 27 ) , newDue : may ( 31 ) , reason : 'push-successor' } )
979+ } )
980+
981+ it ( 'legacy mode keeps the calendar-day length on pull' , ( ) => {
982+ const A = issue ( 'A' , may ( 21 ) , may ( 25 ) )
983+ const B = issue ( 'B' , may ( 21 ) , may ( 22 ) )
984+ const s = pullOnly ( A , B , 'start-to-start' , { issue : B , newStart : may ( 18 ) , newDue : may ( 19 ) } , null )
985+ expect ( s ) . toMatchObject ( { newStart : may ( 18 ) , newDue : may ( 22 ) , reason : 'pull-predecessor' } )
986+ } )
796987} )
797988
798989describe ( 'simulateCascade — legacy mode: reverse-pass guard is a no-op' , ( ) => {
0 commit comments