@@ -913,3 +913,130 @@ fun testInsufficientFundsAndRecovery() {
913913 log (" - All " .concat (activeScheduleCount .toString ()).concat (" yield vaults have active schedules" ))
914914 log (" ========================================" )
915915}
916+
917+ /// Supervisor batch recovery: 200 stuck vaults, no capacity-probe loop.
918+ ///
919+ /// Flow: create 200 yield vaults, run 2 scheduling rounds, drain FLOW so executions fail,
920+ /// wait for vaults to be marked stuck, refund FLOW, schedule the supervisor, then advance
921+ /// time for ceil(200/MAX_BATCH_SIZE)+10 supervisor ticks. Asserts all 200 vaults are
922+ /// recovered (YieldVaultRecovered events), none still stuck, and all have active schedules.
923+ /// The +10 extra ticks are a buffer so every vault is processed despite scheduler timing.
924+ access (all )
925+ fun testSupervisorHandlesManyStuckVaults () {
926+ let n = 200
927+ let maxBatchSize = FlowYieldVaultsSchedulerRegistry .MAX_BATCH_SIZE
928+
929+ if snapshot ! = getCurrentBlockHeight () {
930+ Test .reset (to : snapshot )
931+ }
932+
933+ // 1. Setup: user, FLOW, and grant
934+ let user = Test .createAccount ()
935+ mintFlow (to : user , amount : 100000.0 )
936+ grantBeta (flowYieldVaultsAccount , user )
937+ mintFlow (to : flowYieldVaultsAccount , amount : 10000.0 )
938+
939+ // 2. Create n yield vaults in batch (Test.executeTransactions)
940+ var i = 0
941+ let tx = Test .Transaction (
942+ code : Test .readFile (" ../transactions/flow-yield-vaults/create_yield_vault.cdc" ),
943+ authorizers : [user .address ],
944+ signers : [user ],
945+ arguments : [strategyIdentifier , flowTokenIdentifier , 5.0 ]
946+ )
947+ let txs : [Test .Transaction ] = []
948+ while i < n {
949+ txs .append (tx )
950+ i = i + 1
951+ }
952+ let results = Test .executeTransactions (txs )
953+ for result in results {
954+ Test .expect (result , Test .beSucceeded ())
955+ }
956+ log (" testSupervisorHandlesManyStuckVaults: created \( n .toString ()) yield vaults" )
957+
958+ let yieldVaultIDs = getYieldVaultIDs (address : user .address )!
959+ Test .assert (yieldVaultIDs .length == n , message : " expected \( n .toString ()) vaults, got \( yieldVaultIDs .length .toString ()) " )
960+
961+ // 3. Two scheduling rounds so vaults run once
962+ setMockOraclePrice (signer : flowYieldVaultsAccount , forTokenIdentifier : flowTokenIdentifier , price : 1.5 )
963+ setMockOraclePrice (signer : flowYieldVaultsAccount , forTokenIdentifier : yieldTokenIdentifier , price : 1.2 )
964+ Test .moveTime (by : 60.0 * 10.0 + 10.0 )
965+ Test .commitBlock ()
966+ Test .moveTime (by : 60.0 * 10.0 + 10.0 )
967+ Test .commitBlock ()
968+
969+ // 4. Drain FLOW so subsequent executions fail and vaults become stuck
970+ let balanceBeforeDrain = (executeScript (
971+ " ../scripts/flow-yield-vaults/get_flow_balance.cdc" ,
972+ [flowYieldVaultsAccount .address ]
973+ ).returnValue ! as ! UFix64 )
974+ if balanceBeforeDrain > 0.01 {
975+ let drainRes = _executeTransaction (
976+ " ../transactions/flow-yield-vaults/drain_flow.cdc" ,
977+ [balanceBeforeDrain - 0.001 ],
978+ flowYieldVaultsAccount
979+ )
980+ Test .expect (drainRes , Test .beSucceeded ())
981+ }
982+ log (" testSupervisorHandlesManyStuckVaults: drained FLOW, waiting for vaults to be marked stuck" )
983+
984+ // 5. Wait rounds until vaults are marked stuck
985+ var waitRound = 0
986+ while waitRound < 6 {
987+ Test .moveTime (by : 60.0 * 10.0 + 10.0 )
988+ Test .commitBlock ()
989+ waitRound = waitRound + 1
990+ }
991+
992+ // 6. Refund FLOW and schedule supervisor
993+ mintFlow (to : flowYieldVaultsAccount , amount : 500.0 )
994+ Test .commitBlock ()
995+ Test .moveTime (by : 1.0 )
996+ Test .commitBlock ()
997+
998+ let interval = 60.0 * 10.0
999+ let schedSupRes = _executeTransaction (
1000+ " ../transactions/flow-yield-vaults/admin/schedule_supervisor.cdc" ,
1001+ [interval , UInt8 (1 ), UInt64 (5000 ), true ],
1002+ flowYieldVaultsAccount
1003+ )
1004+ Test .expect (schedSupRes , Test .beSucceeded ())
1005+
1006+ // 7. Advance time for supervisor ticks (ceil(n/MAX_BATCH_SIZE)+10); each tick processes a batch
1007+ let supervisorRunsNeeded = (UInt (n ) + UInt (maxBatchSize ) - 1 ) / UInt (maxBatchSize )
1008+ var run = 0 as UInt
1009+ while run < supervisorRunsNeeded + 10 {
1010+ Test .moveTime (by : 60.0 * 10.0 + 10.0 )
1011+ Test .commitBlock ()
1012+ run = run + 1
1013+ }
1014+ log (" testSupervisorHandlesManyStuckVaults: ran \( supervisorRunsNeeded + 10 ) .toString()) supervisor ticks" )
1015+
1016+ let recoveredEvents = Test .eventsOfType (Type <FlowYieldVaultsSchedulerV1 .YieldVaultRecovered >())
1017+ Test .assert (recoveredEvents .length > = n , message : " expected at least \( n .toString ()) recovered, got \( recoveredEvents .length .toString ()) " )
1018+ log (" testSupervisorHandlesManyStuckVaults: recovered \( recoveredEvents .length .toString ()) vaults" )
1019+
1020+ // 8. Health check: none stuck, all have active schedules
1021+ var stillStuck = 0
1022+ var activeCount = 0
1023+ for yieldVaultID in yieldVaultIDs {
1024+ let isStuckRes = executeScript (
1025+ " ../scripts/flow-yield-vaults/is_stuck_yield_vault.cdc" ,
1026+ [yieldVaultID ]
1027+ )
1028+ if isStuckRes .returnValue ! = nil && (isStuckRes .returnValue ! as ! Bool ) {
1029+ stillStuck = stillStuck + 1
1030+ }
1031+ let hasActiveRes = executeScript (
1032+ " ../scripts/flow-yield-vaults/has_active_schedule.cdc" ,
1033+ [yieldVaultID ]
1034+ )
1035+ if hasActiveRes .returnValue ! = nil && (hasActiveRes .returnValue ! as ! Bool ) {
1036+ activeCount = activeCount + 1
1037+ }
1038+ }
1039+ Test .assert (stillStuck == 0 , message : " expected 0 stuck, got \( stillStuck .toString ()) " )
1040+ Test .assert (activeCount == n , message : " expected \( n .toString ()) active, got \( activeCount .toString ()) " )
1041+ log (" testSupervisorHandlesManyStuckVaults: all \( n .toString ()) vaults healthy, active schedules: \( activeCount .toString ()) " )
1042+ }
0 commit comments