2525 update_vaults_state ,
2626)
2727from src .common .typings import HarvestParams
28+ from src .meta_vault .exceptions import ClaimDelayNotPassedException
2829from src .meta_vault .typings import SubVaultRedemption
2930from src .redemptions .os_token_converter import OsTokenConverter
3031from src .redemptions .typings import OsTokenPosition
@@ -462,41 +463,72 @@ class TestUpdateVaultsState:
462463 async def test_no_vaults (self ) -> None :
463464 with _mock_update_vaults_state () as mocks :
464465 await update_vaults_state (vaults = [], block_number = BlockNumber (100 ))
465- mocks ['is_meta_vault ' ].assert_not_called ()
466+ mocks ['update_state ' ].assert_not_called ()
466467 mocks ['harvest_params' ].assert_not_called ()
467468
468469 @pytest .mark .parametrize (
469470 'needs_update, expected_calls' ,
470471 [(True , 1 ), (False , 0 )],
471472 ids = ['needs_update' , 'up_to_date' ],
472473 )
473- async def test_meta_vault_process_tree_gated_by_needs_update (
474+ async def test_meta_vault_update_state_gated_by_needs_update (
474475 self , needs_update : bool , expected_calls : int
475476 ) -> None :
476- """process_meta_vault_tree runs iff is_meta_vault_state_update_required is True.
477- When it does, graph_get_vaults' result is forwarded by identity."""
478- meta_vaults_map = {VAULT_1 : MagicMock ()}
477+ """meta_vault_tree_update_state runs iff is_meta_vault_state_update_required is True.
478+ When it does, the corresponding Vault entry from meta_vaults_map is forwarded
479+ by identity along with the full meta_vaults_map."""
480+ root_meta_vault = MagicMock ()
481+ meta_vaults_map = {VAULT_1 : root_meta_vault }
479482 with _mock_update_vaults_state (
480- is_meta_vault = True ,
481483 needs_update = needs_update ,
482484 meta_vaults_map = meta_vaults_map ,
483485 ) as mocks :
484486 await update_vaults_state (vaults = [VAULT_1 ], block_number = BlockNumber (100 ))
485487
486- assert mocks ['process_tree ' ].await_count == expected_calls
488+ assert mocks ['update_state ' ].await_count == expected_calls
487489 if needs_update :
488490 mocks ['graph_get_vaults' ].assert_awaited_once_with (is_meta_vault = True )
489- assert mocks ['process_tree' ].await_args .kwargs ['vault' ] == VAULT_1
490- assert mocks ['process_tree' ].await_args .kwargs ['meta_vaults_map' ] is meta_vaults_map
491+ assert mocks ['update_state' ].await_args .kwargs ['root_meta_vault' ] is root_meta_vault
492+ assert mocks ['update_state' ].await_args .kwargs ['meta_vaults_map' ] is meta_vaults_map
493+
494+ async def test_meta_vault_no_sub_vaults_skipped (self ) -> None :
495+ """A meta vault with no sub-vaults is skipped — nothing to update."""
496+ empty_meta_vault = MagicMock ()
497+ empty_meta_vault .sub_vaults = []
498+ with _mock_update_vaults_state (
499+ meta_vaults_map = {VAULT_1 : empty_meta_vault },
500+ ) as mocks :
501+ await update_vaults_state (vaults = [VAULT_1 ], block_number = BlockNumber (100 ))
502+ mocks ['update_state' ].assert_not_called ()
491503
492- async def test_meta_vault_process_tree_failure_raises (self ) -> None :
493- """A failure inside process_meta_vault_tree aborts the round."""
504+ async def test_meta_vault_update_state_failure_raises (self ) -> None :
505+ """A failure inside meta_vault_tree_update_state aborts the round."""
494506 with _mock_update_vaults_state (
495- is_meta_vault = True , process_tree_exception = RuntimeError ('boom' )
507+ meta_vaults_map = {VAULT_1 : MagicMock ()},
508+ update_state_exception = RuntimeError ('boom' ),
496509 ):
497- with pytest .raises (RuntimeError , match = 'Failed to process meta vault tree' ):
510+ with pytest .raises (RuntimeError , match = 'Failed to update meta vault tree state ' ):
498511 await update_vaults_state (vaults = [VAULT_1 ], block_number = BlockNumber (100 ))
499512
513+ async def test_meta_vault_claim_delay_logged_and_continues (self ) -> None :
514+ """ClaimDelayNotPassedException is caught, logged, and does not abort the round.
515+
516+ Regular vaults batched alongside the meta vault are still processed.
517+ """
518+ exit_request = MagicMock ()
519+ exit_request .vault = VAULT_1
520+ exit_request .position_ticket = 1234
521+ with _mock_update_vaults_state (
522+ meta_vaults_map = {VAULT_1 : MagicMock ()},
523+ harvest_params = {VAULT_2 : make_harvest_params ()},
524+ update_state_exception = ClaimDelayNotPassedException (exit_request ),
525+ ) as mocks :
526+ await update_vaults_state (vaults = [VAULT_1 , VAULT_2 ], block_number = BlockNumber (100 ))
527+ mocks ['update_state' ].assert_awaited_once ()
528+ mocks ['multicall' ].tx_aggregate .assert_awaited_once_with (
529+ [(VAULT_2 , ENCODED_UPDATE_STATE_CALL )]
530+ )
531+
500532 @pytest .mark .parametrize (
501533 'has_params, expected_multicall_calls' ,
502534 [(True , 1 ), (False , 0 )],
@@ -507,9 +539,7 @@ async def test_regular_vault_multicall_gated_by_harvest_params(
507539 ) -> None :
508540 """A None entry in get_multiple_harvest_params skips that vault from the multicall."""
509541 params : HarvestParams | None = make_harvest_params () if has_params else None
510- with _mock_update_vaults_state (
511- is_meta_vault = False , harvest_params = {VAULT_1 : params }
512- ) as mocks :
542+ with _mock_update_vaults_state (harvest_params = {VAULT_1 : params }) as mocks :
513543 await update_vaults_state (vaults = [VAULT_1 ], block_number = BlockNumber (100 ))
514544
515545 assert mocks ['multicall' ].tx_aggregate .await_count == expected_multicall_calls
@@ -521,26 +551,28 @@ async def test_regular_vault_multicall_gated_by_harvest_params(
521551 async def test_multicall_tx_failure_raises (self ) -> None :
522552 """A failed multicall receipt aborts the round."""
523553 with _mock_update_vaults_state (
524- is_meta_vault = False ,
525554 harvest_params = {VAULT_1 : make_harvest_params ()},
526555 multicall_tx_status = 0 ,
527556 ):
528557 with pytest .raises (RuntimeError , match = 'Update State multicall tx failed' ):
529558 await update_vaults_state (vaults = [VAULT_1 ], block_number = BlockNumber (100 ))
530559
531560 async def test_mix_of_meta_and_regular_vaults (self ) -> None :
532- """Meta vault is harvested via process_meta_vault_tree ; regular vaults are
561+ """Meta vault is harvested via meta_vault_tree_update_state ; regular vaults are
533562 batched into a single multicall. Harvest params are fetched only for the
534563 regular vaults."""
535564 params = make_harvest_params ()
565+ meta_vaults_map = {VAULT_1 : MagicMock ()}
536566 with _mock_update_vaults_state (
537- is_meta_vault = { VAULT_1 : True , VAULT_2 : False } ,
567+ meta_vaults_map = meta_vaults_map ,
538568 harvest_params = {VAULT_2 : params },
539569 ) as mocks :
540570 await update_vaults_state (vaults = [VAULT_1 , VAULT_2 ], block_number = BlockNumber (100 ))
541571
542- mocks ['process_tree' ].assert_awaited_once ()
543- assert mocks ['process_tree' ].await_args .kwargs ['vault' ] == VAULT_1
572+ mocks ['update_state' ].assert_awaited_once ()
573+ assert (
574+ mocks ['update_state' ].await_args .kwargs ['root_meta_vault' ] is meta_vaults_map [VAULT_1 ]
575+ )
544576 mocks ['harvest_params' ].assert_awaited_once_with ([VAULT_2 ], BlockNumber (100 ))
545577 mocks ['multicall' ].tx_aggregate .assert_awaited_once_with (
546578 [(VAULT_2 , ENCODED_UPDATE_STATE_CALL )]
@@ -960,37 +992,27 @@ def _mock_submit_redeem_position(
960992
961993@contextmanager
962994def _mock_update_vaults_state (
963- is_meta_vault : bool | dict [ChecksumAddress , bool ] = False ,
964995 needs_update : bool = True ,
965996 harvest_params : dict [ChecksumAddress , HarvestParams | None ] | None = None ,
966997 meta_vaults_map : dict | None = None ,
967- process_tree_exception : BaseException | None = None ,
998+ update_state_exception : BaseException | None = None ,
968999 multicall_tx_status : int = 1 ,
9691000) -> Iterator [dict [str , MagicMock ]]:
9701001 """Mock setup for update_vaults_state tests.
9711002
972- ``is_meta_vault`` may be a bool (applied to every address) or a per-address
973- mapping. ``harvest_params`` is the dict returned by get_multiple_harvest_params;
974- a None value for a vault skips it from the multicall (production behavior).
975- ``process_tree_exception`` makes process_meta_vault_tree raise.
1003+ ``meta_vaults_map`` is the dict returned by graph_get_vaults; addresses present
1004+ in this map are treated as meta vaults by update_vaults_state. ``harvest_params``
1005+ is the dict returned by get_multiple_harvest_params; a None value for a vault
1006+ skips it from the multicall (production behavior). ``update_state_exception``
1007+ makes meta_vault_tree_update_state raise.
9761008 """
9771009 meta_vaults_map = {} if meta_vaults_map is None else meta_vaults_map
9781010 harvest_params = {} if harvest_params is None else harvest_params
9791011
980- if isinstance (is_meta_vault , dict ):
981- is_meta_lookup = is_meta_vault
982-
983- async def is_meta_side_effect (addr : ChecksumAddress ) -> bool :
984- return is_meta_lookup .get (addr , False )
985-
986- is_meta_mock : AsyncMock = AsyncMock (side_effect = is_meta_side_effect )
987- else :
988- is_meta_mock = AsyncMock (return_value = is_meta_vault )
989-
990- if process_tree_exception is not None :
991- process_tree_mock = AsyncMock (side_effect = process_tree_exception )
1012+ if update_state_exception is not None :
1013+ update_state_mock = AsyncMock (side_effect = update_state_exception )
9921014 else :
993- process_tree_mock = AsyncMock ()
1015+ update_state_mock = AsyncMock ()
9941016
9951017 def vault_factory (addr : ChecksumAddress ) -> MagicMock :
9961018 mock_vault = MagicMock ()
@@ -1003,12 +1025,11 @@ def vault_factory(addr: ChecksumAddress) -> MagicMock:
10031025 f'{ MODULE } .graph_get_vaults' ,
10041026 new = AsyncMock (return_value = meta_vaults_map ),
10051027 ) as mock_graph ,
1006- patch (f'{ MODULE } .is_meta_vault' , new = is_meta_mock ),
10071028 patch (
10081029 f'{ MODULE } .is_meta_vault_state_update_required' ,
10091030 new = AsyncMock (return_value = needs_update ),
10101031 ),
1011- patch (f'{ MODULE } .process_meta_vault_tree ' , new = process_tree_mock ),
1032+ patch (f'{ MODULE } .meta_vault_tree_update_state ' , new = update_state_mock ),
10121033 patch (
10131034 f'{ MODULE } .get_multiple_harvest_params' ,
10141035 new = AsyncMock (return_value = harvest_params ),
@@ -1018,8 +1039,7 @@ def vault_factory(addr: ChecksumAddress) -> MagicMock:
10181039 ):
10191040 yield {
10201041 'graph_get_vaults' : mock_graph ,
1021- 'is_meta_vault' : is_meta_mock ,
1022- 'process_tree' : process_tree_mock ,
1042+ 'update_state' : update_state_mock ,
10231043 'harvest_params' : mock_harvest_params ,
10241044 'multicall' : multicall_mocks ['mock_multicall' ],
10251045 'vault_cls' : mock_vault_cls ,
0 commit comments